@starci/hfs 4.2.13 → 4.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +7 -3
- package/emit/db-types.mjs +2 -1
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +200 -8
- package/runtime/knowledge/hfs/fe-stack.yaml +221 -0
- package/runtime/knowledge/hfs/rules.yaml +50 -47
- package/runtime/knowledge/hfs/slots.yaml +307 -179
- package/runtime/knowledge/patterns/be/event-bus.yaml +15 -0
- package/runtime/knowledge/patterns/be/jobs.yaml +10 -0
- package/runtime/knowledge/patterns/be/projections.yaml +10 -0
- package/runtime/knowledge/patterns/be/queues.yaml +15 -0
- package/runtime/knowledge/patterns/be/reactors.yaml +8 -0
- package/runtime/knowledge/patterns/be/realtime.yaml +17 -0
- package/runtime/knowledge/patterns/be/saga.yaml +13 -0
- package/runtime/knowledge/patterns/fe/folder.yaml +24 -18
- package/runtime/knowledge/sonar-gate.yaml +10 -2
- package/runtime/modules/kernel/failure-codes.yaml +40 -60
- package/runtime/modules/kernel/removed-vocabulary.yaml +43 -0
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +0 -2
- package/runtime/scripts/hfs/architecture/config.mjs +2 -1
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +6 -2
- package/runtime/scripts/hfs/architecture/error-masked.mjs +10 -1
- package/runtime/scripts/hfs/architecture/frontend.mjs +10 -3
- package/runtime/scripts/hfs/architecture/hfs.mjs +6 -3
- package/runtime/scripts/hfs/architecture/index.mjs +2 -0
- package/runtime/scripts/hfs/architecture/owners.mjs +19 -16
- package/runtime/scripts/hfs/architecture/route-files-thin.mjs +4 -1
- package/runtime/scripts/hfs/architecture/server-action.mjs +75 -20
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +2 -2
- package/runtime/scripts/hfs/architecture/supabase.mjs +36 -210
- package/runtime/scripts/hfs/architecture/tiers.mjs +1 -33
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +110 -54
- package/runtime/scripts/hfs/architecture/type-context.mjs +4 -1
- package/runtime/scripts/hfs/check.mjs +10 -6
- package/runtime/scripts/hfs/edition-slots.mjs +13 -4
- package/runtime/scripts/hfs/manifest-shape.mjs +3 -3
- package/runtime/scripts/hfs/rule-catalog.mjs +21 -1
- package/runtime/scripts/hfs/rules/database-constants.mjs +4 -0
- package/runtime/scripts/hfs/rules/database-migrations.mjs +12 -7
- package/runtime/scripts/hfs/rules/database.mjs +4 -3
- package/runtime/scripts/hfs/rules/edition.mjs +49 -37
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +4 -5
- package/runtime/scripts/hfs/rules/fe-stack.mjs +71 -0
- package/scaffold/add-app.mjs +24 -18
- package/scaffold/add-cli-lite.mjs +33 -3
- package/scaffold/add-inbox-lite.mjs +24 -8
- package/scaffold/add-table.mjs +222 -74
- package/scaffold/add.mjs +85 -35
- package/scaffold/app.mjs +140 -36
- package/scaffold/edition-gate.mjs +21 -2
- package/scaffold/lite-exports.mjs +30 -1
- package/scaffold/service.mjs +5 -8
- package/src/main.mjs +2 -2
- package/sync/index.mjs +19 -15
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +14 -0
- package/templates/app/codegen-graphql.mjs.partial +34 -0
- package/templates/app/codegen-lite/scripts/codegen.mjs +18 -13
- package/templates/app/package-scripts/core.json +1 -0
- package/templates/app/package-scripts-lite/package.json +4 -0
- package/templates/app/skeleton/README.md +1 -1
- package/templates/app/skeleton/scripts/codegen.mjs +4 -1
- package/templates/app/skeleton-lite/README.md +7 -4
- package/templates/app/skeleton-lite/supabase/config.toml +1 -1
- package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +13 -14
- package/templates/be/inbox/database-client.spec.ts.tpl +47 -0
- package/templates/be/inbox/service.spec.ts.tpl +46 -0
- package/templates/be/inbox/webhook-door.spec.ts.tpl +76 -0
- package/templates/be/patterns/api/graphql.module.ts.tpl +2 -2
- package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +6 -3
- package/templates/be/platform/errors-graphql/graphql-error.mapper.spec.ts +52 -0
- package/templates/be/platform/errors-graphql/graphql-error.mapper.ts +31 -0
- package/templates/be/platform/graphql/graphql-depth.policy.spec.ts +43 -0
- package/templates/be/platform/graphql/graphql-depth.policy.ts +21 -0
- package/templates/be/platform/graphql/graphql.contracts.ts +12 -0
- package/templates/be/platform/graphql/graphql.module-definition.ts +7 -0
- package/templates/be/platform/graphql/graphql.module.ts +49 -0
- package/templates/be/platform/graphql/graphql.options.ts +2 -0
- package/templates/be/platform/graphql/index.ts +1 -0
- package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +114 -0
- package/templates/be/platform/graphql/localize-errors.mapper.ts +51 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.ts +8 -3
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +13 -1
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.ts +11 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +21 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +94 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +2 -4
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +10 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +2 -2
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/index.ts +2 -6
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +15 -0
- package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +1 -1
- package/templates/be/table/api/create.command.ts.tpl +10 -0
- package/templates/be/table/api/create.contracts.ts.tpl +8 -0
- package/templates/be/table/api/create.handler.ts.tpl +22 -0
- package/templates/be/table/api/create.mapper.ts.tpl +5 -0
- package/templates/be/table/api/create.resolver.ts.tpl +21 -0
- package/templates/be/table/api/create.type.ts.tpl +9 -0
- package/templates/be/table/api/feature.index.ts.tpl +1 -0
- package/templates/be/table/api/feature.module.ts.tpl +7 -0
- package/templates/be/table/api/get.contracts.ts.tpl +11 -0
- package/templates/be/table/api/get.handler.ts.tpl +22 -0
- package/templates/be/table/api/get.input.ts.tpl +12 -0
- package/templates/be/table/api/get.mapper.ts.tpl +9 -0
- package/templates/be/table/api/get.query.ts.tpl +10 -0
- package/templates/be/table/api/get.resolver.ts.tpl +27 -0
- package/templates/be/table/api/get.type.ts.tpl +9 -0
- package/templates/be/table/api/graphql.module.ts.tpl +8 -0
- package/templates/be/table/error.api.ts.tpl +19 -0
- package/templates/be/table/migration.sql.tpl +13 -21
- package/templates/be/table/service.api.ts.tpl +30 -0
- package/templates/be/table/sql.api.ts.tpl +7 -0
- package/templates/fe/{skeleton-lite/apps/web → skeleton/apps/__app__}/src/components/blocks/SignInForm/index.tsx +16 -18
- package/templates/fe/{skeleton-lite/apps/web → skeleton/apps/__app__}/src/components/blocks/SignOutButton/component.tsx +7 -3
- package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignOutButton/index.tsx +23 -0
- package/templates/fe/skeleton/apps/__app__/src/components/providers/AppProviders/index.tsx +49 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/index.ts +4 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignIn.ts +29 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +27 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/effects/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/effects/useAuthSessionEffect.ts +14 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/swr/SwrSideEffects.tsx +8 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/swr/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/api/graphql/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/api/graphql/types.ts +26 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/api/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/client.ts +21 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/contracts.ts +27 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/index.ts +2 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/api.ts +52 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/hooks.ts +33 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/toast.tsx +32 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/types/ambient/styles.d.ts +2 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/ReduxProvider.tsx +11 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/hooks.ts +8 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/index.ts +3 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/slices/session.ts +26 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/store.ts +14 -0
- package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +5 -2
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/component.tsx +10 -2
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +1 -1
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +23 -0
- package/templates/fe/skeleton/apps/landing/src/modules/types/ambient/styles.d.ts +2 -0
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +51 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/providers.tsx +10 -13
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/component.tsx +8 -5
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +2 -17
- package/templates/fe/skeleton-lite/apps/web/src/modules/config/index.ts +3 -10
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/index.ts +0 -6
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +11 -8
- package/templates/fe/skeleton-lite/apps/web/src/modules/routes/index.ts +0 -3
- package/templates/fe/skeleton-lite/apps/web/src/proxy.ts +9 -25
- package/templates/fe/table/graphql/mutation.ts.tpl +25 -0
- package/templates/fe/table/graphql/mutation.types.ts.tpl +7 -0
- package/templates/fe/table/graphql/query.ts.tpl +25 -0
- package/templates/fe/table/graphql/query.types.ts.tpl +7 -0
- package/templates/fe/table/graphql/useMutateSwr.ts.tpl +21 -0
- package/templates/fe/table/graphql/useQuerySwr.ts.tpl +17 -0
- package/templates/fe/transport/graphql/clients/clients/auth.ts +39 -0
- package/templates/fe/transport/graphql/clients/clients/index.ts +2 -0
- package/templates/fe/transport/graphql/clients/clients/no-auth.ts +33 -0
- package/templates/fe/transport/graphql/clients/index.ts +2 -0
- package/templates/fe/transport/graphql/clients/links/attach-access-token.ts +9 -0
- package/templates/fe/transport/graphql/clients/links/error.ts +30 -0
- package/templates/fe/transport/graphql/clients/links/http.ts +16 -0
- package/templates/fe/transport/graphql/clients/links/index.ts +8 -0
- package/templates/fe/transport/graphql/clients/links/locale.ts +13 -0
- package/templates/fe/transport/graphql/clients/links/options.ts +17 -0
- package/templates/fe/transport/graphql/clients/links/refresh-token.ts +36 -0
- package/templates/fe/transport/graphql/clients/links/retry.ts +16 -0
- package/templates/fe/transport/graphql/clients/links/timeout.ts +28 -0
- package/templates/fe/transport/graphql/clients/response.ts +43 -0
- package/upgrade/check.mjs +1 -1
- package/upgrade/index.mjs +3 -2
- package/runtime/scripts/hfs/architecture/supabase-results.mjs +0 -182
- package/templates/app/quality-config-lite/sonar-project.properties +0 -5
- package/templates/be/patterns/api/feature.index.http.ts.tpl +0 -1
- package/templates/be/patterns/api/http.controller.ts.tpl +0 -27
- package/templates/be/patterns/api/http.mapper.ts.tpl +0 -9
- package/templates/be/patterns/api/http.module.ts.tpl +0 -7
- package/templates/be/patterns/api/http.request.ts.tpl +0 -10
- package/templates/be/patterns/api/http.response.ts.tpl +0 -5
- package/templates/fe/skeleton-lite/apps/web/src/app/auth/callback/route.ts +0 -24
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignOutButton/index.tsx +0 -38
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/read-session.ts +0 -25
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-in.ts +0 -25
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +0 -17
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/browser.ts +0 -14
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/index.ts +0 -3
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/outcome.ts +0 -59
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/principal.ts +0 -29
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/server.ts +0 -70
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +0 -58
- package/templates/fe/skeleton-lite/apps/web/src/modules/routes/safe-next-path.ts +0 -26
- package/templates/fe/table/read.ts.tpl +0 -16
- package/templates/fe/table/write.ts.tpl +0 -16
- /package/templates/be/patterns/api/{application.command.http.ts.tpl → application.command.principal.ts.tpl} +0 -0
- /package/templates/be/patterns/api/{application.handler.http.ts.tpl → application.handler.principal.ts.tpl} +0 -0
- /package/templates/fe/{skeleton-lite/apps/web → skeleton/apps/__app__}/src/components/blocks/SignInForm/component.tsx +0 -0
package/scaffold/add-table.mjs
CHANGED
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
} from "../runtime/scripts/hfs/slots.mjs";
|
|
8
8
|
import { render, TEMPLATES_DIR } from "../sync/index.mjs";
|
|
9
9
|
import { ScaffoldError, pascalOf } from "./service.mjs";
|
|
10
|
-
import { registerLiteExports } from "./lite-exports.mjs";
|
|
10
|
+
import { filesBelow, registerLiteDomainService, registerLiteExports, registrationFiles } from "./lite-exports.mjs";
|
|
11
|
+
import { byCodeUnit } from "../report/order.mjs";
|
|
12
|
+
import { coreFeApps } from "./app.mjs";
|
|
11
13
|
|
|
12
14
|
const NAME = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
|
|
13
15
|
const MIGRATION = /^(\d{14})_[a-z][a-z0-9]*(?:-[a-z0-9]+)*\.sql$/;
|
|
@@ -158,34 +160,168 @@ function removeEmptyParents(file, stop) {
|
|
|
158
160
|
}
|
|
159
161
|
}
|
|
160
162
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const lines =
|
|
163
|
+
/** The app-root relative path of a file under the app (posix). */
|
|
164
|
+
const at = (root, relative) => path.join(root, ...relative.split("/"));
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Registers `entry` in the `imports` array of a Nest module file (one import line after the last import, one array entry right
|
|
168
|
+
* after `imports: [`), unless `symbol` is already imported. Returns the new text.
|
|
169
|
+
*/
|
|
170
|
+
function wireImport(text, relative, { importLine, symbol, entry, kinds }) {
|
|
171
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
172
|
+
if (lines.some((line) => line.startsWith("import ") && new RegExp(`\\b${symbol}\\b`).test(line))) return text;
|
|
170
173
|
const lastImport = lines.reduce((last, line, index) => (line.startsWith("import ") ? index : last), -1);
|
|
174
|
+
if (lastImport < 0 || lines.findIndex((line) => /^\s*imports: \[$/.test(line)) < 0)
|
|
175
|
+
throw new ScaffoldError("HFS_ADD_WIRE_INVALID", `add table could not find the imports array in ${relative}`);
|
|
176
|
+
lines.splice(lastImport + 1, 0, importLine);
|
|
171
177
|
const imports = lines.findIndex((line) => /^\s*imports: \[$/.test(line));
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
lines.splice(shiftedImports + 1, 0, `${indent} ${symbol}.register({ isGlobal: true }),`);
|
|
180
|
-
const shiftedKinds = lines.findIndex((line) => /^\s*kinds: \[$/.test(line));
|
|
181
|
-
const kindsIndent = /^(\s*)/.exec(lines[shiftedKinds])?.[1] ?? "";
|
|
182
|
-
lines.splice(shiftedKinds + 1, 0, `${kindsIndent} ${errorKinds},`);
|
|
178
|
+
const indent = /^(\s*)/.exec(lines[imports])?.[1] ?? "";
|
|
179
|
+
lines.splice(imports + 1, 0, `${indent} ${entry},`);
|
|
180
|
+
if (kinds !== undefined) {
|
|
181
|
+
const kindsAt = lines.findIndex((line) => /^\s*kinds: \[$/.test(line));
|
|
182
|
+
if (kindsAt < 0) throw new ScaffoldError("HFS_ADD_WIRE_INVALID", `add table could not find the error kinds array in ${relative}`);
|
|
183
|
+
const kindsIndent = /^(\s*)/.exec(lines[kindsAt])?.[1] ?? "";
|
|
184
|
+
lines.splice(kindsAt + 1, 0, `${kindsIndent} ${kinds},`);
|
|
183
185
|
}
|
|
184
|
-
|
|
186
|
+
return `${lines.join("\n").replace(/\n+$/, "")}\n`;
|
|
185
187
|
}
|
|
186
188
|
|
|
189
|
+
/** The api app root after the table's domain module is registered (its module and error kinds). */
|
|
190
|
+
const wireDomainModule = (text, relative, name) =>
|
|
191
|
+
wireImport(text, relative, {
|
|
192
|
+
importLine: `import { ${snakeOf(name).toUpperCase()}_ERROR_KINDS, ${pascalOf(name)}Module } from "@modules/domain/${name}"`,
|
|
193
|
+
symbol: `${pascalOf(name)}Module`,
|
|
194
|
+
entry: `${pascalOf(name)}Module.register({ isGlobal: true })`,
|
|
195
|
+
kinds: `${snakeOf(name).toUpperCase()}_ERROR_KINDS`,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
/** The api app root after the GraphQL server (platform/graphql) and the table's GraphQL transport are registered. */
|
|
199
|
+
const wireGraphqlFeature = (text, relative, name) =>
|
|
200
|
+
wireImport(
|
|
201
|
+
wireImport(text, relative, {
|
|
202
|
+
importLine: 'import { GraphqlModule } from "@modules/platform/graphql"',
|
|
203
|
+
symbol: "GraphqlModule",
|
|
204
|
+
entry: "GraphqlModule.register({ isGlobal: true })",
|
|
205
|
+
}),
|
|
206
|
+
relative,
|
|
207
|
+
{
|
|
208
|
+
importLine: `import { ${pascalOf(name)}GraphqlModule } from "@features/api/${name}"`,
|
|
209
|
+
symbol: `${pascalOf(name)}GraphqlModule`,
|
|
210
|
+
entry: `${pascalOf(name)}GraphqlModule`,
|
|
211
|
+
},
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
/** The platform GraphQL server capability (templates/be/platform/graphql), whole or absent: written once, by the first GraphQL feature. */
|
|
215
|
+
const GRAPHQL_PLATFORM = "be/src/modules/platform/graphql";
|
|
216
|
+
/** The GraphQL error formatter the errors capability owns (BE_ERROR_MASKED: the registration passes platform/errors' formatter). */
|
|
217
|
+
const ERRORS_PLATFORM = "be/src/modules/platform/errors";
|
|
218
|
+
/** Every file the first GraphQL feature writes: platform/graphql whole and the GraphQL formatter of platform/errors. */
|
|
219
|
+
export const graphqlPlatformFiles = () =>
|
|
220
|
+
[
|
|
221
|
+
["graphql", GRAPHQL_PLATFORM],
|
|
222
|
+
["errors-graphql", ERRORS_PLATFORM],
|
|
223
|
+
].flatMap(([templates, target]) => {
|
|
224
|
+
const dir = path.join(TEMPLATES_DIR, "be", "platform", templates);
|
|
225
|
+
return fs.readdirSync(dir).sort().map((file) => ({
|
|
226
|
+
relative: `${target}/${file}`,
|
|
227
|
+
body: fs.readFileSync(path.join(dir, file), "utf8").replace(/\r\n/g, "\n"),
|
|
228
|
+
}));
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The fe GraphQL transport of one product app (templates/fe/transport/graphql/clients): the auth and no-auth Apollo clients,
|
|
233
|
+
* their link chain and the GraphQLResponse mapping, whole or absent, written once by the first GraphQL operation of the app,
|
|
234
|
+
* as platform/graphql is on the back end. A new app has no operation, so it carries no client.
|
|
235
|
+
*/
|
|
236
|
+
const feTransportFiles = (app) => {
|
|
237
|
+
const dir = path.join(TEMPLATES_DIR, "fe", "transport", "graphql", "clients");
|
|
238
|
+
return filesBelow(dir)
|
|
239
|
+
.map((file) => path.relative(dir, file).split(path.sep).join("/"))
|
|
240
|
+
.sort(byCodeUnit)
|
|
241
|
+
.map((rel) => ({
|
|
242
|
+
relative: `fe/apps/${app}/src/modules/api/graphql/clients/${rel}`,
|
|
243
|
+
body: fs.readFileSync(path.join(dir, ...rel.split("/")), "utf8").replace(/\r\n/g, "\n"),
|
|
244
|
+
}));
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
/** The env reader the transport's http link calls (modules/config, the app's one process.env reader), added with the transport. */
|
|
248
|
+
const GRAPHQL_URL_READER = [
|
|
249
|
+
"/** The back end's GraphQL endpoint, the terminal link of modules/api; throws when `NEXT_PUBLIC_GRAPHQL_URL` is not set. */",
|
|
250
|
+
'export const graphqlUrl = (): string => required("NEXT_PUBLIC_GRAPHQL_URL", process.env.NEXT_PUBLIC_GRAPHQL_URL)',
|
|
251
|
+
].join("\n");
|
|
252
|
+
|
|
253
|
+
/** An app's modules/config text with graphqlUrl appended once. */
|
|
254
|
+
const withGraphqlUrl = (text) =>
|
|
255
|
+
/^export const graphqlUrl\b/m.test(text) ? text : `${text.replace(/\s*$/, "")}\n${GRAPHQL_URL_READER}\n`;
|
|
256
|
+
|
|
257
|
+
/** The be api feature of a table (owner ruling 2026-10-13, lite = full): get-<singular> and create-<singular> over GraphQL. */
|
|
258
|
+
const apiFeatureFiles = (values) => {
|
|
259
|
+
const base = `be/src/features/api/${values.name}`;
|
|
260
|
+
const get = `get-${values.singular}`;
|
|
261
|
+
const create = `create-${values.singular}`;
|
|
262
|
+
return [
|
|
263
|
+
[`${base}/index.ts`, "feature.index.ts.tpl"],
|
|
264
|
+
[`${base}/${values.name}.module.ts`, "feature.module.ts.tpl"],
|
|
265
|
+
[`${base}/application/${get}.query.ts`, "get.query.ts.tpl"],
|
|
266
|
+
[`${base}/application/${get}.handler.ts`, "get.handler.ts.tpl"],
|
|
267
|
+
[`${base}/application/${get}.contracts.ts`, "get.contracts.ts.tpl"],
|
|
268
|
+
[`${base}/application/${create}.command.ts`, "create.command.ts.tpl"],
|
|
269
|
+
[`${base}/application/${create}.handler.ts`, "create.handler.ts.tpl"],
|
|
270
|
+
[`${base}/application/${create}.contracts.ts`, "create.contracts.ts.tpl"],
|
|
271
|
+
[`${base}/transport/graphql/${get}.resolver.ts`, "get.resolver.ts.tpl"],
|
|
272
|
+
[`${base}/transport/graphql/${get}.mapper.ts`, "get.mapper.ts.tpl"],
|
|
273
|
+
[`${base}/transport/graphql/dto/${get}.input.ts`, "get.input.ts.tpl"],
|
|
274
|
+
[`${base}/transport/graphql/dto/${get}.type.ts`, "get.type.ts.tpl"],
|
|
275
|
+
[`${base}/transport/graphql/${create}.resolver.ts`, "create.resolver.ts.tpl"],
|
|
276
|
+
[`${base}/transport/graphql/${create}.mapper.ts`, "create.mapper.ts.tpl"],
|
|
277
|
+
[`${base}/transport/graphql/dto/${create}.type.ts`, "create.type.ts.tpl"],
|
|
278
|
+
[`${base}/transport/graphql/${values.name}-graphql.module.ts`, "graphql.module.ts.tpl"],
|
|
279
|
+
].map(([relative, file]) => ({ relative, body: template(`be/table/api/${file}`, values) }));
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* The fe data code of a table in one product app: one GraphQL operation file per operation (modules/api/graphql/{queries,
|
|
284
|
+
* mutations}, its request/data types from the generated wire types) and one SWR hook per operation (hooks/swr/api/graphql).
|
|
285
|
+
* Never a Server Action and never a Supabase reader: Supabase on the front end is the Auth client only.
|
|
286
|
+
*/
|
|
287
|
+
const feOperationFiles = (app, values) => {
|
|
288
|
+
const api = `fe/apps/${app}/src/modules/api/graphql`;
|
|
289
|
+
const swr = `fe/apps/${app}/src/hooks/swr/api/graphql`;
|
|
290
|
+
return [
|
|
291
|
+
[`${api}/queries/query-${values.singular}.ts`, "query.ts.tpl"],
|
|
292
|
+
[`${api}/queries/types/${values.singular}.ts`, "query.types.ts.tpl"],
|
|
293
|
+
[`${api}/mutations/mutation-create-${values.singular}.ts`, "mutation.ts.tpl"],
|
|
294
|
+
[`${api}/mutations/types/create-${values.singular}.ts`, "mutation.types.ts.tpl"],
|
|
295
|
+
[`${swr}/queries/useQuery${values.Singular}Swr.ts`, "useQuerySwr.ts.tpl"],
|
|
296
|
+
[`${swr}/mutations/useMutateCreate${values.Singular}Swr.ts`, "useMutateSwr.ts.tpl"],
|
|
297
|
+
].map(([relative, file]) => ({ relative, body: template(`fe/table/graphql/${file}`, values) }));
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
/** The barrel lines that make one table's SWR hooks reachable from hooks/swr (each barrel created when absent). */
|
|
301
|
+
const feBarrels = (app, values) => {
|
|
302
|
+
const swr = `fe/apps/${app}/src/hooks/swr`;
|
|
303
|
+
return [
|
|
304
|
+
[`${swr}/api/graphql/queries/index.ts`, `export { QUERY_${values.singularUpper}_SWR_KEY, useQuery${values.Singular}Swr } from "./useQuery${values.Singular}Swr"`],
|
|
305
|
+
[`${swr}/api/graphql/mutations/index.ts`, `export { MUTATE_CREATE_${values.singularUpper}_SWR_KEY, useMutateCreate${values.Singular}Swr } from "./useMutateCreate${values.Singular}Swr"`],
|
|
306
|
+
[`${swr}/api/graphql/index.ts`, 'export * from "./mutations"'],
|
|
307
|
+
[`${swr}/api/graphql/index.ts`, 'export * from "./queries"'],
|
|
308
|
+
[`${swr}/api/index.ts`, 'export * from "./graphql"'],
|
|
309
|
+
// the owner's public entry names its hooks explicitly (ARCH_OWNER_EXPORT_STAR); the inner barrels may re-export whole
|
|
310
|
+
[`${swr}/index.ts`, `export { QUERY_${values.singularUpper}_SWR_KEY, useQuery${values.Singular}Swr } from "./api"`],
|
|
311
|
+
[`${swr}/index.ts`, `export { MUTATE_CREATE_${values.singularUpper}_SWR_KEY, useMutateCreate${values.Singular}Swr } from "./api"`],
|
|
312
|
+
];
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
/** A barrel text with `line` added once, kept in code-unit order. */
|
|
316
|
+
const withExport = (text, line) => {
|
|
317
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n").filter(Boolean);
|
|
318
|
+
if (lines.includes(line)) return text;
|
|
319
|
+
return `${[...lines, line].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).join("\n")}\n`;
|
|
320
|
+
};
|
|
321
|
+
|
|
187
322
|
/**
|
|
188
|
-
* Adds one Supabase table migration and
|
|
323
|
+
* Adds one Supabase table migration and its domain capability; with `fe`, the table's api feature over GraphQL (the GraphQL
|
|
324
|
+
* server capability on first use) and, in every product Next app, its operation files and SWR hooks.
|
|
189
325
|
* `emitTypes` returns the generated database.types.ts text; false is the explicit `--no-types` seam.
|
|
190
326
|
*/
|
|
191
327
|
export function addTable({
|
|
@@ -233,6 +369,7 @@ export function addTable({
|
|
|
233
369
|
`the current UTC timestamp ${stamp} is not later than the latest migration ${latest}; fix the clock or wait for the next second`,
|
|
234
370
|
);
|
|
235
371
|
|
|
372
|
+
const singular = singularOf(feature);
|
|
236
373
|
const values = {
|
|
237
374
|
name: feature,
|
|
238
375
|
table,
|
|
@@ -240,57 +377,51 @@ export function addTable({
|
|
|
240
377
|
Name: pascalOf(feature),
|
|
241
378
|
nameCamel: camelOf(feature),
|
|
242
379
|
upper: table.toUpperCase(),
|
|
243
|
-
singular
|
|
244
|
-
Singular: pascalOf(
|
|
380
|
+
singular,
|
|
381
|
+
Singular: pascalOf(singular),
|
|
382
|
+
singularCamel: camelOf(singular),
|
|
383
|
+
singularUpper: snakeOf(singular).toUpperCase(),
|
|
245
384
|
};
|
|
385
|
+
const apps = fe ? coreFeApps(repo.sides?.fe?.apps ?? []) : [];
|
|
386
|
+
if (fe && !apps.length)
|
|
387
|
+
throw new ScaffoldError(
|
|
388
|
+
"HFS_ADD_NO_FRONT_END",
|
|
389
|
+
"add table --fe needs at least one declared product Next app",
|
|
390
|
+
);
|
|
391
|
+
const api = fe ? ".api" : "";
|
|
392
|
+
const domain = `be/src/modules/domain/${feature}`;
|
|
246
393
|
const planned = [
|
|
247
394
|
{
|
|
248
395
|
relative: `supabase/migrations/${stamp}_${feature}.sql`,
|
|
249
396
|
body: template("be/table/migration.sql.tpl", values),
|
|
250
397
|
},
|
|
251
|
-
{ relative:
|
|
252
|
-
{ relative:
|
|
253
|
-
{ relative:
|
|
254
|
-
{ relative:
|
|
255
|
-
{ relative:
|
|
256
|
-
{ relative:
|
|
257
|
-
{ relative:
|
|
258
|
-
{ relative:
|
|
398
|
+
{ relative: `${domain}/index.ts`, body: template("be/table/index.ts.tpl", values) },
|
|
399
|
+
{ relative: `${domain}/errors/${feature}.error.ts`, body: template(`be/table/error${api}.ts.tpl`, values) },
|
|
400
|
+
{ relative: `${domain}/${feature}.module.ts`, body: template("be/table/module.ts.tpl", values) },
|
|
401
|
+
{ relative: `${domain}/${feature}.module-definition.ts`, body: template("be/table/module-definition.ts.tpl", values) },
|
|
402
|
+
{ relative: `${domain}/${feature}.options.ts`, body: template("be/table/options.ts.tpl", values) },
|
|
403
|
+
{ relative: `${domain}/${feature}.service.ts`, body: template(`be/table/service${api}.ts.tpl`, values) },
|
|
404
|
+
{ relative: `${domain}/persistence/${feature}.rows.ts`, body: template("be/table/rows.ts.tpl", values) },
|
|
405
|
+
{ relative: `${domain}/persistence/${feature}.sql.ts`, body: template(`be/table/sql${api}.ts.tpl`, values) },
|
|
259
406
|
];
|
|
260
407
|
const databaseSql = "be/src/modules/platform/database/database.sql.ts";
|
|
261
|
-
if (repo.edition === "lite" && !fs.existsSync(
|
|
408
|
+
if (repo.edition === "lite" && !fs.existsSync(at(root, databaseSql))) {
|
|
262
409
|
planned.splice(1, 0, {
|
|
263
410
|
relative: databaseSql,
|
|
264
411
|
body: render(fs.readFileSync(path.join(TEMPLATES_DIR, "be", "skeleton-lite", "src", "modules", "platform", "database", "database.sql.ts"), "utf8"), {}),
|
|
265
412
|
});
|
|
266
413
|
}
|
|
267
414
|
if (fe) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (!apps.length)
|
|
271
|
-
throw new ScaffoldError(
|
|
272
|
-
"HFS_ADD_NO_FRONT_END",
|
|
273
|
-
"add table --fe needs at least one declared Next app",
|
|
274
|
-
);
|
|
415
|
+
planned.push(...apiFeatureFiles(values));
|
|
416
|
+
if (!fs.existsSync(at(root, `${GRAPHQL_PLATFORM}/index.ts`))) planned.push(...graphqlPlatformFiles());
|
|
275
417
|
for (const app of apps) {
|
|
276
|
-
|
|
277
|
-
planned.push(
|
|
278
|
-
{
|
|
279
|
-
relative: `${base}/read-${feature}.ts`,
|
|
280
|
-
body: template("fe/table/read.ts.tpl", values),
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
relative: `${base}/write-${feature}.ts`,
|
|
284
|
-
body: template("fe/table/write.ts.tpl", values),
|
|
285
|
-
},
|
|
286
|
-
);
|
|
418
|
+
if (!fs.existsSync(at(root, `fe/apps/${app.name}/src/modules/api/graphql/clients/index.ts`))) planned.push(...feTransportFiles(app.name));
|
|
419
|
+
planned.push(...feOperationFiles(app.name, values));
|
|
287
420
|
}
|
|
288
421
|
}
|
|
289
422
|
|
|
290
423
|
const clash = planned
|
|
291
|
-
.filter((file) =>
|
|
292
|
-
fs.existsSync(path.join(root, ...file.relative.split("/"))),
|
|
293
|
-
)
|
|
424
|
+
.filter((file) => fs.existsSync(at(root, file.relative)))
|
|
294
425
|
.map((file) => file.relative);
|
|
295
426
|
if (clash.length)
|
|
296
427
|
throw new ScaffoldError(
|
|
@@ -300,41 +431,58 @@ export function addTable({
|
|
|
300
431
|
const owner = repo.sides.be.apps.find((app) => app.name === connection.owner && app.kind === "api");
|
|
301
432
|
if (!owner)
|
|
302
433
|
throw new ScaffoldError("HFS_ADD_NO_API_OWNER", `add table needs the Supabase connection owner ${connection.owner} to be a declared api app`);
|
|
303
|
-
const
|
|
304
|
-
if (!fs.existsSync(
|
|
305
|
-
throw new ScaffoldError("HFS_ADD_WIRE_MISSING", `add table registers ${name} in
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
const
|
|
310
|
-
|
|
434
|
+
const ownerRelative = `be/apps/${owner.name}/src/app.module.ts`;
|
|
435
|
+
if (!fs.existsSync(at(root, ownerRelative)))
|
|
436
|
+
throw new ScaffoldError("HFS_ADD_WIRE_MISSING", `add table registers ${name} in ${ownerRelative}, which does not exist`);
|
|
437
|
+
|
|
438
|
+
// Every existing file this run edits, as it was: restored on any failure, with every created file removed.
|
|
439
|
+
const touched = new Map();
|
|
440
|
+
const edit = (relative, change) => {
|
|
441
|
+
const file = at(root, relative);
|
|
442
|
+
if (!touched.has(file)) touched.set(file, fs.existsSync(file) ? fs.readFileSync(file) : null);
|
|
443
|
+
const before = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
444
|
+
const after = change(before);
|
|
445
|
+
if (after === before) return;
|
|
446
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
447
|
+
fs.writeFileSync(file, after);
|
|
448
|
+
};
|
|
311
449
|
const created = [];
|
|
312
450
|
try {
|
|
313
451
|
for (const file of planned) {
|
|
314
|
-
const target =
|
|
452
|
+
const target = at(root, file.relative);
|
|
315
453
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
316
454
|
fs.writeFileSync(target, file.body);
|
|
317
455
|
created.push(target);
|
|
318
456
|
}
|
|
319
457
|
if (typeof emitTypes === "function") {
|
|
320
458
|
const generated = emitTypes({ root });
|
|
321
|
-
if (typeof generated === "string")
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
459
|
+
if (typeof generated === "string") edit(dbTypesPath, () => generated);
|
|
460
|
+
}
|
|
461
|
+
edit(ownerRelative, (text) => wireDomainModule(text, ownerRelative, feature));
|
|
462
|
+
const registrations = [
|
|
463
|
+
...(repo.edition === "lite" ? ["table"] : []),
|
|
464
|
+
...(fe ? ["api", "graphql"] : []),
|
|
465
|
+
];
|
|
466
|
+
for (const generator of registrations) for (const relative of registrationFiles(generator)) edit(relative, (text) => text);
|
|
467
|
+
for (const generator of registrations) registerLiteExports({ root, generator });
|
|
468
|
+
if (fe) {
|
|
469
|
+
registerLiteDomainService({ root, moduleSpecifier: `@modules/domain/${feature}`, service: `${values.Name}Service` });
|
|
470
|
+
edit(ownerRelative, (text) => wireGraphqlFeature(text, ownerRelative, feature));
|
|
471
|
+
for (const app of apps) {
|
|
472
|
+
for (const [relative, line] of feBarrels(app.name, values)) edit(relative, (text) => withExport(text, line));
|
|
473
|
+
edit(`fe/apps/${app.name}/src/modules/config/index.ts`, withGraphqlUrl);
|
|
325
474
|
}
|
|
326
475
|
}
|
|
327
|
-
wireApiModule(root, owner.name, feature);
|
|
328
|
-
if (repo.edition === "lite") registerLiteExports({ root, generator: "table" });
|
|
329
476
|
} catch (error) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
477
|
+
for (const [file, before] of touched) {
|
|
478
|
+
if (before === null) fs.rmSync(file, { force: true });
|
|
479
|
+
else fs.writeFileSync(file, before);
|
|
480
|
+
}
|
|
334
481
|
for (const file of [...created].reverse()) {
|
|
335
482
|
if (fs.existsSync(file)) fs.rmSync(file);
|
|
336
483
|
removeEmptyParents(file, root);
|
|
337
484
|
}
|
|
485
|
+
for (const [file, before] of touched) if (before === null) removeEmptyParents(file, root);
|
|
338
486
|
throw error;
|
|
339
487
|
}
|
|
340
488
|
return {
|
package/scaffold/add.mjs
CHANGED
|
@@ -20,12 +20,12 @@ import { createSlotResolver, loadSlotManifest, readRepoDeclaration, ruleParams }
|
|
|
20
20
|
import { parseYaml } from '../runtime/engine/yaml.mjs';
|
|
21
21
|
import { TEMPLATES_DIR } from '../sync/index.mjs';
|
|
22
22
|
import { ScaffoldError, pascalOf } from './service.mjs';
|
|
23
|
-
import { refuseInEdition } from './edition-gate.mjs';
|
|
24
|
-
import { addTable } from './add-table.mjs';
|
|
23
|
+
import { refuseFullAdapterInLite, refuseInEdition } from './edition-gate.mjs';
|
|
24
|
+
import { addTable, graphqlPlatformFiles } from './add-table.mjs';
|
|
25
25
|
import { addApp } from './add-app.mjs';
|
|
26
|
-
import { ensureLiteCli
|
|
26
|
+
import { ensureLiteCli } from './add-cli-lite.mjs';
|
|
27
27
|
import { addLiteWebhookInbox, selectLiteWebhookEntries } from './add-inbox-lite.mjs';
|
|
28
|
-
import { jsonText } from './app.mjs';
|
|
28
|
+
import { bundledPins, jsonText, packageJsonText, peerDependencies } from './app.mjs';
|
|
29
29
|
import { byCodeUnit } from '../report/order.mjs';
|
|
30
30
|
import { registerLiteDomainService, registerLiteExports } from './lite-exports.mjs';
|
|
31
31
|
|
|
@@ -47,21 +47,20 @@ function readTree(topic) {
|
|
|
47
47
|
return doc.files;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (entry.path === 'src/features/api/<feature>/index.ts') return { ...entry, template: 'api/feature.index.http.ts.tpl' };
|
|
53
|
-
if (entry.path === 'src/features/api/<feature>/application/<action>.command.ts') return { ...entry, template: 'api/application.command.http.ts.tpl' };
|
|
54
|
-
if (entry.path === 'src/features/api/<feature>/application/<action>.handler.ts') return { ...entry, template: 'api/application.handler.http.ts.tpl' };
|
|
55
|
-
return entry;
|
|
56
|
-
}),
|
|
57
|
-
{ path: 'src/features/api/<feature>/transport/http/<action>.controller.ts', slot: 'be.transport.http', template: 'api/http.controller.ts.tpl' },
|
|
58
|
-
{ path: 'src/features/api/<feature>/transport/http/<action>.mapper.ts', slot: 'be.transport.http', template: 'api/http.mapper.ts.tpl' },
|
|
59
|
-
{ path: 'src/features/api/<feature>/transport/http/dto/<action>.request.ts', slot: 'be.transport.http', template: 'api/http.request.ts.tpl' },
|
|
60
|
-
{ path: 'src/features/api/<feature>/transport/http/dto/<action>.response.ts', slot: 'be.transport.http', template: 'api/http.response.ts.tpl' },
|
|
61
|
-
{ path: 'src/features/api/<feature>/transport/http/<feature>-http.module.ts', slot: 'be.transport.http', template: 'api/http.module.ts.tpl' },
|
|
62
|
-
];
|
|
50
|
+
/** The parsed document of the topic `topic` (knowledge/patterns/be/<topic>.yaml) the package carries. */
|
|
51
|
+
const readTopic = (topic) => ({ id: topic, doc: parseYaml(fs.readFileSync(path.join(TOPICS_DIR, `${topic}.yaml`), 'utf8')) });
|
|
63
52
|
|
|
64
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* The lite api tree: the full tree, GraphQL door included (owner ruling 2026-10-13: lite and full have one code structure), with
|
|
55
|
+
* the command and handler that hand the verified Supabase principal to the domain service (lite's identity adapter).
|
|
56
|
+
*/
|
|
57
|
+
const selectLiteApiEntries = entries => entries.map(entry => {
|
|
58
|
+
if (entry.path === 'src/features/api/<feature>/application/<action>.command.ts') return { ...entry, template: 'api/application.command.principal.ts.tpl' };
|
|
59
|
+
if (entry.path === 'src/features/api/<feature>/application/<action>.handler.ts') return { ...entry, template: 'api/application.handler.principal.ts.tpl' };
|
|
60
|
+
return entry;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/** The templates the lite api tree names beyond the pattern topic's own files tree (the principal-aware command and handler): the template set stays one set with the topics' (tests/packages-hfs/hfs-add.spec.mjs). */
|
|
65
64
|
export const liteApiTemplates = () => selectLiteApiEntries(readTree('api')).map(entry => entry.template).filter(Boolean);
|
|
66
65
|
|
|
67
66
|
/** Fills the `<name>` variables of a path; returns null when a variable has no value. */
|
|
@@ -147,8 +146,11 @@ export function addKind({ repoRoot, noun, name, options = {}, now = Date.now })
|
|
|
147
146
|
const topics = new Set([...specs.map(([, eachSpec]) => eachSpec.topic), ...topicsOfPatterns(patterns)]);
|
|
148
147
|
const liteCli = noun === 'cli' && repo.edition === 'lite';
|
|
149
148
|
const liteApi = noun === 'api' && repo.edition === 'lite';
|
|
149
|
+
// Lite has every pattern (owner ruling 2026-10-12), but a topic with a liteAdapter is generated only from full templates.
|
|
150
|
+
refuseFullAdapterInLite({ edition: repo.edition, topics: [...topics].map(readTopic), command: `add ${noun}` });
|
|
150
151
|
const trees = new Map([...topics].map((topic) => {
|
|
151
|
-
|
|
152
|
+
// Lite keeps every colocated unit spec of a tree (owner ruling 2026-10-12); only the webhook door and its spec are lite's own.
|
|
153
|
+
const entries = repo.edition === 'lite' ? selectLiteWebhookEntries(readTree(topic)) : readTree(topic);
|
|
152
154
|
return [topic, liteApi && topic === spec.topic ? selectLiteApiEntries(entries) : entries];
|
|
153
155
|
}));
|
|
154
156
|
// The slots the noun's trees name decide whether the edition has the noun (a slot lite does not carry or forbids
|
|
@@ -196,34 +198,81 @@ export function addKind({ repoRoot, noun, name, options = {}, now = Date.now })
|
|
|
196
198
|
registerLiteDomainService({ root: repoRoot, moduleSpecifier: values.serviceModule, service: values.service });
|
|
197
199
|
}
|
|
198
200
|
if (spec.wire) wire({ beRoot, wire: spec.wire, forms, noun, name });
|
|
199
|
-
|
|
201
|
+
const platformCreated = repo.edition === 'lite' && (noun === 'api' || noun === 'webhook') ? wireLiteFeature({ repoRoot, repo, noun, name }) : [];
|
|
200
202
|
const inboxCreated = addLiteWebhookInbox({ root: repoRoot, repo, noun, provider: name, values, now });
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
+
const graphqlDoor = noun === 'api' ? ensureGraphqlPeers(repoRoot) : [];
|
|
204
|
+
const registered = register({ declarationFile, spec: { patterns, trigger: spec.trigger, optionalSlots: noun === 'api' ? [GRAPHQL_CONTRACT_SLOT] : [] } });
|
|
205
|
+
return { created: [...bootstrapCreated, ...planned.map((file) => `be/${file.target}`), ...platformCreated, ...inboxCreated, ...graphqlDoor], registered };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The slot of the emitted GraphQL snapshot (contracts/<app>/schema.graphql), which an app serving a GraphQL api feature opts into. */
|
|
209
|
+
const GRAPHQL_CONTRACT_SLOT = 'be.contract.graphql';
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Declares, in the app root package.json, the runtime peers its GraphQL api needs (R111, knowledge/hfs/peer-integrations.yaml) at
|
|
213
|
+
* their canon pins; returns ['package.json'] when it changed, else []. Both editions.
|
|
214
|
+
*/
|
|
215
|
+
function ensureGraphqlPeers(repoRoot) {
|
|
216
|
+
const file = path.join(repoRoot, 'package.json');
|
|
217
|
+
if (!fs.existsSync(file)) throw new ScaffoldError('HFS_ADD_NOT_AN_APP', 'add api needs the app root package.json');
|
|
218
|
+
const pkg = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
219
|
+
const peers = peerDependencies(pkg, bundledPins());
|
|
220
|
+
if (!Object.keys(peers).length) return [];
|
|
221
|
+
pkg.dependencies = Object.fromEntries(Object.entries({ ...pkg.dependencies, ...peers }).sort(([a], [b]) => a.localeCompare(b)));
|
|
222
|
+
fs.writeFileSync(file, packageJsonText(pkg));
|
|
223
|
+
return ['package.json'];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Adds `importLine` and one `symbol` entry of the `imports` array of a module file's lines, once. */
|
|
227
|
+
function addModuleImport(lines, { importLine, entry, relative, noun }) {
|
|
228
|
+
if (lines.includes(importLine)) return;
|
|
229
|
+
const lastImport = lines.reduce((last, line, index) => line.startsWith('import ') ? index : last, -1);
|
|
230
|
+
if (lastImport < 0 || lines.findIndex(line => /^\s*imports: \[$/.test(line)) < 0) throw new ScaffoldError('HFS_ADD_WIRE_INVALID', `add ${noun} could not find the imports array in ${relative}`);
|
|
231
|
+
lines.splice(lastImport + 1, 0, importLine);
|
|
232
|
+
const imports = lines.findIndex(line => /^\s*imports: \[$/.test(line));
|
|
233
|
+
const indent = /^(\s*)/.exec(lines[imports])?.[1] ?? '';
|
|
234
|
+
lines.splice(imports + 1, 0, `${indent} ${entry},`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The GraphQL server capability (be/src/modules/platform/graphql), written whole by the first GraphQL feature of an app that has none. */
|
|
238
|
+
function ensureGraphqlPlatform(repoRoot) {
|
|
239
|
+
if (fs.existsSync(path.join(repoRoot, 'be', 'src', 'modules', 'platform', 'graphql', 'index.ts'))) return [];
|
|
240
|
+
const written = [];
|
|
241
|
+
for (const { relative, body } of graphqlPlatformFiles()) {
|
|
242
|
+
const file = path.join(repoRoot, ...relative.split('/'));
|
|
243
|
+
if (fs.existsSync(file)) continue;
|
|
244
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
245
|
+
fs.writeFileSync(file, body);
|
|
246
|
+
written.push(relative);
|
|
247
|
+
}
|
|
248
|
+
registerLiteExports({ root: repoRoot, generator: 'graphql' });
|
|
249
|
+
return written;
|
|
203
250
|
}
|
|
204
251
|
|
|
205
|
-
/**
|
|
252
|
+
/**
|
|
253
|
+
* Lite has one API app; compose each generated transport into that app immediately: an api feature's GraphQL module (with the
|
|
254
|
+
* GraphQL server on first use), a webhook's HTTP door.
|
|
255
|
+
*/
|
|
206
256
|
function wireLiteFeature({ repoRoot, repo, noun, name }) {
|
|
207
257
|
const app = repo.sides.be.apps.find(entry => entry.kind === 'api');
|
|
208
258
|
if (!app) throw new ScaffoldError('HFS_ADD_NO_API_OWNER', `add ${noun} needs a declared lite api app`);
|
|
209
259
|
const relative = `be/apps/${app.name}/src/app.module.ts`;
|
|
210
260
|
const file = path.join(repoRoot, ...relative.split('/'));
|
|
211
261
|
if (!fs.existsSync(file)) throw new ScaffoldError('HFS_ADD_WIRE_MISSING', `add ${noun} registers its transport in ${relative}, which does not exist`);
|
|
212
|
-
const symbol = `${pascalOf(name)}HttpModule`;
|
|
213
|
-
const owner = noun === 'api' ? 'api' : 'webhooks';
|
|
214
|
-
const importLine = `import { ${symbol} } from "@features/${owner}/${name}"`;
|
|
215
262
|
const lines = fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n').split('\n');
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
263
|
+
const created = [];
|
|
264
|
+
if (noun === 'api') {
|
|
265
|
+
created.push(...ensureGraphqlPlatform(repoRoot));
|
|
266
|
+
addModuleImport(lines, { importLine: 'import { GraphqlModule } from "@modules/platform/graphql"', entry: 'GraphqlModule.register({ isGlobal: true })', relative, noun });
|
|
267
|
+
const symbol = `${pascalOf(name)}GraphqlModule`;
|
|
268
|
+
addModuleImport(lines, { importLine: `import { ${symbol} } from "@features/api/${name}"`, entry: symbol, relative, noun });
|
|
269
|
+
} else {
|
|
270
|
+
const symbol = `${pascalOf(name)}HttpModule`;
|
|
271
|
+
addModuleImport(lines, { importLine: `import { ${symbol} } from "@features/webhooks/${name}"`, entry: symbol, relative, noun });
|
|
224
272
|
}
|
|
225
273
|
fs.writeFileSync(file, `${lines.join('\n').replace(/\n+$/, '')}\n`);
|
|
226
274
|
if (noun === 'webhook') wireLiteWebhookConfig({ repoRoot, app: app.name, provider: name });
|
|
275
|
+
return created;
|
|
227
276
|
}
|
|
228
277
|
|
|
229
278
|
/** Adds one generated webhook provider to the lite API's parsed options and enables exact raw-body verification. */
|
|
@@ -287,7 +336,7 @@ function topicsOfPatterns(patterns) {
|
|
|
287
336
|
return topics;
|
|
288
337
|
}
|
|
289
338
|
|
|
290
|
-
/** Adds the patterns
|
|
339
|
+
/** Adds the patterns, the trigger kind and the opt-in slots of the noun to sides.be of hfs.json, keeping its other content and key order. */
|
|
291
340
|
function register({ declarationFile, spec }) {
|
|
292
341
|
const declaration = JSON.parse(fs.readFileSync(declarationFile, 'utf8'));
|
|
293
342
|
const be = declaration.sides.be;
|
|
@@ -295,6 +344,7 @@ function register({ declarationFile, spec }) {
|
|
|
295
344
|
const kinds = [...new Set([...(be.kinds ?? []), ...(spec.trigger ? [spec.trigger] : [])])].sort(byCodeUnit);
|
|
296
345
|
be.patterns = patterns;
|
|
297
346
|
if (kinds.length) be.kinds = kinds;
|
|
347
|
+
if (spec.optionalSlots.some(slot => !(be.optionalSlots ?? []).includes(slot))) be.optionalSlots = [...new Set([...(be.optionalSlots ?? []), ...spec.optionalSlots])].sort(byCodeUnit);
|
|
298
348
|
fs.writeFileSync(declarationFile, jsonText(declaration));
|
|
299
349
|
return { patterns, kinds };
|
|
300
350
|
}
|