diff --git a/README.md b/README.md index fd379c0..fffef5f 100644 --- a/README.md +++ b/README.md @@ -75,4 +75,5 @@ const unregister = poly.myWebhookContext.paymentReceieved(event => { unregister(); ``` -Happy hacking! \ No newline at end of file +Happy hacking! +. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8584eaa..b1ccffa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "polyapi", - "version": "0.24.20", + "version": "0.25.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "polyapi", - "version": "0.24.20", + "version": "0.25.7", "license": "MIT", "dependencies": { "@guanghechen/helper-string": "4.7.1", @@ -14,7 +14,7 @@ "@typescript-eslint/eslint-plugin": "8.32.1", "@typescript-eslint/parser": "8.32.1", "adm-zip": "0.5.16", - "axios": "1.8.3", + "axios": "1.12.0", "chalk": "4.1.2", "comment-json": "4.2.3", "dotenv": "16.0.3", @@ -2352,13 +2352,13 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", - "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", + "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -8865,9 +8865,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "axios": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", - "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", + "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", "requires": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", diff --git a/package.json b/package.json index 5c17d92..d875448 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "polyapi", - "version": "0.24.20", + "version": "0.25.7", "description": "Poly is a CLI tool to help create and manage your Poly definitions.", "license": "MIT", "repository": { @@ -51,7 +51,7 @@ "@typescript-eslint/eslint-plugin": "8.32.1", "@typescript-eslint/parser": "8.32.1", "adm-zip": "0.5.16", - "axios": "1.8.3", + "axios": "1.12.0", "chalk": "4.1.2", "comment-json": "4.2.3", "dotenv": "16.0.3", diff --git a/src/api.ts b/src/api.ts index e7290d5..4632dda 100644 --- a/src/api.ts +++ b/src/api.ts @@ -98,7 +98,8 @@ export const createOrUpdateServerFunction = async ( code: string, visibility: string, typeSchemas: Record, - requirements: string[], + externalDependencies: Record | undefined, + internalDependencies: Record> | undefined, other?: Record, executionApiKey?: string, ) => { @@ -112,7 +113,10 @@ export const createOrUpdateServerFunction = async ( code, visibility, typeSchemas, - requirements, + // Keeping backwards compatability on requirements + requirements: externalDependencies ? Object.keys(externalDependencies) : null, + externalDependencies, + internalDependencies, executionApiKey, ...other, }, @@ -177,6 +181,8 @@ export const createOrUpdateClientFunction = async ( code: string, visibility: string, typeSchemas: Record, + externalDependencies: Record | undefined, + internalDependencies: Record> | undefined, other?: Record, ) => { return ( @@ -189,6 +195,8 @@ export const createOrUpdateClientFunction = async ( code, visibility, typeSchemas, + externalDependencies, + internalDependencies, ...other, }, { diff --git a/src/cli.ts b/src/cli.ts index 43f78c0..7fca459 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -71,10 +71,11 @@ void yargs demandOption: false, type: 'string', }, - functionIds: { - describe: 'Function IDs to generate', + ids: { + describe: 'Resource IDs to generate', demandOption: false, type: 'string', + alias: 'functionIds', }, customPath: { describe: 'Custom path to .poly directory (internal use only)', @@ -93,7 +94,7 @@ void yargs exitWhenNoConfig, contexts, names, - functionIds, + ids, customPath = DEFAULT_POLY_PATH, noTypes = false, }) => { @@ -113,7 +114,7 @@ void yargs polyPath: customPath, contexts: contexts?.split(','), names: names?.split(','), - functionIds: functionIds?.split(','), + ids: ids?.split(','), noTypes, }); }, diff --git a/src/commands/function.ts b/src/commands/function.ts index e54c4ea..09dc620 100644 --- a/src/commands/function.ts +++ b/src/commands/function.ts @@ -79,6 +79,7 @@ export const addOrUpdateCustomFunction = async ( } const typeSchemas = generateTypeSchemas(file, DeployableTypeEntries.map(d => d[0]), name); + const [externalDependencies, internalDependencies] = await getDependencies(code, file, tsConfigBaseUrl); if (server) { shell.echo( @@ -86,8 +87,7 @@ export const addOrUpdateCustomFunction = async ( `${updating ? 'Updating' : 'Adding'} custom server side function...`, ); - const dependencies = getDependencies(code, file, tsConfigBaseUrl); - if (dependencies.length) { + if (externalDependencies) { shell.echo( chalk.yellow( 'Please note that deploying your functions will take a few minutes because it makes use of libraries other than polyapi.', @@ -106,7 +106,8 @@ export const addOrUpdateCustomFunction = async ( code, visibility, typeSchemas, - dependencies, + externalDependencies, + internalDependencies, other, executionApiKey, ); @@ -142,6 +143,8 @@ export const addOrUpdateCustomFunction = async ( code, visibility, typeSchemas, + externalDependencies, + internalDependencies, ); shell.echo(chalk.green('DONE')); shell.echo(`Client Function ID: ${customFunction.id}`); @@ -149,7 +152,7 @@ export const addOrUpdateCustomFunction = async ( await generateSingleCustomFunction(polyPath, customFunction.id, updating); } catch (e) { - shell.echo(chalk.red('ERROR\n')); - shell.echo(`${e.response?.data?.message || e.message}`); + shell.echo(chalk.redBright('ERROR\n')); + shell.echo(chalk.red((e instanceof Error ? e.message : e.response?.data?.message) || 'Unexpected error.')); } }; diff --git a/src/commands/generate/index.ts b/src/commands/generate/index.ts index 5c9fca8..7d701fc 100644 --- a/src/commands/generate/index.ts +++ b/src/commands/generate/index.ts @@ -8,6 +8,7 @@ import { ApiFunctionSpecification, AuthFunctionSpecification, CustomFunctionSpecification, + GraphQLSubscriptionSpecification, ServerFunctionSpecification, ServerVariableSpecification, Specification, @@ -17,10 +18,9 @@ import { import { getSpecs } from '../../api'; import { loadConfig, addOrUpdateConfig } from '../../config'; import { - generateContextDataFile, - getContextDataFileContent, + writeCachedSpecs, + getCachedSpecs, getPolyLibPath, - getSpecsFromContextData, showErrGettingSpecs, getStringPaths, loadTemplate, @@ -53,8 +53,9 @@ const getApiBaseUrl = () => const getApiKey = () => process.env.POLY_API_KEY; -const prepareDir = async (polyPath: string) => { - const libPath = getPolyLibPath(polyPath); +const prepareDir = async (polyPath: string, temp = false) => { + let libPath = getPolyLibPath(polyPath); + if (temp) libPath = libPath.replace('/lib', '/temp'); fs.rmSync(libPath, { recursive: true, force: true }); fs.mkdirSync(libPath, { recursive: true }); @@ -62,6 +63,7 @@ const prepareDir = async (polyPath: string) => { fs.mkdirSync(`${libPath}/client`); fs.mkdirSync(`${libPath}/auth`); fs.mkdirSync(`${libPath}/webhooks`); + fs.mkdirSync(`${libPath}/subscriptions`); fs.mkdirSync(`${libPath}/server`); fs.mkdirSync(`${libPath}/vari`); fs.mkdirSync(`${libPath}/tabi`); @@ -144,6 +146,9 @@ const generateJSFiles = async ( const tables = specs.filter( (spec) => spec.type === 'table', ) as TableSpecification[]; + const gqlSubscriptions = specs.filter( + (spec) => spec.type === 'graphqlSubscription', + ) as GraphQLSubscriptionSpecification[]; await generateIndexJSFile(libPath); await generatePolyCustomJSFile(libPath); @@ -158,6 +163,7 @@ const generateJSFiles = async ( 'custom functions', ); await tryAsync(generateWebhooksJSFiles(libPath, webhookHandles), 'webhooks'); + await tryAsync(generateGraphQLSubscriptionJSFiles(libPath, gqlSubscriptions), 'GraphQL subscriptions'); await tryAsync( generateAuthFunctionJSFiles(libPath, authFunctions), 'auth functions', @@ -284,6 +290,21 @@ const generateWebhooksJSFiles = async ( fs.copyFileSync(templateUrl('webhooks-index.js'), `${libPath}/webhooks/index.js`); }; +const generateGraphQLSubscriptionJSFiles = async ( + libPath: string, + specifications: GraphQLSubscriptionSpecification[], +) => { + const template = handlebars.compile(loadTemplate('graphql-subscriptions.js.hbs')); + fs.writeFileSync( + `${libPath}/subscriptions/subscriptions.js`, + template({ + specifications, + apiKey: getApiKey(), + }), + ); + fs.copyFileSync(templateUrl('graphql-subscriptions-index.js'), `${libPath}/subscriptions/index.js`); +} + const generateServerFunctionJSFiles = async ( libPath: string, specifications: ServerFunctionSpecification[], @@ -441,11 +462,11 @@ const generateSingleCustomFunction = async ( updated ? 'Regenerating TypeScript SDK...' : 'Generating TypeScript SDK...', ); - const libPath = getPolyLibPath(polyPath); - let contextData: Record = {}; + let libPath = getPolyLibPath(polyPath); + let prevSpecs: Specification[] = []; try { - contextData = getContextDataFileContent(libPath); + prevSpecs = getCachedSpecs(libPath); } catch (error) { shell.echo(chalk.red('ERROR')); shell.echo('Error while fetching local context data.'); @@ -454,8 +475,6 @@ const generateSingleCustomFunction = async ( return; } - const prevSpecs = getSpecsFromContextData(contextData); - let specs: Specification[] = []; try { @@ -479,11 +498,18 @@ const generateSingleCustomFunction = async ( specs = prevSpecs; } - await prepareDir(polyPath); + await prepareDir(polyPath, true); + libPath = getPolyLibPath(polyPath); + const tempPath = libPath.replace('/lib', '/temp'); + + writeCachedSpecs(tempPath, specs); setGenerationErrors(false); - await generateSpecs(libPath, specs, noTypes); + await generateSpecs(tempPath, specs, noTypes); + // Now remove old lib and rename temp directory to force a switchover in typescript + fs.rmSync(libPath, { recursive: true, force: true }); + fs.renameSync(tempPath, libPath); if (getGenerationErrors()) { shell.echo( @@ -532,13 +558,13 @@ const generate = async ({ polyPath, contexts, names, - functionIds, + ids, noTypes, }: { polyPath: string; contexts?: string[]; names?: string[]; - functionIds?: string[]; + ids?: string[]; noTypes: boolean; }) => { let specs: Specification[] = []; @@ -548,20 +574,26 @@ const generate = async ({ : 'Generating Poly TypeScript SDK...'; shell.echo('-n', generateMsg); - await prepareDir(polyPath); + await prepareDir(polyPath, true); loadConfig(polyPath); - try { - specs = await getSpecs(contexts, names, functionIds, noTypes); + const libPath = getPolyLibPath(polyPath); + const tempPath = libPath.replace('/lib', '/temp'); - updateLocalConfig(polyPath, contexts, names, functionIds, noTypes); + try { + specs = await getSpecs(contexts, names, ids, noTypes); + writeCachedSpecs(tempPath, specs); + updateLocalConfig(polyPath, contexts, names, ids, noTypes); } catch (error) { showErrGettingSpecs(error); return; } setGenerationErrors(false); - await generateSpecs(getPolyLibPath(polyPath), specs, noTypes); + await generateSpecs(tempPath, specs, noTypes); + // Now remove old lib and rename temp directory to force a switchover in typescript + fs.rmSync(libPath, { recursive: true, force: true }); + fs.renameSync(tempPath, libPath); if (getGenerationErrors()) { shell.echo( @@ -638,8 +670,6 @@ export const generateSpecs = async ( ); } - generateContextDataFile(libPath, filteredSpecs); - if (missingNames.length) { setGenerationErrors(true); missingNames.map((s) => echoGenerationError(s)); diff --git a/src/commands/generate/schemaTypes.ts b/src/commands/generate/schemaTypes.ts index 99ef2fc..2d08090 100644 --- a/src/commands/generate/schemaTypes.ts +++ b/src/commands/generate/schemaTypes.ts @@ -558,7 +558,7 @@ const printSchemaTreeAsTypes = ( depth + 1, ); } catch (err) { - console.error(err); + shell.echo(chalk.red(err)); echoGenerationError(child as SchemaSpec); setGenerationErrors(true); } diff --git a/src/commands/generate/table.ts b/src/commands/generate/table.ts index b03cb6e..4cd1a07 100644 --- a/src/commands/generate/table.ts +++ b/src/commands/generate/table.ts @@ -1,4 +1,6 @@ import fs from 'fs'; +import chalk from 'chalk'; +import shell from 'shelljs'; import { EOL } from 'node:os'; import { echoGenerationError, templateUrl } from '../../utils'; import { TableSpecification } from '../../types'; @@ -73,9 +75,9 @@ const printTableInterface = (table: TableSpecification | string): string => { `${_ws}upsertMany(query: ${formattedName}.InsertManyQuery): Promise<${formattedName}.QueryResults>;`, `${_ws}upsertOne(query: ${formattedName}.InsertOneQuery): Promise<${formattedName}.QueryResult>;`, `${_ws}updateMany(query: ${formattedName}.UpdateQuery): Promise<${formattedName}.QueryResults>;`, - // `${_ws}updateOne(query: ${formattedName}.UpdateQuery): Promise<${formattedName}.QueryResult>;`, + `${_ws}updateOne(id: string, query: ${formattedName}.UpdateQuery): Promise<${formattedName}.QueryResult>;`, `${_ws}deleteMany(query: ${formattedName}.DeleteQuery): Promise<${formattedName}.DeleteResults>;`, - // `${_ws}deleteOne(query: ${formattedName}.DeleteQuery): Promise<${formattedName}.DeleteResult>;`, + `${_ws}deleteOne(id: string, query?: ${formattedName}.DeleteQuery): Promise<${formattedName}.DeleteResult>;`, '}', ].join(`${EOL}${ws(2)}`); }; @@ -95,7 +97,7 @@ const printTableNamespace = (schema: JsonSchema, name: string, depth = 1): strin 'type QueryResults = PolyQueryResults;', 'type QueryResult = PolyQueryResult;', 'type DeleteResults = PolyDeleteResults;', - // 'type DeleteResult = PolyDeleteResult;', + 'type DeleteResult = PolyDeleteResult;', 'type CountResult = PolyCountResult;', ].join(`${EOL}${ws(depth + 1)}`) }${EOL}${ws(depth)}}`; @@ -119,7 +121,7 @@ const printTableTreeAsTypes = ( depth + 1, ); } catch (err) { - console.error(err); + shell.echo(chalk.red(err)); echoGenerationError(child as TableSpecification); setGenerationErrors(true); } diff --git a/src/commands/generate/types.ts b/src/commands/generate/types.ts index 42f81a6..f207866 100644 --- a/src/commands/generate/types.ts +++ b/src/commands/generate/types.ts @@ -420,6 +420,8 @@ const getIDComment = (specification: Specification) => { return `* Auth provider ID: ${specification.id}`; case 'webhookHandle': return `* Webhook ID: ${specification.id}`; + case 'graphqlSubscription': + return `* GraphQL Subscription ID: ${specification.id}`; default: return null; } diff --git a/src/commands/prepare.ts b/src/commands/prepare.ts index dc9f286..8d9d744 100644 --- a/src/commands/prepare.ts +++ b/src/commands/prepare.ts @@ -1,4 +1,5 @@ import shell from 'shelljs'; +import chalk from 'chalk'; import { FunctionArgumentDto } from '../types'; import { getTSBaseUrl, parseDeployable } from '../transpiler'; import { @@ -95,12 +96,12 @@ const getAllDeployables = async ( ); const fullName = `${deployable.context}.${deployable.name}`; if (found.has(fullName)) { - console.error( + shell.echo(chalk.redBright( `Prepared ${deployable.type.replaceAll( '-', ' ', )} ${fullName}: DUPLICATE`, - ); + )); } else { found.set( fullName, @@ -115,8 +116,8 @@ const getAllDeployables = async ( ); } } catch (err) { - console.error(`ERROR parsing ${possible}`); - console.error(err); + shell.echo(chalk.redBright(`ERROR parsing ${possible}`)); + shell.echo(chalk.red(err)); } } return Array.from(found.values()); diff --git a/src/commands/snippet.ts b/src/commands/snippet.ts index 7e3e6a9..816bb4d 100644 --- a/src/commands/snippet.ts +++ b/src/commands/snippet.ts @@ -4,7 +4,6 @@ import chalk from 'chalk'; import shell from 'shelljs'; import { upsertSnippet } from '../api'; -import { upsertResourceInSpec } from '../utils'; const readFile = promisify(fs.readFile); @@ -56,11 +55,6 @@ export const addSnippet = async ( ); shell.echo(`Snippet ID: ${response.data.id}`); - upsertResourceInSpec(polyPath, { - updated: response.status === 200, - resourceId: response.data.id, - resourceName: 'snippet', - }); } catch (error) { const httpStatusCode = error.response?.status; diff --git a/src/commands/sync.ts b/src/commands/sync.ts index d5fd025..d4f6a5f 100644 --- a/src/commands/sync.ts +++ b/src/commands/sync.ts @@ -74,41 +74,48 @@ const removeDeployable = async ( }; const syncDeployableAndGetId = async (deployable, code) => { - switch (deployable.type) { - case 'server-function': - return ( - await createOrUpdateServerFunction( - deployable.context, - deployable.name, - deployable.description, - code, - deployable.config.visibility, - deployable.typeSchemas, - deployable.dependencies, - deployable.config, - ) - ).id; - case 'client-function': - return ( - await createOrUpdateClientFunction( - deployable.context, - deployable.name, - deployable.description, - code, - deployable.config.visibility, - deployable.typeSchemas, - deployable.config, - ) - ).id; - case 'webhook': - return ( - await createOrUpdateWebhook( - deployable.context, - deployable.name, - deployable.description, - deployable.config, - ) - ).id; + if (deployable.type === 'webhook') { + return ( + await createOrUpdateWebhook( + deployable.context, + deployable.name, + deployable.description, + deployable.config, + ) + ).id; + } + let { externalDependencies, dependencies } = deployable; + if (Array.isArray(dependencies) && !externalDependencies) { + externalDependencies = Object.fromEntries(dependencies.map(d => [d, "latest"])); + } + if (deployable.type === 'server-function') { + return ( + await createOrUpdateServerFunction( + deployable.context, + deployable.name, + deployable.description, + code, + deployable.config.visibility, + deployable.typeSchemas, + externalDependencies, + deployable.internalDependencies, + deployable.config, + ) + ).id; + } else if (deployable.type === 'client-function') { + return ( + await createOrUpdateClientFunction( + deployable.context, + deployable.name, + deployable.description, + code, + deployable.config.visibility, + deployable.typeSchemas, + externalDependencies, + deployable.internalDependencies, + deployable.config, + ) + ).id; } throw new Error(`Unsupported deployable type: '${deployable.type}'`); }; diff --git a/src/deployables.ts b/src/deployables.ts index 31c4ee2..5b5fac2 100644 --- a/src/deployables.ts +++ b/src/deployables.ts @@ -78,7 +78,9 @@ export type DeployableRecord = ParsedDeployableConfig & { }; }; typeSchemas?: Record; - dependencies?: string[]; + dependencies?: string[]; // old version of external dependencies + externalDependencies?: Record, + internalDependencies?: Record>, description?: string; deployments: Deployment[]; deploymentCommentRanges?: Array<[startIndex: number, endIndex: number]>; diff --git a/src/transpiler.ts b/src/transpiler.ts index 20c7299..cdca109 100644 --- a/src/transpiler.ts +++ b/src/transpiler.ts @@ -13,6 +13,10 @@ import { getDeployableFileRevision, ParsedDeployableConfig, } from './deployables'; +import { getCachedSpecs, getPolyLibPath, writeCachedSpecs } from './utils'; +import { DEFAULT_POLY_PATH } from './constants'; +import { Specification } from './types'; +import { getSpecs } from './api'; // NodeJS built-in libraries + polyapi // https://www.w3schools.com/nodejs/ref_modules.asp @@ -78,12 +82,24 @@ const loadTsSourceFile = (filePath: string): ts.SourceFile => { return sourceFile; }; -export const getDependencies = ( +let fetchedSpecs = false; + +export const getDependencies = async ( code: string, fileName: string, baseUrl: string | undefined, ) => { const importedLibraries = new Set(); + const internalReferences = new Set(); + let polyImportIdentifier: string | null = null; + let variImportIdentifier: string | null = null; + let tabiImportIdentifier: string | null = null; + let schemasImportIdentifier: string | null = null; + const otherImportIdentifiers: string[] = []; + let lookForInternalDependencies = false; + // Users can alias references to parts of the poly tree by assigning to a variable + // So this map holds those references + const aliasMap = new Map(); const compilerOptions = { module: ts.ModuleKind.CommonJS, @@ -92,6 +108,49 @@ export const getDependencies = ( baseUrl, }; + + // Helper to extract dotted path + const getPropertyPath = (expr: ts.PropertyAccessExpression): string => { + const parts: string[] = []; + let current: ts.Expression = expr; + while (ts.isPropertyAccessExpression(current)) { + parts.unshift(current.name.text); + current = current.expression; + } + if (ts.isIdentifier(current)) { + parts.unshift(current.text); + } + return parts.join('.'); + } + + const unwrapExpression = (expr: ts.Expression): ts.Expression => { + while ( + ts.isParenthesizedExpression(expr) || + ts.isAsExpression(expr) || + ts.isTypeAssertionExpression(expr) + ) { + expr = expr.expression; + } + return expr; + } + + const flattenTypeName = (name: ts.EntityName): string => { + const parts: string[] = []; + const recurse = (n: ts.EntityName): void => { + if (ts.isQualifiedName(n)) { + recurse(n.left); + parts.push(n.right.text); + } else { + parts.push(n.text); + } + } + recurse(name); + return parts.join('.'); + } + + const VariMethods = /\.(?:(?:get)|(?:update)|(?:onUpdate)|(?:inject))$/; + const TabiMethods = /\.(?:(?:count)|(?:selectMany)|(?:selectOne)|(?:insertMany)|(?:insertOne)|(?:upsertMany)|(?:upsertOne)|(?:updateMany)|(?:updateOne)|(?:deleteMany)|(?:deleteOne))$/; + const compilerHost = ts.createCompilerHost(compilerOptions); ts.transpileModule(code, { compilerOptions, @@ -100,9 +159,41 @@ export const getDependencies = ( (context) => { return (sourceFile) => { const visitor = (node: ts.Node): ts.Node => { + // Pull out external dependencies if (ts.isImportDeclaration(node)) { - const moduleName = (node.moduleSpecifier as ts.StringLiteral) - .text; + const moduleName = (node.moduleSpecifier as ts.StringLiteral).text; + + // Capture poly imports + if (moduleName === "polyapi" && node.importClause) { + // Get name of polyapi default import if defined + if (node.importClause.name) { + polyImportIdentifier = `${node.importClause.name.text}.`; + lookForInternalDependencies = true; + } + + // Look for any other named imports (like vari, tabi, schemas, and other top-level namespaces (like OOB, etc)) + if (node.importClause.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) { + for (const element of node.importClause.namedBindings.elements) { + const imported = element.propertyName ? element.propertyName.text : element.name.text; + const local = element.name.text; + + if (imported === "vari") { + variImportIdentifier = `${local}.`; + lookForInternalDependencies = true; + } else if (imported === "tabi") { + tabiImportIdentifier = `${local}.`; + lookForInternalDependencies = true; + } else if (imported === "schemas") { + schemasImportIdentifier = `${local}.`; + lookForInternalDependencies = true; + } else if (local !== 'polyCustom') { + otherImportIdentifiers.push(`${local}.`); + lookForInternalDependencies = true; + } + } + } + } + const resolvedModule = ts.resolveModuleName( moduleName, fileName, @@ -120,8 +211,111 @@ export const getDependencies = ( importedLibraries.add(moduleName); } } + return node; + } + + + // Pull out internal dependencies + if (lookForInternalDependencies) { + // Track assignments of poly imports to follow aliases + if (ts.isVariableDeclaration(node) && node.initializer) { + const initializer = unwrapExpression(node.initializer); + // Simple variable assignments (aliases), ex. `const OOB = polyapi.OOB;` + if (ts.isIdentifier(node.name) && ts.isPropertyAccessExpression(initializer)) { + const path = getPropertyPath(initializer); + + if ( + // Capture poly reference aliases + (polyImportIdentifier && path.startsWith(polyImportIdentifier)) || + // Capture vari reference aliases + (variImportIdentifier && path.startsWith(variImportIdentifier)) || + // Capture tabi reference aliases + (tabiImportIdentifier && path.startsWith(tabiImportIdentifier)) || + // Capture other top-level namespace reference aliases + (otherImportIdentifiers.length && otherImportIdentifiers.some(other => path.startsWith(other))) + ) { + if (node.name && ts.isIdentifier(node.name)) { + aliasMap.set(node.name.text, path); + return node; // Don't recurse into assignment, just move on + } + } + } + // Destructuring assignments (aliases), ex. `const { OOB } = polyapi;` + else if (ts.isObjectBindingPattern(node.name)) { + let basePath: string | undefined; + if (ts.isPropertyAccessExpression(initializer)) { + basePath = getPropertyPath(initializer); + } else if (ts.isIdentifier(initializer)) { + basePath = initializer.text; + } + if (!basePath) return node; + + for (const element of node.name.elements) { + if (!ts.isBindingElement(element)) continue; + + // Handle `propertyName: alias` and shorthand `{ prop }` + const propName = element.propertyName + ? element.propertyName.getText() + : element.name.getText(); + const localName = element.name.getText(); + let path = `${basePath}.${propName}`; + const root = path.split('.')[0]; + // Check for alias to handle case where we're destructuring something from an aliased import + if (aliasMap.has(root)) { + const aliasBase = aliasMap.get(root); + path = aliasBase.split(".").concat(path.split('.').slice(1)).join('.'); + } + if ( + (polyImportIdentifier && path.startsWith(polyImportIdentifier)) || + (variImportIdentifier && path.startsWith(variImportIdentifier)) || + (tabiImportIdentifier && path.startsWith(tabiImportIdentifier)) || + (otherImportIdentifiers.length && otherImportIdentifiers.some(other => path.startsWith(other))) + ) { + aliasMap.set(localName, path); + } + } + return node; + } + } + // Look for use of imported poly dep! + else if (ts.isPropertyAccessExpression(node) && !ts.isPropertyAccessExpression(node.parent)) { + let path = getPropertyPath(node); + const root = path.split('.')[0]; + // If root is an alias, substitute + if (aliasMap.has(root)) { + const aliasBase = aliasMap.get(root)!; + path = aliasBase.split(".").concat(path.split('.').slice(1)).join('.'); + } + // Capture poly references (all function types, webhooks, and subscriptions) + if (polyImportIdentifier && path.startsWith(polyImportIdentifier)) { + internalReferences.add(path.replace(polyImportIdentifier, '')); + } + // Capture vari references + else if (variImportIdentifier && path.startsWith(variImportIdentifier)) { + internalReferences.add(path.replace(VariMethods, '').replace(variImportIdentifier, '')); + } + // Capture tabi references + else if (tabiImportIdentifier && path.startsWith(tabiImportIdentifier)) { + internalReferences.add(path.replace(TabiMethods, '').replace(tabiImportIdentifier, '')); + } + // Capture other top-level namespace references + else if (otherImportIdentifiers.length) { + const match = otherImportIdentifiers.find(other => path.startsWith(other)); + if (match) internalReferences.add(path); + } + } + } + + // Capture type references + if (schemasImportIdentifier && ts.isTypeReferenceNode(node)) { + const path = flattenTypeName(node.typeName); + if (path.startsWith(schemasImportIdentifier)) { + internalReferences.add(path.replace(schemasImportIdentifier, '')); + return node; + } } - return node; + + return ts.visitEachChild(node, visitor, context); }; return ts.visitEachChild(sourceFile, visitor, context); }; @@ -130,17 +324,21 @@ export const getDependencies = ( }, }); - let dependencies = Array.from(importedLibraries).filter( + const dependencies = Array.from(importedLibraries).filter( (library) => !EXCLUDED_REQUIREMENTS.includes(library), ); + const externalDependencies = {}; + const internalDependencies = {}; + // Finalize any external dependencies if (dependencies.length) { - let packageJson: any = fs.readFileSync( - path.join(process.cwd(), 'package.json'), - 'utf-8', - ); - + let packageJson: any = {}; try { + // Read dependency versions from package.json + packageJson = fs.readFileSync( + path.join(process.cwd(), 'package.json'), + 'utf-8', + ); packageJson = JSON.parse(packageJson); } catch (error) { shell.echo( @@ -153,10 +351,10 @@ export const getDependencies = ( const packageJsonDevDependencies = packageJson.devDependencies || {}; for (const dependency of dependencies) { - if ( - packageJsonDependencies[dependency] || - packageJsonDevDependencies[dependency] - ) { + let dependencyName = dependency; + let version = packageJsonDependencies[dependencyName] || packageJsonDevDependencies[dependencyName]; + if (version) { + externalDependencies[dependency] = version; continue; } @@ -165,26 +363,68 @@ export const getDependencies = ( while (dependencyParts.length > 0) { dependencyParts.pop(); - const newDependencyPath = dependencyParts.join('/'); - - if ( - packageJsonDependencies[newDependencyPath] || - packageJsonDevDependencies[newDependencyPath] - ) { - dependencies = dependencies.map((currentDependency) => { - if (currentDependency === dependency) { - return dependencyParts.join('/'); - } + dependencyName = dependencyParts.join('/'); + version = packageJsonDependencies[dependencyName] || packageJsonDevDependencies[dependencyName]; - return currentDependency; - }); + if (version) { + externalDependencies[dependencyName] = version; break; } } + if (!dependencyName) { + externalDependencies[dependency] = 'latest'; + } + } + } + + // Finalize any internal dependencies + if (internalReferences.size) { + // Find each reference in the specs + // Group internal references by type in a map from contextName to id + const libPath = getPolyLibPath(DEFAULT_POLY_PATH); + let specs = getCachedSpecs(libPath); + const found: Specification[] = []; + let missing: string[] = []; + + const findReferencedSpecs = (toFind: string[] | Set) => { + for (const path of internalReferences) { + const spec = specs.find(s => s.contextName.toLowerCase() === path.toLowerCase()); + if (spec) { + found.push(spec); + let type: string = spec.type; + const dep = { path: spec.contextName, id: spec.id }; + if ( + spec.visibilityMetadata.visibility === 'PUBLIC' && + ['apiFunction', 'customFunction', 'serverFunction'].includes(type) + ) { + type = `public${type.substring(0, 1).toUpperCase()}${type.substring(1)}`; + dep['tenantName'] = spec.visibilityMetadata.foreignTenantName; + dep['environmentName'] = spec.visibilityMetadata.foreignEnvironmentName; + } + internalDependencies[type] = internalDependencies[type] || []; + internalDependencies[type].push(dep); + } else { + missing.push(path); + } + } + } + findReferencedSpecs(internalReferences); + if (missing.length && !fetchedSpecs) { + // In case user generated the library with only a subset of the specs needed, grab them fresh from the api and try again + specs = await getSpecs(); + writeCachedSpecs(libPath, specs); + fetchedSpecs = true; + const toFind = missing; + missing = []; + findReferencedSpecs(toFind); + } + + if (missing.length) { + throw new Error(`Cannot resolve all poly resources referenced within function.\nMissing:\n${missing.map(n => `'${n}'`).join('\n')}`) } } - return dependencies; + return [dependencies.length ? externalDependencies : undefined, internalReferences.size ? internalDependencies : undefined]; }; export const parseDeployComment = (comment: string): Deployment => { @@ -411,13 +651,13 @@ const getFunctionDetails = ( return functionDetails; }; -const parseDeployableFunction = ( +const parseDeployableFunction = async ( sourceFile: ts.SourceFile, polyConfig: ParsedDeployableConfig, baseUrl: string, fileRevision: string, gitRevision: string, -): DeployableRecord => { +): Promise => { const [deployments, deploymentCommentRanges] = getDeployComments(sourceFile); const functionDetails = getFunctionDetails(sourceFile, polyConfig.name); if (polyConfig.description) { @@ -428,14 +668,15 @@ const parseDeployableFunction = ( } else { polyConfig.description = functionDetails.types.description || ''; } - const dependencies = getDependencies(sourceFile.getFullText(), sourceFile.fileName, baseUrl); + const [externalDependencies, internalDependencies] = await getDependencies(sourceFile.getFullText(), sourceFile.fileName, baseUrl); const typeSchemas = generateTypeSchemas(sourceFile.fileName, DeployableTypeEntries.map(d => d[0]), polyConfig.name); return { ...polyConfig, ...functionDetails, deployments, deploymentCommentRanges, - dependencies, + externalDependencies, + internalDependencies, typeSchemas, fileRevision, gitRevision, @@ -479,7 +720,7 @@ export const parseDeployable = async ( case 'server-function': case 'client-function': return [ - parseDeployableFunction( + await parseDeployableFunction( sourceFile, polyConfig, baseUrl, @@ -501,42 +742,41 @@ export const parseDeployable = async ( ]; } throw new Error('Invalid Poly deployment with unsupported type'); - } catch (err) { - console.error( - `Prepared ${polyConfig.type.replaceAll('-', ' ')} ${polyConfig.context}.${ - polyConfig.name + } catch (e) { + shell.echo(chalk.redBright( + `Prepared ${polyConfig.type.replaceAll('-', ' ')} ${polyConfig.context}.${polyConfig.name }: ERROR`, - ); - console.error(err); + )); + shell.echo(chalk.red((e instanceof Error ? e.message : e.response?.data?.message) || 'Unexpected error.')); } }; const dereferenceSchema = (obj: any, definitions: any, visited: Set = new Set()): any => { if (!obj || typeof obj !== 'object') return obj; - + if (Array.isArray(obj)) { return obj.map(item => dereferenceSchema(item, definitions, visited)); } - + const result: any = {}; - + for (const [key, value] of Object.entries(obj)) { if (key === '$ref' && typeof value === 'string') { const match = value.match(/^#\/definitions\/(.+)$/); if (match) { const defName = match[1]; - + // Prevent infinite recursion if (visited.has(defName)) { return { $ref: value }; } - + const definition = definitions[defName]; if (definition) { // Inspired by old flattenDefinitions logic: Check if this looks like a "real" type // vs a utility type by examining the definition structure const shouldInline = isInlineableDefinition(definition, defName); - + if (shouldInline) { // Inline the definition visited.add(defName); @@ -555,46 +795,46 @@ const dereferenceSchema = (obj: any, definitions: any, visited: Set = ne result[key] = dereferenceSchema(value, definitions, visited); } } - + return result; }; const isInlineableDefinition = (definition: any, defName: string): boolean => { const decodedDefName = decodeURIComponent(defName); - + // If it has "real" object properties, it's probably a real interface that should be inlined - if (definition.type === 'object' && definition.properties && - Object.keys(definition.properties).length > 0) { + if (definition.type === 'object' && definition.properties && + Object.keys(definition.properties).length > 0) { return true; } - + // If it has array items with concrete structure, inline it - if (definition.type === 'array' && definition.items && - typeof definition.items === 'object' && definition.items.type) { + if (definition.type === 'array' && definition.items && + typeof definition.items === 'object' && definition.items.type) { return true; } - + // If it's a simple type (string, number, boolean), inline it if (['string', 'number', 'boolean', 'integer'].includes(definition.type)) { return true; } - + // If it has enum values, it's a real type, inline it if (definition.enum && definition.enum.length > 0) { return true; } - + // If it's a union/intersection of concrete types, inline it if ((definition.anyOf || definition.allOf) && !decodedDefName.includes('<')) { return true; } - + // Keep as reference if it looks like a utility type (contains < > or other TypeScript operators) - if (decodedDefName.includes('<') || decodedDefName.includes('>') || - decodedDefName.includes('|') || decodedDefName.includes('&')) { + if (decodedDefName.includes('<') || decodedDefName.includes('>') || + decodedDefName.includes('|') || decodedDefName.includes('&')) { return false; } - + // If we can't determine, err on the side of inlining for "normal" looking names return !/[<>%|&]/.test(decodedDefName); }; @@ -624,7 +864,7 @@ const dereferenceRoot = (schema: any): any => { // Now recursively dereference based on intelligent heuristics const { definitions, $schema, ...rest } = rootSchema; const dereferencedRest = dereferenceSchema(rest, definitions); - + // Collect remaining references that weren't inlined const filteredDefinitions: any = {}; const findReferences = (obj: any, visited: Set = new Set()): void => { @@ -639,11 +879,11 @@ const dereferenceRoot = (schema: any): any => { if (match) { const encodedDefName = match[1]; const decodedDefName = decodeURIComponent(encodedDefName); - + // Try both encoded and decoded versions - const actualDefName = definitions[encodedDefName] ? encodedDefName : - definitions[decodedDefName] ? decodedDefName : null; - + const actualDefName = definitions[encodedDefName] ? encodedDefName : + definitions[decodedDefName] ? decodedDefName : null; + if (actualDefName && !visited.has(actualDefName)) { visited.add(actualDefName); filteredDefinitions[actualDefName] = definitions[actualDefName]; @@ -656,9 +896,9 @@ const dereferenceRoot = (schema: any): any => { } } }; - + findReferences(dereferencedRest); - + return { ...dereferencedRest, ...(Object.keys(filteredDefinitions).length > 0 && { definitions: filteredDefinitions }), @@ -676,17 +916,17 @@ export const extractTypesFromAST = (filePath: string, functionName: string): str if (!sourceFile) throw new Error(`Could not load file: ${filePath}`); const extractedTypes: Set = new Set(); - + const extractFromTypeNode = (typeNode: ts.TypeNode) => { if (ts.isTypeReferenceNode(typeNode)) { const typeName = typeNode.typeName.getText(sourceFile); - + // Skip primitive and built-in types const primitives = ['Promise', 'Array', 'Record', 'string', 'number', 'boolean', 'void', 'any', 'unknown', 'object', 'undefined', 'null']; if (!primitives.includes(typeName)) { extractedTypes.add(typeName); } - + // Extract type arguments from generics (like Promise -> extract T) if (typeNode.typeArguments) { for (const typeArg of typeNode.typeArguments) { @@ -709,7 +949,7 @@ export const extractTypesFromAST = (filePath: string, functionName: string): str extractFromTypeNode(param.type); } } - + // Extract types from return type if (node.type) { extractFromTypeNode(node.type); @@ -730,12 +970,12 @@ export const generateTypeSchemas = ( const tsconfigPath = path.resolve('tsconfig.json'); // Use provided function name or default to filename const actualFunctionName = functionName || path.basename(filePath, '.ts'); - + // FIXED: Use AST-based type extraction instead of string manipulation const extractedTypes = extractTypesFromAST(filePath, actualFunctionName); - + // Filter ignored types - const typeNames = extractedTypes.filter(typeName => + const typeNames = extractedTypes.filter(typeName => !ignoredTypeNames.includes(typeName) ); diff --git a/src/types/specifications.ts b/src/types/specifications.ts index 5b3acff..a903717 100644 --- a/src/types/specifications.ts +++ b/src/types/specifications.ts @@ -14,6 +14,7 @@ export type SpecificationType = | 'serverVariable' | 'snippet' | 'table' + | 'graphqlSubscription' | 'schema'; export interface ISpecification { @@ -126,6 +127,12 @@ export interface WebhookHandleSpecification extends ISpecification { function: FunctionSpecification; } +export interface GraphQLSubscriptionSpecification extends ISpecification { + type: 'graphqlSubscription'; + description: string; + function: FunctionSpecification; +} + export interface ServerVariableSpecification extends ISpecification { type: 'serverVariable'; variable: VariableSpecification; @@ -170,6 +177,7 @@ export type Specification = | ServerVariableSpecification | SnippetSpecification | SchemaSpecification + | GraphQLSubscriptionSpecification | TableSpecification; interface CreateWebhookHandleDtoForSpecificationInput diff --git a/src/utils.ts b/src/utils.ts index fb85dba..1eb8f34 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -13,7 +13,6 @@ import { SpecificationType, } from './types'; -import { getSpecs } from './api'; import { INSTANCE_URL_MAP } from './constants'; export const getPolyLibPath = (polyPath: string) => @@ -21,48 +20,57 @@ export const getPolyLibPath = (polyPath: string) => ? `${polyPath}/lib` : `${__dirname}/../../../${polyPath}/lib`; -export const getContextDataFileContent = (libPath: string) => { +export const getCachedSpecs = (libPath: string) => { try { const contents = fs.readFileSync(`${libPath}/specs.json`, 'utf-8'); - return JSON.parse(contents) as Record; + return JSON.parse(contents) as Specification[]; } catch (err) { - return {}; + return []; } }; -export const getSpecsFromContextData = (contextData) => { - const specs: Specification[] = []; - - const traverseAndGetSpec = (data) => { - for (const key of Object.keys(data)) { - if (typeof data[key].context === 'string') { - specs.push(data[key]); - } else { - traverseAndGetSpec(data[key]); - } - } - }; - - traverseAndGetSpec(contextData); +export const writeCachedSpecs = ( + libPath: string, + specs: Specification[], +) => { + fs.mkdirSync(libPath, { recursive: true }); + fs.writeFileSync( + `${libPath}/specs.json`, + JSON.stringify( + specs.filter((spec) => { + if (spec.type === 'snippet') { + return spec.language === 'javascript'; + } + if (spec.type === 'customFunction') { + return spec.language === 'javascript'; + } - return specs; + return true; + }), + null, + 2, + ), + ); }; + export type GenerationError = { specification: Specification; stack: string; }; export const echoGenerationError = (specification: Specification) => { - const typeMap = { + const typeMap: Record = { apiFunction: 'API Function', customFunction: 'Custom Function', authFunction: 'Auth Function', webhookHandle: 'Webhook Handle', + graphqlSubscription: 'Webhook Handle', serverFunction: 'Server Function', serverVariable: 'Variable', schema: 'Schema', snippet: 'Snippet', + table: 'Table' }; const type = typeMap[specification.type]; @@ -98,90 +106,6 @@ export const showErrGettingSpecs = (error: any) => { shell.exit(1); }; -export const generateContextDataFile = ( - libPath: string, - specs: Specification[], -) => { - fs.writeFileSync( - `${libPath}/specs.json`, - JSON.stringify( - specs.filter((spec) => { - if (spec.type === 'snippet') { - return spec.language === 'javascript'; - } - if (spec.type === 'customFunction') { - return spec.language === 'javascript'; - } - - return true; - }), - null, - 2, - ), - ); -}; - -export const upsertResourceInSpec = async ( - polyPath: string, - { - resourceId, - resourceName, - updated, - }: { - resourceId: string; - resourceName: string; - updated: boolean; - }, -) => { - shell.echo( - '-n', - updated - ? `Updating ${resourceName} in specs...` - : `Adding ${resourceName} to SDK...`, - ); - - let contextData: Record = {}; - - try { - contextData = getContextDataFileContent(getPolyLibPath(polyPath)); - } catch (error) { - shell.echo(chalk.red('ERROR')); - shell.echo('Error while fetching local context data.'); - shell.echo(chalk.red(error.message)); - shell.echo(chalk.red(error.stack)); - return; - } - - const prevSpecs = getSpecsFromContextData(contextData); - - let specs: Specification[] = []; - - try { - specs = await getSpecs([], [], [resourceId], false); - } catch (error) { - showErrGettingSpecs(error); - return; - } - - const [resource] = specs; - - if (prevSpecs.some((prevSpec) => prevSpec.id === resource.id)) { - specs = prevSpecs.map((prevSpec) => { - if (prevSpec.id === resource.id) { - return resource; - } - return prevSpec; - }); - } else { - prevSpecs.push(resource); - specs = prevSpecs; - } - - generateContextDataFile(getPolyLibPath(polyPath), specs); - - shell.echo(chalk.green('DONE')); -}; - export const getStringPaths = (data: Record | any[]) => { const paths = jp.paths(data, '$..*', 100); diff --git a/templates/graphql-subscriptions-index.js b/templates/graphql-subscriptions-index.js new file mode 100644 index 0000000..deb3a63 --- /dev/null +++ b/templates/graphql-subscriptions-index.js @@ -0,0 +1,35 @@ +const set = require('lodash/set'); +const { subscriptions } = require('./subscriptions'); + +const registerGraphQLSubscriptionEventListener = (clientID, getSocket, getApiKey, subscriptionId, callback) => { + const socket = getSocket(); + socket.emit('registerSubscriptionHandler', { + clientID, + subscriptionId, + apiKey: getApiKey(), + }, registered => { + if (registered) { + socket.on( + `handleSubscriptionEvent:${subscriptionId}`, + async ({ event, params, executionId }) => { + await callback(event, params, { executionId }); + } + ); + } else { + console.log(`Could not register GraphQL subscription event handler for ${subscriptionId}`); + } + }); + + return () => { + socket.emit('unregisterSubscriptionHandler', { + clientID, + subscriptionId, + apiKey: getApiKey(), + }); + } +}; + +module.exports = (clientID, getSocket, getApiKey) => subscriptions.reduce( + (acc, [path, id]) => set(acc, path, (callback) => registerGraphQLSubscriptionEventListener(clientID, getSocket, getApiKey, id, callback)), + {} +); diff --git a/templates/graphql-subscriptions.js.hbs b/templates/graphql-subscriptions.js.hbs new file mode 100644 index 0000000..7353133 --- /dev/null +++ b/templates/graphql-subscriptions.js.hbs @@ -0,0 +1,11 @@ +module.exports = { + subscriptions: [ + {{#each specifications}} + {{#if context}} + ['{{context}}.{{name}}', '{{id}}'], + {{else}} + ['{{name}}', '{{id}}'], + {{/if}} + {{/each}} + ] +} diff --git a/templates/index.d.ts.hbs b/templates/index.d.ts.hbs index 4cd088b..9581d00 100644 --- a/templates/index.d.ts.hbs +++ b/templates/index.d.ts.hbs @@ -15,6 +15,7 @@ declare const poly: Poly; export default poly; export type UnregisterWebhookEventListener = () => void; +export type UnregisterGraphQLSubscriptionEventListener = () => void; export type AuthFunctionCallback = (token?: string, url?: string, error?: any) => any; export interface AuthFunctionResponse { diff --git a/templates/index.js b/templates/index.js index 2830db8..40def6b 100644 --- a/templates/index.js +++ b/templates/index.js @@ -3,6 +3,7 @@ const { io } = require('socket.io-client'); const apiFunctions = require('./api'); const clientFunctions = require('./client'); const webhooks = require('./webhooks'); +const subscriptions = require('./subscriptions'); const authFunctions = require('./auth'); const serverFunctions = require('./server'); const vari = require('./vari'); @@ -68,6 +69,7 @@ merge( serverFunctions(CLIENT_ID, polyCustom), authFunctions(CLIENT_ID, getSocket, getApiKey), webhooks(CLIENT_ID, getSocket, getApiKey), + subscriptions(CLIENT_ID, getSocket, getApiKey), ), module.exports = { ...poly, diff --git a/templates/tabi/index.js b/templates/tabi/index.js index 1936375..9369e35 100644 --- a/templates/tabi/index.js +++ b/templates/tabi/index.js @@ -24,13 +24,13 @@ const firstResult = (rsp) => { return rsp; } -// const deleteOneResponse = (rsp) => { -// if (typeof rsp.deleted === 'number') { -// return { deleted: rsp.deleted > 0 }; -// } -// // Else rsp is some kind of error -// return rsp; -// } +const deleteOneResponse = (rsp) => { + if (typeof rsp.deleted === 'number') { + return { deleted: rsp.deleted > 0 }; + } + // Else rsp is some kind of error + return rsp; +} class Table { constructor(id, clientID, polyCustom) { @@ -78,17 +78,21 @@ class Table { return executeQuery(this.id, 'update', query, this.clientID, this.polyCustom); } - // updateOne(query) { - // return executeQuery(this.id, 'update', query, this.clientID, this.polyCustom).then(firstResult); - // } + updateOne(id, query) { + if (!query.where) query.where = {}; + query.where.id = id; + return executeQuery(this.id, 'update', query, this.clientID, this.polyCustom).then(firstResult); + } deleteMany(query) { return executeQuery(this.id, 'delete', query, this.clientID, this.polyCustom); } - // deleteOne(query) { - // return executeQuery(this.id, 'delete', query, this.clientID, this.polyCustom).then(deleteOneResponse); - // } + deleteOne(id, query = {}) { + if (!query.where) query.where = {}; + query.where.id = id; + return executeQuery(this.id, 'delete', query, this.clientID, this.polyCustom).then(deleteOneResponse); + } } module.exports = (clientID, polyCustom) => tables.reduce( diff --git a/templates/tabi/types.d.ts b/templates/tabi/types.d.ts index 675b10e..552ec07 100644 --- a/templates/tabi/types.d.ts +++ b/templates/tabi/types.d.ts @@ -81,10 +81,10 @@ type PolyDeleteResults = { deleted: number; } -// type PolyDeleteResult = { -// deleted: boolean; -// } +type PolyDeleteResult = { + deleted: boolean; +} type PolyCountResult = { - count: boolean; + count: number; } \ No newline at end of file