Skip to content

Commit f61f8e9

Browse files
adrians5jCopilot
andcommitted
feat: introduce BuildParams ext (webiny#4904)
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: adrians5j <5121148+adrians5j@users.noreply.github.com>
1 parent e7dc97b commit f61f8e9

29 files changed

Lines changed: 524 additions & 14 deletions

File tree

extensions/MyApiKeyAfterUpdate.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,32 @@
1-
import { ApiKeyAfterUpdateHandler } from "webiny/api/security/features/UpdateApiKey";
2-
1+
import { ApiKeyAfterUpdateHandler } from "webiny/api/security/apiKey";
32
import { Logger } from "webiny/api/logger";
3+
import { BuildParams } from "webiny/api/buildParams";
44

55
class MyApiKeyAfterUpdateImpl implements ApiKeyAfterUpdateHandler.Interface {
6-
constructor(private logger: Logger.Interface) {}
6+
constructor(
7+
private logger: Logger.Interface,
8+
private buildParams: BuildParams.Interface
9+
) {}
710

811
async handle() {
912
this.logger.warn("An API key was updated!");
13+
14+
// Read build params
15+
const param1 = this.buildParams.get<string>("MY_CUSTOM_BUILD_PARAM");
16+
const param2 = this.buildParams.get<{ myKey: number; nested: { foo: string } }>(
17+
"MY_CUSTOM_BUILD_PARAM-2"
18+
);
19+
20+
console.log("---- Build Params ----");
21+
22+
console.log(`Build param 1: ${param1}`);
23+
console.log(`Build param 2:`, param2);
1024
}
1125
}
1226

1327
const MyApiKeyAfterUpdate = ApiKeyAfterUpdateHandler.createImplementation({
1428
implementation: MyApiKeyAfterUpdateImpl,
15-
dependencies: [Logger]
29+
dependencies: [Logger, BuildParams]
1630
});
1731

1832
export default MyApiKeyAfterUpdate;

packages/api-core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"minimatch": "^5.1.6",
7676
"pino": "^9.14.0",
7777
"pino-lambda": "^4.4.1",
78+
"ts-morph": "^27.0.2",
7879
"zod": "^3.25.76"
7980
},
8081
"devDependencies": {

packages/api-core/src/ApiCoreFeature.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import { AdminUsersFeature } from "~/features/users/AdminUsersFeature.js";
99
import type { ApiCoreStorageOperations } from "~/types/core.js";
1010
import { IdpAuthenticatorFeature } from "~/idp/feature.js";
1111
import { KeyValueStoreFeature } from "~/features/keyValueStore/feature.js";
12+
import { BuildParamsFeature } from "~/features/buildParams/feature.js";
1213

1314
export const ApiCoreFeature = createFeature({
1415
name: "ApiCore",
1516
register(container: Container, config: ApiCoreStorageOperations) {
1617
// Register features
1718
LoggerFeature.register(container);
1819
EventPublisherFeature.register(container);
20+
BuildParamsFeature.register(container);
1921
TenancyFeature.register(container, config.tenancyStorageOperations);
2022
SecurityFeature.register(container, config.securityStorageOperations);
2123
AdminUsersFeature.register(container, config.usersStorageOperations);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { BuildParam, BuildParams } from "~/features/buildParams/index.js";
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { z } from "zod";
2+
import { Node, Project, ArrayLiteralExpression } from "ts-morph";
3+
import { defineExtension } from "@webiny/project/defineExtension";
4+
import crypto from "crypto";
5+
import path from "path";
6+
import fs from "fs";
7+
8+
export const BuildParam = defineExtension({
9+
type: "Api/BuildParam",
10+
tags: { runtimeContext: "app-build", appName: "api" },
11+
description: "Add build-time parameter to API app.",
12+
multiple: true,
13+
paramsSchema: () => {
14+
return z.object({
15+
paramName: z.string(),
16+
value: z.union([
17+
z.string(),
18+
z.record(z.any()),
19+
z.array(z.any()),
20+
z.number(),
21+
z.boolean()
22+
])
23+
});
24+
},
25+
async build(params, ctx) {
26+
const extensionsTsFilePath = ctx.project.paths.workspaceFolder
27+
.join("apps", "api", "graphql", "src", "extensions.ts")
28+
.toString();
29+
30+
const buildParamsDir = ctx.project.paths.workspaceFolder
31+
.join("apps", "api", "graphql", "src", "buildParams")
32+
.toString();
33+
34+
const { paramName, value } = params;
35+
36+
// Serialize value to a TypeScript literal.
37+
const valueStr = JSON.stringify(value, null, 4);
38+
39+
// Generate a unique class name based on the paramName.
40+
const hash = crypto.createHash("sha256").update(paramName).digest("hex");
41+
const className = `BuildParam_${hash.slice(-10)}`;
42+
const fileName = `${className}.ts`;
43+
const filePath = path.join(buildParamsDir, fileName);
44+
45+
// Ensure buildParams directory exists.
46+
if (!fs.existsSync(buildParamsDir)) {
47+
fs.mkdirSync(buildParamsDir, { recursive: true });
48+
}
49+
50+
// Check if file already exists.
51+
if (fs.existsSync(filePath)) {
52+
// File exists, just ensure it's imported in extensions.ts
53+
} else {
54+
// Create the BuildParam implementation file.
55+
const fileContent = `import { BuildParam } from "webiny/api/buildParams";
56+
57+
class ${className} implements BuildParam.Interface {
58+
key = "${paramName}";
59+
value = ${valueStr};
60+
}
61+
62+
export default BuildParam.createImplementation({
63+
implementation: ${className},
64+
dependencies: []
65+
});
66+
`;
67+
fs.writeFileSync(filePath, fileContent, "utf8");
68+
}
69+
70+
// Now update extensions.ts to import and register this BuildParam.
71+
const project = new Project();
72+
project.addSourceFileAtPath(extensionsTsFilePath);
73+
74+
const source = project.getSourceFileOrThrow(extensionsTsFilePath);
75+
76+
// Calculate import path relative to extensions.ts.
77+
let importPath = path
78+
.relative(path.dirname(extensionsTsFilePath), filePath)
79+
.replace(/\.tsx?$/, ".js");
80+
81+
// Ensure the path starts with ./
82+
if (!importPath.startsWith(".")) {
83+
importPath = "./" + importPath;
84+
}
85+
86+
// Check if import already exists.
87+
const existingImportDeclaration = source.getImportDeclaration(importPath);
88+
if (existingImportDeclaration) {
89+
return;
90+
}
91+
92+
let index = 1;
93+
94+
const importDeclarations = source.getImportDeclarations();
95+
if (importDeclarations.length) {
96+
const last = importDeclarations[importDeclarations.length - 1];
97+
index = last.getChildIndex() + 1;
98+
}
99+
100+
// Add import for the BuildParam implementation.
101+
source.insertImportDeclaration(index, {
102+
defaultImport: className,
103+
moduleSpecifier: importPath
104+
});
105+
106+
// Add the registration to the plugins array.
107+
const pluginsArray = source.getFirstDescendant(node =>
108+
Node.isArrayLiteralExpression(node)
109+
) as ArrayLiteralExpression;
110+
111+
pluginsArray.addElement(
112+
`\ncreateContextPlugin(ctx => {\n\tregisterExtension(ctx.container, ${className});\n})`
113+
);
114+
115+
{
116+
let index = 1;
117+
118+
const importDeclarations = source.getImportDeclarations();
119+
if (importDeclarations.length) {
120+
const last = importDeclarations[importDeclarations.length - 1];
121+
index = last.getChildIndex() + 1;
122+
}
123+
124+
const contextPluginImportPath = "@webiny/api/plugins/ContextPlugin";
125+
const existingContextPluginImport =
126+
source.getImportDeclaration(contextPluginImportPath);
127+
if (!existingContextPluginImport) {
128+
source.insertImportDeclaration(index, {
129+
namedImports: ["createContextPlugin"],
130+
moduleSpecifier: contextPluginImportPath
131+
});
132+
}
133+
}
134+
135+
await source.save();
136+
}
137+
});

packages/api-core/src/extensions/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,11 @@ import {
3838

3939
// Generic extension that supports any API abstraction.
4040
import { GenericExtension } from "./GenericExtension.js";
41+
import { BuildParam } from "./BuildParam.js";
4142

4243
// Exports.
4344
export { GenericExtension };
45+
export { BuildParam };
4446
export { ApiKeyBeforeCreate };
4547
export { ApiKeyAfterCreate };
4648
export { ApiKeyBeforeUpdate };
@@ -85,6 +87,7 @@ export { SystemInstalled };
8587
// Definitions (used internally). 👇
8688
export const definitions = [
8789
GenericExtension.def,
90+
BuildParam.def,
8891
ApiKeyBeforeCreate.def,
8992
ApiKeyAfterCreate.def,
9093
ApiKeyBeforeUpdate.def,
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { BuildParam, BuildParams as BuildParamsAbstraction } from "./abstractions.js";
2+
3+
export class BuildParamsImpl implements BuildParamsAbstraction.Interface {
4+
constructor(private params: BuildParam.Interface[]) {}
5+
6+
get<T = any>(key: string): T | null {
7+
const param = this.params.find(p => p.key === key);
8+
return param ? param.value : null;
9+
}
10+
}
11+
12+
export const BuildParams = BuildParamsAbstraction.createImplementation({
13+
implementation: BuildParamsImpl,
14+
dependencies: [[BuildParam, { multiple: true }]]
15+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { createAbstraction } from "@webiny/feature/api";
2+
3+
export interface IBuildParam {
4+
key: string;
5+
value: any;
6+
}
7+
8+
export const BuildParam = createAbstraction<IBuildParam>("BuildParam");
9+
10+
export namespace BuildParam {
11+
export type Interface = IBuildParam;
12+
}
13+
14+
export interface IBuildParams {
15+
get<T = any>(key: string): T | null;
16+
}
17+
18+
export const BuildParams = createAbstraction<IBuildParams>("BuildParams");
19+
20+
export namespace BuildParams {
21+
export type Interface = IBuildParams;
22+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { createFeature } from "@webiny/feature/api";
2+
import { Container } from "@webiny/di";
3+
import { BuildParams } from "./BuildParams.js";
4+
5+
export const BuildParamsFeature = createFeature({
6+
name: "BuildParamsFeature",
7+
register(container: Container) {
8+
container.register(BuildParams);
9+
}
10+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { BuildParam, BuildParams } from "./abstractions.js";

0 commit comments

Comments
 (0)