1751 extensibility twenty sdk v2 use twenty sdk to define a serverless function trigger (#15347)

This PR adds 2 columns handlerPath and handlerName in serverlessFunction
to locate the entrypoint of a serverless in a codebase

It adds the following decorators in twenty-sdk:
- ServerlessFunction
- DatabaseEventTrigger
- RouteTrigger
- CronTrigger
- ApplicationVariable

It still supports deprecated entity.manifest.jsonc 

Overall code needs to be cleaned a little bit, but it should work
properly so you can try to test if the DEVX fits your needs

See updates in hello-world application

```typescript
import axios from 'axios';
import {
  DatabaseEventTrigger,
  ServerlessFunction,
  RouteTrigger,
  CronTrigger,
  ApplicationVariable,
} from 'twenty-sdk';

@ApplicationVariable({
  universalIdentifier: 'dedc53eb-9c12-4fe2-ba86-4a2add19d305',
  key: 'TWENTY_API_KEY',
  description: 'Twenty API Key',
  isSecret: true,
})
@DatabaseEventTrigger({
  universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
  eventName: 'person.created',
})
@RouteTrigger({
  universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
  path: '/post-card/create',
  httpMethod: 'GET',
  isAuthRequired: false,
})
@CronTrigger({
  universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
  pattern: '0 0 1 1 *', // Every year 1st of January
})
@ServerlessFunction({
  universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
})
class CreateNewPostCard {
  main = async (params: { recipient: string }): Promise<string> => {
    const { recipient } = params;

    const options = {
      method: 'POST',
      url: 'http://localhost:3000/rest/postCards',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${process.env.TWENTY_API_KEY}`,
      },
      data: { name: recipient ?? 'Unknown' },
    };

    try {
      const { data } = await axios.request(options);

      console.log(`New post card to "${recipient}" created`);

      return data;
    } catch (error) {
      console.error(error);
      throw error;
    }
  };
}

export const createNewPostCardHandler = new CreateNewPostCard().main;

```


### [edit] V2 

After the v1 proposal, I see that using a class method to define the
serverless function handler is pretty confusing. Lets leave
serverlessFunction configuration decorators on the class, but move the
handler like before. Here is the v2 hello-world serverless function:

```typescript
import axios from 'axios';
import {
  DatabaseEventTrigger,
  ServerlessFunction,
  RouteTrigger,
  CronTrigger,
  ApplicationVariable,
} from 'twenty-sdk';

@ApplicationVariable({
  universalIdentifier: 'dedc53eb-9c12-4fe2-ba86-4a2add19d305',
  key: 'TWENTY_API_KEY',
  description: 'Twenty API Key',
  isSecret: true,
})
@DatabaseEventTrigger({
  universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
  eventName: 'person.created',
})
@RouteTrigger({
  universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
  path: '/post-card/create',
  httpMethod: 'GET',
  isAuthRequired: false,
})
@CronTrigger({
  universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
  pattern: '0 0 1 1 *', // Every year 1st of January
})
@ServerlessFunction({
  universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
})
export class ServerlessFunctionDefinition {}

export const main = async (params: { recipient: string }): Promise<string> => {
  const { recipient } = params;

  const options = {
    method: 'POST',
    url: 'http://localhost:3000/rest/postCards',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${process.env.TWENTY_API_KEY}`,
    },
    data: { name: recipient ?? 'Unknown' },
  };

  try {
    const { data } = await axios.request(options);

    console.log(`New post card to "${recipient}" created`);

    return data;
  } catch (error) {
    console.error(error);
    throw error;
  }
};

```


### [edit] V3

After the v2 proposal, we don't really like decorators on empty classes.
We decided to go with a Vercel approach with a config constant

```typescript
import axios from 'axios';
import { ServerlessFunctionConfig } from 'twenty-sdk';

export const main = async (params: { recipient: string }): Promise<string> => {
  const { recipient } = params;

  const options = {
    method: 'POST',
    url: 'http://localhost:3000/rest/postCards',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${process.env.TWENTY_API_KEY}`,
    },
    data: { name: recipient ?? 'Unknown' },
  };

  try {
    const { data } = await axios.request(options);

    console.log(`New post card to "${recipient}" created`);

    return data;
  } catch (error) {
    console.error(error);
    throw error;
  }
};

export const config: ServerlessFunctionConfig = {
  universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
  routeTriggers: [
  {
    universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
    path: '/post-card/create',
    httpMethod: 'GET',
    isAuthRequired: false,
  }
  ],
  cronTriggers: [
    {
      universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
      pattern: '0 0 1 1 *', // Every year 1st of January
    }
  ],
  databaseEventTriggers: [
  {
    universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
    eventName: 'person.created',
   }
  ]
}

```
This commit is contained in:
martmull
2025-10-29 17:51:43 +01:00
committed by GitHub
parent 75ed5cb3a2
commit a6cc80eedd
81 changed files with 2070 additions and 1044 deletions
@@ -1,5 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { parse } from 'path';
import { isDefined } from 'twenty-shared/utils';
import { ALL_METADATA_NAME, AllMetadataName } from 'twenty-shared/metadata';
@@ -11,13 +13,11 @@ import {
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { ApplicationInput } from 'src/engine/core-modules/application/dtos/application.input';
import {
AgentManifest,
ObjectManifest,
ServerlessFunctionManifest,
ServerlessFunctionTriggerManifest,
} from 'src/engine/core-modules/application/types/application.types';
import { ApplicationVariableEntityService } from 'src/engine/core-modules/applicationVariable/application-variable.service';
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
import { CronTriggerV2Service } from 'src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service';
import { FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
@@ -34,6 +34,7 @@ import { ServerlessFunctionLayerService } from 'src/engine/metadata-modules/serv
import { ServerlessFunctionV2Service } from 'src/engine/metadata-modules/serverless-function/services/serverless-function-v2.service';
import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
import { Sources } from 'src/engine/core-modules/file-storage/types/source.type';
@Injectable()
export class ApplicationSyncService {
@@ -47,7 +48,6 @@ export class ApplicationSyncService {
private readonly serverlessFunctionV2Service: ServerlessFunctionV2Service,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly dataSourceService: DataSourceService,
private readonly agentService: AgentService,
private readonly databaseEventTriggerV2Service: DatabaseEventTriggerV2Service,
private readonly cronTriggerV2Service: CronTriggerV2Service,
private readonly routeTriggerV2Service: RouteTriggerV2Service,
@@ -69,12 +69,6 @@ export class ApplicationSyncService {
yarnLock,
});
await this.syncAgents({
agentsToSync: manifest.agents,
workspaceId,
applicationId: application.id,
});
await this.syncObjects({
objectsToSync: manifest.objects,
workspaceId,
@@ -83,6 +77,7 @@ export class ApplicationSyncService {
await this.syncServerlessFunctions({
serverlessFunctionsToSync: manifest.serverlessFunctions,
code: manifest.sources,
workspaceId,
applicationId: application.id,
serverlessFunctionLayerId: application.serverlessFunctionLayerId,
@@ -100,10 +95,12 @@ export class ApplicationSyncService {
workspaceId: string;
}): Promise<ApplicationEntity> {
const application = await this.applicationService.findByUniversalIdentifier(
manifest.universalIdentifier,
manifest.application.universalIdentifier,
workspaceId,
);
const name = manifest.application.displayName ?? packageJson.name;
if (!isDefined(application)) {
const serverlessFunctionLayer =
await this.serverlessFunctionLayerService.create(
@@ -115,18 +112,18 @@ export class ApplicationSyncService {
);
const application = await this.applicationService.create({
universalIdentifier: manifest.universalIdentifier,
name: manifest.name,
description: manifest.description,
version: manifest.version,
universalIdentifier: manifest.application.universalIdentifier,
name,
description: manifest.application.description,
version: packageJson.version,
sourcePath: 'cli-sync', // Placeholder for CLI-synced apps
serverlessFunctionLayerId: serverlessFunctionLayer.id,
workspaceId,
});
await this.applicationVariableService.upsertManyApplicationVariableEntitys(
await this.applicationVariableService.upsertManyApplicationVariableEntities(
{
env: manifest.env,
applicationVariables: manifest.application.applicationVariables,
applicationId: application.id,
},
);
@@ -143,62 +140,21 @@ export class ApplicationSyncService {
);
await this.applicationService.update(application.id, {
name: manifest.name,
description: manifest.description,
version: manifest.version,
name,
description: manifest.application.description,
version: packageJson.version,
});
await this.applicationVariableService.upsertManyApplicationVariableEntitys({
env: manifest.env,
applicationId: application.id,
});
await this.applicationVariableService.upsertManyApplicationVariableEntities(
{
applicationVariables: manifest.application.applicationVariables,
applicationId: application.id,
},
);
return application;
}
private async syncAgents({
agentsToSync,
workspaceId,
applicationId,
}: {
agentsToSync: AgentManifest[];
workspaceId: string;
applicationId: string;
}) {
for (const agentToSync of agentsToSync) {
const existingAgent =
await this.agentService.findOneByApplicationAndStandardId({
workspaceId,
applicationId,
standardId: agentToSync.standardId,
});
if (isDefined(existingAgent)) {
await this.agentService.updateOneAgent(
{ id: existingAgent.id, ...agentToSync },
workspaceId,
);
return;
}
await this.agentService.createOneAgent(
{
name: agentToSync.name,
label: agentToSync.label,
description: agentToSync.description,
icon: agentToSync.icon,
prompt: agentToSync.prompt,
modelId: agentToSync.modelId,
standardId: agentToSync.standardId,
isCustom: true,
applicationId,
},
workspaceId,
);
}
}
private async syncObjects({
objectsToSync,
workspaceId,
@@ -224,27 +180,31 @@ export class ApplicationSyncService {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) as any[];
const objectsToSyncStandardIds = objectsToSync.map((obj) => obj.standardId);
const objectsToSyncUniversalIds = objectsToSync.map(
(obj) => obj.universalIdentifier,
);
const applicationObjectsStandardIds = applicationObjects.map(
(obj) => obj.standardId,
(obj) => obj.universalIdentifier,
);
const objectsToDelete = applicationObjects.filter(
(obj) =>
isDefined(obj.standardId) &&
!objectsToSyncStandardIds.includes(obj.standardId),
isDefined(obj.universalIdentifier) &&
!objectsToSyncUniversalIds.includes(obj.universalIdentifier),
);
const objectsToUpdate = applicationObjects.filter(
(obj) =>
isDefined(obj.standardId) &&
objectsToSyncStandardIds.includes(obj.standardId),
isDefined(obj.universalIdentifier) &&
objectsToSyncUniversalIds.includes(obj.universalIdentifier),
);
const objectsToCreate = objectsToSync.filter(
(objectToSync) =>
!applicationObjectsStandardIds.includes(objectToSync.standardId),
!applicationObjectsStandardIds.includes(
objectToSync.universalIdentifier,
),
);
for (const objectToDelete of objectsToDelete) {
@@ -257,12 +217,12 @@ export class ApplicationSyncService {
for (const objectToUpdate of objectsToUpdate) {
const objectToSync = objectsToSync.find(
(obj) => obj.standardId === objectToUpdate.standardId,
(obj) => obj.universalIdentifier === objectToUpdate.universalIdentifier,
);
if (!objectToSync) {
throw new ApplicationException(
`Failed to find object to sync with standardId ${objectToUpdate.standardId}`,
`Failed to find object to sync with universalIdentifier ${objectToUpdate.universalIdentifier}`,
ApplicationExceptionCode.OBJECT_NOT_FOUND,
);
}
@@ -298,7 +258,8 @@ export class ApplicationSyncService {
labelPlural: objectToCreate.labelPlural,
icon: objectToCreate.icon || undefined,
description: objectToCreate.description || undefined,
standardId: objectToCreate.standardId || undefined,
standardId: objectToCreate.universalIdentifier,
universalIdentifier: objectToCreate.universalIdentifier,
dataSourceId: dataSourceMetadata.id,
applicationId,
};
@@ -312,12 +273,14 @@ export class ApplicationSyncService {
private async syncServerlessFunctions({
serverlessFunctionsToSync,
code,
workspaceId,
applicationId,
serverlessFunctionLayerId,
}: {
serverlessFunctionsToSync: ServerlessFunctionManifest[];
workspaceId: string;
code: Sources;
applicationId: string;
serverlessFunctionLayerId: string;
}) {
@@ -392,12 +355,18 @@ export class ApplicationSyncService {
);
}
const name =
serverlessFunctionToSync.name ??
parse(serverlessFunctionToSync.handlerName).name;
const updateServerlessFunctionInput = {
id: serverlessFunctionToUpdate.id,
update: {
name: serverlessFunctionToSync.name,
name,
code,
timeoutSeconds: serverlessFunctionToSync.timeoutSeconds,
code: serverlessFunctionToSync.code,
handlerPath: serverlessFunctionToSync.handlerPath,
handlerName: serverlessFunctionToSync.handlerName,
},
};
@@ -426,11 +395,17 @@ export class ApplicationSyncService {
}
for (const serverlessFunctionToCreate of serverlessFunctionsToCreate) {
const name =
serverlessFunctionToCreate.name ??
parse(serverlessFunctionToCreate.handlerName).name;
const createServerlessFunctionInput = {
name: serverlessFunctionToCreate.name,
code: serverlessFunctionToCreate.code,
name,
code,
universalIdentifier: serverlessFunctionToCreate.universalIdentifier,
timeoutSeconds: serverlessFunctionToCreate.timeoutSeconds,
handlerPath: serverlessFunctionToCreate.handlerPath,
handlerName: serverlessFunctionToCreate.handlerName,
applicationId,
serverlessFunctionLayerId,
};
@@ -659,7 +634,7 @@ export class ApplicationSyncService {
id: triggerToUpdate.id,
update: {
settings: {
pattern: triggerToSync.schedule,
pattern: triggerToSync.pattern,
},
},
};
@@ -677,7 +652,7 @@ export class ApplicationSyncService {
const createCronTriggerInput = {
settings: {
pattern: triggerToCreate.schedule,
pattern: triggerToCreate.pattern,
},
universalIdentifier: triggerToCreate.universalIdentifier,
serverlessFunctionId,
@@ -8,7 +8,6 @@ import { ApplicationService } from 'src/engine/core-modules/application/applicat
import { ApplicationVariableEntityModule } from 'src/engine/core-modules/applicationVariable/application-variable.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
import { AgentModule } from 'src/engine/metadata-modules/agent/agent.module';
import { CronTriggerModule } from 'src/engine/metadata-modules/cron-trigger/cron-trigger.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { DatabaseEventTriggerModule } from 'src/engine/metadata-modules/database-event-trigger/database-event-trigger.module';
@@ -25,7 +24,6 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
WorkspaceManyOrAllFlatEntityMapsCacheModule,
ObjectMetadataModule,
DataSourceModule,
AgentModule,
ApplicationVariableEntityModule,
ServerlessFunctionLayerModule,
ServerlessFunctionModule,
@@ -57,11 +57,11 @@ export class ApplicationResolver {
@Mutation(() => Boolean)
async deleteApplication(
@Args() { packageJson }: DeleteApplicationInput,
@Args() { universalIdentifier }: DeleteApplicationInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
await this.applicationSyncService.deleteApplication({
applicationUniversalIdentifier: packageJson.universalIdentifier,
applicationUniversalIdentifier: universalIdentifier,
workspaceId,
});
@@ -1,11 +1,7 @@
import { ArgsType, Field } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
import { PackageJson } from 'src/engine/core-modules/application/types/application.types';
@ArgsType()
export class DeleteApplicationInput {
@Field(() => GraphQLJSON, { nullable: false })
packageJson: PackageJson;
@Field(() => String)
universalIdentifier: string;
}
@@ -1,51 +1,56 @@
import { type HTTPMethod } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
import { type ServerlessFunctionCode } from 'src/engine/metadata-modules/serverless-function/types/serverless-function-code.type';
export type PackageJson = {
$schema?: string;
universalIdentifier: string;
name: string;
description?: string;
license: string;
engines: {
node: string;
npm: string;
yarn: string;
};
env?: EnvManifest;
icon?: string;
packageManager: string;
version: string;
dependencies?: object;
devDependencies?: object;
};
export type EnvManifest = Record<string, EnvVariableManifest>;
export type EnvVariableManifest = {
type ApplicationVariable = {
universalIdentifier: string;
value?: string;
description?: string;
isSecret: boolean;
isSecret?: boolean;
};
export type AppManifest = PackageJson & {
agents: AgentManifest[];
type Sources = { [key: string]: string | Sources };
type Application = {
universalIdentifier: string;
displayName?: string;
description?: string;
icon?: string;
applicationVariables?: Record<string, ApplicationVariable>;
};
export type AppManifest = {
application: Application;
objects: ObjectManifest[];
serverlessFunctions: ServerlessFunctionManifest[];
sources: Sources;
};
export type ServerlessFunctionManifest = {
$schema?: string;
universalIdentifier: string;
name: string;
name?: string;
description?: string;
timeoutSeconds?: number;
handlerPath: string;
handlerName: string;
triggers: ServerlessFunctionTriggerManifest[];
code: ServerlessFunctionCode;
};
export type ServerlessFunctionTriggerManifest = (
| {
type: 'cron';
schedule: string;
pattern: string;
}
| {
type: 'databaseEvent';
@@ -62,8 +67,7 @@ export type ServerlessFunctionTriggerManifest = (
};
export type ObjectManifest = {
$schema?: string;
standardId: string;
universalIdentifier: string;
nameSingular: string;
namePlural: string;
labelSingular: string;
@@ -4,7 +4,7 @@ import { isDefined } from 'twenty-shared/utils';
import { In, Not, Repository } from 'typeorm';
import { ApplicationVariableEntity } from 'src/engine/core-modules/applicationVariable/application-variable.entity';
import { EnvManifest } from 'src/engine/core-modules/application/types/application.types';
import { AppManifest } from 'src/engine/core-modules/application/types/application.types';
export class ApplicationVariableEntityService {
constructor(
@@ -27,33 +27,52 @@ export class ApplicationVariableEntityService {
);
}
async upsertManyApplicationVariableEntitys({
env,
async upsertManyApplicationVariableEntities({
applicationVariables,
applicationId,
}: {
env?: EnvManifest;
applicationVariables?: AppManifest['application']['applicationVariables'];
applicationId: string;
}) {
if (!isDefined(env)) {
if (!isDefined(applicationVariables)) {
return;
}
for (const [key, { value, description, isSecret }] of Object.entries(env)) {
await this.applicationVariableRepository.upsert(
{
for (const [key, { value, description, isSecret }] of Object.entries(
applicationVariables,
)) {
if (
await this.applicationVariableRepository.findOne({
where: {
key,
applicationId,
},
})
) {
await this.applicationVariableRepository.update(
{
key,
applicationId,
},
{
description,
isSecret,
},
);
} else {
await this.applicationVariableRepository.save({
key,
value,
description,
isSecret,
applicationId,
},
{ conflictPaths: ['key', 'applicationId'] },
);
});
}
}
await this.applicationVariableRepository.delete({
applicationId,
key: Not(In(Object.keys(env))),
key: Not(In(Object.keys(applicationVariables))),
});
}
}
@@ -6,20 +6,23 @@ export const handler = async (event) => {
const mainPath = `/tmp/${randomId}.mjs`;
// eslint-disable-next-line no-undef
const oldProcessEnv = { ...process.env };
try {
const { code, params, env } = event;
const { code, params, env, handlerName } = event;
await fs.writeFile(mainPath, code, 'utf8');
// eslint-disable-next-line no-undef
process.env = { ...process.env, ...(env ?? {}) };
const mainFile = await import(mainPath);
return await mainFile.main(params);
return await mainFile[handlerName](params);
} finally {
await fs.rm(mainPath, { force: true });
// eslint-disable-next-line no-undef
process.env = oldProcessEnv;
}
};
@@ -51,6 +51,13 @@ import { buildEnvVar } from 'src/engine/core-modules/serverless/drivers/utils/bu
const UPDATE_FUNCTION_DURATION_TIMEOUT_IN_SECONDS = 60;
const CREDENTIALS_DURATION_IN_SECONDS = 60 * 60; // 1h
type LambdaDriverExecutorPayload = {
code: string;
params: object;
env: Record<string, string>;
handlerName: string;
};
export interface LambdaDriverOptions extends LambdaClientConfig {
fileStorageService: FileStorageService;
region: string;
@@ -326,8 +333,10 @@ export class LambdaDriver implements ServerlessDriver {
let builtBundleFilePath = '';
try {
builtBundleFilePath =
await buildServerlessFunctionInMemory(sourceTemporaryDir);
builtBundleFilePath = await buildServerlessFunctionInMemory({
sourceTemporaryDir,
handlerPath: serverlessFunction.handlerPath,
});
} catch (error) {
return formatBuildError(error, startTime);
}
@@ -336,10 +345,11 @@ export class LambdaDriver implements ServerlessDriver {
'utf-8',
);
const executorPayload = {
const executorPayload: LambdaDriverExecutorPayload = {
params: payload,
code: compiledCode,
env: buildEnvVar(serverlessFunction),
handlerName: serverlessFunction.handlerName,
};
const params: InvokeCommandInput = {
@@ -61,27 +61,6 @@ export class LocalDriver implements ServerlessDriver {
await this.createLayerIfNotExists(serverlessFunction);
}
private async executeWithTimeout<T>(
fn: () => Promise<T>,
timeoutMs: number,
): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`Task timed out after ${timeoutMs / 1_000} seconds`));
}, timeoutMs);
fn()
.then((result) => {
clearTimeout(timer);
resolve(result);
})
.catch((err) => {
clearTimeout(timer);
reject(err);
});
});
}
async execute(
serverlessFunction: ServerlessFunctionEntity,
payload: object,
@@ -109,8 +88,10 @@ export class LocalDriver implements ServerlessDriver {
let builtBundleFilePath = '';
try {
builtBundleFilePath =
await buildServerlessFunctionInMemory(sourceTemporaryDir);
builtBundleFilePath = await buildServerlessFunctionInMemory({
sourceTemporaryDir,
handlerPath: serverlessFunction.handlerPath,
});
} catch (error) {
return formatBuildError(error, startTime);
}
@@ -164,10 +145,11 @@ export class LocalDriver implements ServerlessDriver {
});
try {
const runnerPath = await this.writeBootstrapRunner(
sourceTemporaryDir,
builtBundleFilePath,
);
const runnerPath = await this.writeBootstrapRunner({
dir: sourceTemporaryDir,
builtFileAbsPath: builtBundleFilePath,
handlerName: serverlessFunction.handlerName,
});
const { ok, result, error, stack, stdout, stderr } =
await this.runChildWithEnv({
@@ -222,7 +204,15 @@ export class LocalDriver implements ServerlessDriver {
}
}
async writeBootstrapRunner(dir: string, builtFileAbsPath: string) {
async writeBootstrapRunner({
dir,
builtFileAbsPath,
handlerName,
}: {
dir: string;
builtFileAbsPath: string;
handlerName: string;
}) {
const runnerPath = join(dir, '__runner.cjs');
const code = `
// Auto-generated. Do not edit.
@@ -232,8 +222,8 @@ export class LocalDriver implements ServerlessDriver {
try {
const builtUrl = pathToFileURL(${JSON.stringify(builtFileAbsPath)});
const mod = await import(builtUrl.href);
if (typeof mod.main !== 'function') {
throw new Error('Export "main" not found in serverless bundle');
if (typeof mod.${handlerName} !== 'function') {
throw new Error('Export "${handlerName}" not found in serverless bundle');
}
let payload = undefined;
@@ -241,7 +231,7 @@ export class LocalDriver implements ServerlessDriver {
process.on('message', async (msg) => {
if (!msg || msg.type !== 'run') return;
try {
const out = await mod.main(msg.payload);
const out = await mod.${handlerName}(msg.payload);
process.send && process.send({ ok: true, result: out });
process.exit(0);
} catch (err) {
@@ -253,7 +243,7 @@ export class LocalDriver implements ServerlessDriver {
// Fallback: read payload from argv[2] (JSON) and print to stdout
const json = process.argv[2];
payload = json ? JSON.parse(json) : undefined;
const out = await mod.main(payload);
const out = await mod.${handlerName}(payload);
console.log(JSON.stringify({ ok: true, result: out }));
process.exit(0);
}
@@ -2,10 +2,14 @@ import { join } from 'path';
import { build } from 'esbuild';
export const buildServerlessFunctionInMemory = async (
sourceTemporaryDir: string,
) => {
const entryFilePath = join(sourceTemporaryDir, 'src', 'index.ts');
export const buildServerlessFunctionInMemory = async ({
sourceTemporaryDir,
handlerPath,
}: {
sourceTemporaryDir: string;
handlerPath: string;
}) => {
const entryFilePath = join(sourceTemporaryDir, handlerPath);
const builtBundleFilePath = join(sourceTemporaryDir, 'dist', 'main.mjs');