diff --git a/README_INTERNAL.md b/README_INTERNAL.md index ecde7f2..0698d30 100644 --- a/README_INTERNAL.md +++ b/README_INTERNAL.md @@ -41,7 +41,7 @@ Add the line "HELLO WORLD" to the end of `README.md` Then run this command to publish to your local Verdaccio instance: ```bash -./publish_local.sh +./scripts/publish_local.sh ``` ## Install Your Updated NPM Package Locally diff --git a/package-lock.json b/package-lock.json index f5074c8..8bcf6c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "polyapi", - "version": "0.24.18", + "version": "0.24.19", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "polyapi", - "version": "0.24.18", + "version": "0.24.19", "license": "MIT", "dependencies": { "@guanghechen/helper-string": "4.7.1", diff --git a/package.json b/package.json index 51239d3..4312158 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "polyapi", - "version": "0.24.18", + "version": "0.24.19", "description": "Poly is a CLI tool to help create and manage your Poly definitions.", "license": "MIT", "repository": { diff --git a/src/api.ts b/src/api.ts index 4f96531..a495af2 100644 --- a/src/api.ts +++ b/src/api.ts @@ -123,21 +123,39 @@ export const createOrUpdateServerFunction = async ( ).data; }; +export const getServerFunctionById = async (id: string) => { + return ( + await axios.get>( + `${getApiBaseURL()}/functions/server/${id}`, + { + headers: { + 'Content-Type': 'application/json', + ...getApiHeaders(), + }, + }, + ) + ).data; +}; + export const getServerFunctionByName = async ( context: string, name: string, + detail = false ) => { - return ( - await axios.get>( - `${getApiBaseURL()}/functions/server`, + const basic = ( + await axios.get>( + `${getApiBaseURL()}/functions/server?search=${encodeURIComponent(`${context}${context && name ? '.' : ''}${name}`)}`, { headers: { 'Content-Type': 'application/json', ...getApiHeaders(), + 'x-poly-api-version': '2', }, }, ) - ).data.find((fn) => fn.name === name && fn.context === context); + ).data.results.find((fn) => fn.name === name && fn.context === context); + if (!detail || !basic) return basic; + return getServerFunctionById(basic.id); }; export const deleteServerFunction = async (id: string) => { @@ -178,21 +196,39 @@ export const createOrUpdateClientFunction = async ( ).data; }; +export const getClientFunctionById = async (id: string) => { + return ( + await axios.get>( + `${getApiBaseURL()}/functions/client/${id}`, + { + headers: { + 'Content-Type': 'application/json', + ...getApiHeaders(), + }, + }, + ) + ).data; +}; + export const getClientFunctionByName = async ( context: string, name: string, + detail = false, ) => { - return ( - await axios.get>( - `${getApiBaseURL()}/functions/client`, + const basic = ( + await axios.get>( + `${getApiBaseURL()}/functions/client?search=${encodeURIComponent(`${context}${context && name ? '.' : ''}${name}`)}`, { headers: { 'Content-Type': 'application/json', ...getApiHeaders(), + 'x-poly-api-version': '2', }, }, ) - ).data.find((fn) => fn.name === name && fn.context === context); + ).data.results.find((fn) => fn.name === name && fn.context === context); + if (!detail || !basic) return basic; + return getClientFunctionById(basic.id); }; export const deleteClientFunction = async (id: string) => { @@ -482,8 +518,22 @@ export const createOrUpdateWebhook = async ( ).data; }; -export const getWebhookByName = async (context: string, name: string) => { +export const getWebhookById = async (id: string) => { return ( + await axios.get>( + `${getApiBaseURL()}/webhooks/${id}`, + { + headers: { + 'Content-Type': 'application/json', + ...getApiHeaders(), + }, + }, + ) + ).data; +}; + +export const getWebhookByName = async (context: string, name: string, detail = false) => { + const basic = ( await axios.get>( `${getApiBaseURL()}/webhooks`, { @@ -496,6 +546,8 @@ export const getWebhookByName = async (context: string, name: string) => { ).data.find( (webhook) => webhook.name === name && webhook.context === context, ); + if (!detail || !basic) return basic; + return getWebhookById(basic.id); }; export const deleteWebhook = async (webhookId: string) => { diff --git a/src/commands/sync.ts b/src/commands/sync.ts index bf1a927..df3494f 100644 --- a/src/commands/sync.ts +++ b/src/commands/sync.ts @@ -11,6 +11,8 @@ import { getCacheDeploymentsRevision, removeDeployableRecords, prepareDeployableDirectory, + getDeployableFileRevision, + getRandomString, } from '../deployables'; import { createOrUpdateClientFunction, @@ -19,14 +21,18 @@ import { deleteClientFunction, deleteServerFunction, deleteWebhook, + getClientFunctionById, getClientFunctionByName, + getServerFunctionById, getServerFunctionByName, + getWebhookById, getWebhookByName, } from '../api'; +import { FunctionDetailsDto, WebhookHandleDto } from '../types'; const DEPLOY_ORDER: DeployableTypes[] = [ - 'server-function', 'client-function', + 'server-function', 'webhook', ]; @@ -105,6 +111,32 @@ const syncDeployableAndGetId = async (deployable, code) => { throw new Error(`Unsupported deployable type: '${deployable.type}'`); }; +const getDeployableFromServer = async ( + deployable: SyncDeployment, +): Promise => { + try { + switch(deployable.type) { + case 'server-function': { + return deployable.id + ? getServerFunctionById(deployable.id) as T + : getServerFunctionByName(deployable.context, deployable.name, true) as T; + } + case 'client-function': { + return deployable.id + ? getClientFunctionById(deployable.id) as T + : getClientFunctionByName(deployable.context, deployable.name, true) as T; + } + case 'webhook': { + return deployable.id + ? getWebhookById(deployable.id) as T + : getWebhookByName(deployable.context, deployable.name, true) as T; + } + } + } catch (err) { + return null; + } +} + const syncDeployable = async ( deployable: SyncDeployment, ): Promise => { @@ -149,33 +181,51 @@ export const syncDeployables = async ( const previousDeployment = deployable.deployments.find( (i) => i.instance === instance, ); + // Any deployable may be deployed to multiple instances/environments at the same time + // So we reduce the deployable record down to a single instance we want to deploy to + const syncDeployment: SyncDeployment = { + ...deployable, + ...previousDeployment, // flatten to grab name & context + type: deployable.type, // but make sure we use the latest type + description: deployable.description ?? deployable.types?.description, + instance, + }; + const deployed = await getDeployableFromServer(syncDeployment); const gitRevisionChanged = gitRevision !== deployable.gitRevision; - const fileRevisionChanged = - previousDeployment?.fileRevision !== deployable.fileRevision; + const serverFileRevision = !deployed + ? '' + : type === 'webhook' + // TODO: Actually calculate real revision on webhook + ? getRandomString(8) + : ((deployed as FunctionDetailsDto).hash || getDeployableFileRevision((deployed as FunctionDetailsDto).code)); + const fileRevisionChanged = serverFileRevision !== deployable.fileRevision; + // TODO: If deployed variabnt exists AND was deployed after timestamp on previousDeployment then sync it back to the repo let action = gitRevisionChanged ? 'REMOVED' - : !previousDeployment?.id + : !previousDeployment?.id && !deployed ? 'ADDED' : fileRevisionChanged ? 'UPDATED' - : 'OK'; + : 'SKIPPED'; - if (!dryRun && (gitRevisionChanged || fileRevisionChanged)) { + if (!dryRun && action !== 'SKIPPED') { // if user is changing type, ex. server -> client function or vice versa // then try to cleanup the old type first if (previousDeployment && deployable.type !== previousDeployment.type) { await removeDeployable(previousDeployment); } - // Any deployable may be deployed to multiple instances/environments at the same time - // So we reduce the deployable record down to a single instance we want to deploy to - const syncDeployment: SyncDeployment = { - ...deployable, - ...previousDeployment, // flatten to grab name & context - type: deployable.type, // but make sure we use the latest type - description: deployable.description ?? deployable.types?.description, - instance, - }; - if (gitRevision === deployable.gitRevision) { + if (gitRevisionChanged) { + // This deployable no longer exists so let's remove it + const found = await removeDeployable(syncDeployment); + if (!found) action = 'NOT FOUND'; + const removeIndex = allDeployables.findIndex( + (d) => + d.name === deployable.name && + d.context === deployable.context && + d.file === deployable.file, + ); + toRemove.push(...allDeployables.splice(removeIndex, 1)); + } else { const deployment = await syncDeployable(syncDeployment); if (previousDeployment) { previousDeployment.id = deployment.id; @@ -187,17 +237,6 @@ export const syncDeployables = async ( } else { deployable.deployments.unshift(deployment); } - } else { - // This deployable no longer exists so let's remove it - const found = await removeDeployable(syncDeployment); - if (!found) action = 'NOT FOUND'; - const removeIndex = allDeployables.findIndex( - (d) => - d.name === deployable.name && - d.context === deployable.context && - d.file === deployable.file, - ); - toRemove.push(...allDeployables.splice(removeIndex, 1)); } } diff --git a/src/deployables.ts b/src/deployables.ts index 1de572a..31c4ee2 100644 --- a/src/deployables.ts +++ b/src/deployables.ts @@ -150,7 +150,6 @@ const writeJsonFile = async ( path: string, contents: T, ): Promise => { - await open(path, 'w'); return writeFile(path, JSON.stringify(contents, undefined, 2), { encoding: 'utf8', flag: 'w', @@ -268,8 +267,14 @@ export const getDeployableFileRevision = (fileContents: string): string => fileContents.replace(/^(\/\/.*\n)+/, ''), ) .digest('hex') - // Trimming to 7 characters to align with git revision format and to keep this nice and short! - .substring(0, 7); + // Trimming to 8 characters to align with git revision format and to keep this nice and short! + .substring(0, 8); + +export const getRandomString = (length = 8) => { + return Array.from({ length }, () => + Math.floor(Math.random() * 16).toString(16), + ).join(''); +} export const getGitRevision = (branchOrTag = 'HEAD'): string => { try { @@ -281,9 +286,7 @@ export const getGitRevision = (branchOrTag = 'HEAD'): string => { return result; } catch (err) { console.warn('Failed to get git revision. Falling back to random hash.'); - return Array.from({ length: 8 }, () => - Math.floor(Math.random() * 16).toString(16), - ).join(''); + return getRandomString(8); } }; diff --git a/src/types/functions.ts b/src/types/functions.ts index 6831cae..e3e198d 100644 --- a/src/types/functions.ts +++ b/src/types/functions.ts @@ -59,6 +59,15 @@ export interface FunctionDetailsDto extends FunctionBasicDto { * If there are some missing poly schemas in `returnTypeSchema`, they will be listed here. */ unresolvedReturnTypePolySchemaRefs?: SchemaRef[]; + code: string; + language: string; + logsEnabled?: boolean; + serverSideAsync?: boolean; + minScale?: number | null; + maxScale?: number | null; + requirements?: string | null; + generateContexts?: string[] | null; + hash: string; } export interface EntrySource { diff --git a/src/types/webhooks.ts b/src/types/webhooks.ts index 80a9a61..b4f627a 100644 --- a/src/types/webhooks.ts +++ b/src/types/webhooks.ts @@ -71,43 +71,34 @@ export interface ExecuteWebhookHandleDescriptionGenerationDto { eventPayload: any; } -export interface WebhookHandleDto { +export interface WebhookHandleBasicDto { id: string; - context: string; name: string; + context: string; contextName: string; description: string; - url: string; - uri: string; visibility: Visibility; + subpath: string | null; + state: LifecycleState; + enabled: boolean; ownerUserId?: string | null; +} + +export interface WebhookHandleDto extends WebhookHandleBasicDto { + url: string; + uri: string; eventPayloadType: string; eventPayloadTypeSchema?: Record; responsePayload?: any; responseHeaders?: any; responseStatus: number | null; slug: string | null; - subpath: string | null; - method: string | null; + method: string; requirePolyApiKey: boolean; securityFunctions: WebhookSecurityFunction[]; - enabled: boolean; xmlParserOptions: WebhookHandleXmlParserOptions; } -export interface WebhookHandleBasicDto { - id: string; - name: string; - context: string; - contextName: string; - description: string; - visibility: Visibility; - subpath: string | null; - state: LifecycleState; - enabled: boolean; - ownerUserId?: string | null; -} - export interface WebhookHandleDescriptionGenerationDto { name: string; context: string;