diff --git a/resolvespec-js/package.json b/resolvespec-js/package.json new file mode 100644 index 0000000..348b8b9 --- /dev/null +++ b/resolvespec-js/package.json @@ -0,0 +1,71 @@ +{ + "name": "@warkypublic/resolvespec-js", + "version": "1.0.0", + "description": "Client side library for the ResolveSpec API", + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.ts", + "publishConfig": { + "access": "public", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "files": [ + "dist", + "bin", + "README.md" + ], + "scripts": { + "build": "vite build", + "clean": "rm -rf dist", + "prepublishOnly": "npm run build", + "test": "vitest run", + "lint": "eslint src" + }, + "keywords": [ + "string", + "blob", + "dependencies", + "workspace", + "package", + "cli", + "tools", + "npm", + "yarn", + "pnpm" + ], + "author": "Hein (Warkanum) Puth", + "license": "MIT", + "dependencies": { + "semver": "^7.6.3", + "uuid": "^11.0.3" + }, + "devDependencies": { + "@changesets/cli": "^2.27.10", + "@eslint/js": "^9.16.0", + "@types/jsdom": "^21.1.7", + "eslint": "^9.16.0", + "globals": "^15.13.0", + "jsdom": "^25.0.1", + "typescript": "^5.7.2", + "typescript-eslint": "^8.17.0", + "vite": "^6.0.2", + "vite-plugin-dts": "^4.3.0", + "vitest": "^2.1.8" + }, + "engines": { + "node": ">=14.16" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Warky-Devs/ResolveSpec" + }, + "bugs": { + "url": "https://github.com/Warky-Devs/ResolveSpec/issues" + }, + "homepage": "https://github.com/Warky-Devs/ResolveSpec#readme", + "packageManager": "pnpm@9.6.0+sha512.38dc6fba8dba35b39340b9700112c2fe1e12f10b17134715a4aa98ccf7bb035e76fd981cf0bb384dfa98f8d6af5481c2bef2f4266a24bfa20c34eb7147ce0b5e" + } + \ No newline at end of file diff --git a/resolvespec-js/src/api.ts b/resolvespec-js/src/api.ts new file mode 100644 index 0000000..9c548bd --- /dev/null +++ b/resolvespec-js/src/api.ts @@ -0,0 +1,132 @@ +import { ClientConfig, APIResponse, TableMetadata, Options, RequestBody } from "./types"; + +// Helper functions +const getHeaders = (options?: Record): HeadersInit => { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + + if (options?.token) { + headers['Authorization'] = `Bearer ${options.token}`; + } + + return headers; +}; + +const buildUrl = (config: ClientConfig, schema: string, entity: string, id?: string): string => { + let url = `${config.baseUrl}/${schema}/${entity}`; + if (id) { + url += `/${id}`; + } + return url; +}; + +const fetchWithError = async (url: string, options: RequestInit): Promise> => { + try { + const response = await fetch(url, options); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error?.message || 'An error occurred'); + } + + return data; + } catch (error) { + throw error; + } +}; + +// API Functions +export const getMetadata = async ( + config: ClientConfig, + schema: string, + entity: string +): Promise> => { + const url = buildUrl(config, schema, entity); + return fetchWithError(url, { + method: 'GET', + headers: getHeaders(config), + }); +}; + +export const read = async ( + config: ClientConfig, + schema: string, + entity: string, + id?: string, + options?: Options +): Promise> => { + const url = buildUrl(config, schema, entity, id); + const body: RequestBody = { + operation: 'read', + options, + }; + + return fetchWithError(url, { + method: 'POST', + headers: getHeaders(config), + body: JSON.stringify(body), + }); +}; + +export const create = async ( + config: ClientConfig, + schema: string, + entity: string, + data: any | any[], + options?: Options +): Promise> => { + const url = buildUrl(config, schema, entity); + const body: RequestBody = { + operation: 'create', + data, + options, + }; + + return fetchWithError(url, { + method: 'POST', + headers: getHeaders(config), + body: JSON.stringify(body), + }); +}; + +export const update = async ( + config: ClientConfig, + schema: string, + entity: string, + data: any | any[], + id?: string | string[], + options?: Options +): Promise> => { + const url = buildUrl(config, schema, entity, typeof id === 'string' ? id : undefined); + const body: RequestBody = { + operation: 'update', + id: typeof id === 'string' ? undefined : id, + data, + options, + }; + + return fetchWithError(url, { + method: 'POST', + headers: getHeaders(config), + body: JSON.stringify(body), + }); +}; + +export const deleteEntity = async ( + config: ClientConfig, + schema: string, + entity: string, + id: string +): Promise> => { + const url = buildUrl(config, schema, entity, id); + const body: RequestBody = { + operation: 'delete', + }; + + return fetchWithError(url, { + method: 'POST', + headers: getHeaders(config), + body: JSON.stringify(body), + }); +}; diff --git a/resolvespec-js/src/examples.ts b/resolvespec-js/src/examples.ts new file mode 100644 index 0000000..3c1e014 --- /dev/null +++ b/resolvespec-js/src/examples.ts @@ -0,0 +1,68 @@ +import { getMetadata, read, create, update, deleteEntity } from "./api"; +import { ClientConfig } from "./types"; + +// Usage Examples +const config: ClientConfig = { + baseUrl: 'http://api.example.com/v1', + token: 'your-token-here' +}; + +// Example usage +const examples = async () => { + // Get metadata + const metadata = await getMetadata(config, 'test', 'employees'); + + + // Read with relations + const employees = await read(config, 'test', 'employees', undefined, { + preload: [ + { + relation: 'department', + columns: ['id', 'name'] + } + ], + filters: [ + { + column: 'status', + operator: 'eq', + value: 'active' + } + ] + }); + + // Create single record + const newEmployee = await create(config, 'test', 'employees', { + first_name: 'John', + last_name: 'Doe', + email: 'john@example.com' + }); + + // Bulk create + const newEmployees = await create(config, 'test', 'employees', [ + { + first_name: 'Jane', + last_name: 'Smith', + email: 'jane@example.com' + }, + { + first_name: 'Bob', + last_name: 'Johnson', + email: 'bob@example.com' + } + ]); + + // Update single record + const updatedEmployee = await update(config, 'test', 'employees', + { status: 'inactive' }, + 'emp123' + ); + + // Bulk update + const updatedEmployees = await update(config, 'test', 'employees', + { department_id: 'dept2' }, + ['emp1', 'emp2', 'emp3'] + ); + + // Delete + await deleteEntity(config, 'test', 'employees', 'emp123'); +}; \ No newline at end of file diff --git a/resolvespec-js/src/index.ts b/resolvespec-js/src/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/resolvespec-js/src/types.ts b/resolvespec-js/src/types.ts new file mode 100644 index 0000000..18ae382 --- /dev/null +++ b/resolvespec-js/src/types.ts @@ -0,0 +1,86 @@ +// Types +export type Operator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'ilike' | 'in'; +export type Operation = 'read' | 'create' | 'update' | 'delete'; +export type SortDirection = 'asc' | 'desc'; + +export interface PreloadOption { + relation: string; + columns?: string[]; + filters?: FilterOption[]; +} + +export interface FilterOption { + column: string; + operator: Operator; + value: any; +} + +export interface SortOption { + column: string; + direction: SortDirection; +} + +export interface CustomOperator { + name: string; + sql: string; +} + +export interface ComputedColumn { + name: string; + expression: string; +} + +export interface Options { + preload?: PreloadOption[]; + columns?: string[]; + filters?: FilterOption[]; + sort?: SortOption[]; + limit?: number; + offset?: number; + customOperators?: CustomOperator[]; + computedColumns?: ComputedColumn[]; +} + +export interface RequestBody { + operation: Operation; + id?: string | string[]; + data?: any | any[]; + options?: Options; +} + +export interface APIResponse { + success: boolean; + data: T; + metadata?: { + total: number; + filtered: number; + limit: number; + offset: number; + }; + error?: { + code: string; + message: string; + details?: any; + }; +} + +export interface Column { + name: string; + type: string; + is_nullable: boolean; + is_primary: boolean; + is_unique: boolean; + has_index: boolean; +} + +export interface TableMetadata { + schema: string; + table: string; + columns: Column[]; + relations: string[]; +} + +export interface ClientConfig { + baseUrl: string; + token?: string; +} diff --git a/resolvespec-python/todo.md b/resolvespec-python/todo.md new file mode 100644 index 0000000..8a82c1f --- /dev/null +++ b/resolvespec-python/todo.md @@ -0,0 +1,5 @@ +# Python Implementation of the ResolveSpec API + +# Server + +# Client \ No newline at end of file