@starci/hfs 4.9.11 → 4.10.1
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 -9
- package/db/backup-receipt.mjs +10 -47
- package/db/drift.mjs +1 -1
- package/db/dump.mjs +11 -10
- package/db/entity-list.mjs +1 -1
- package/db/migrate.mjs +83 -259
- package/db/new.mjs +46 -0
- package/db/post-cutover.mjs +13 -17
- package/db/postgres.mjs +80 -0
- package/emit/contracts.mjs +5 -9
- package/package.json +1 -1
- package/release/gate.mjs +3 -15
- package/runtime/knowledge/hfs/canon-pins.yaml +44 -24
- package/runtime/knowledge/hfs/design-truths.yaml +1 -1
- package/runtime/knowledge/hfs/fe-stack.yaml +4 -11
- package/runtime/knowledge/hfs/rules.yaml +179 -157
- package/runtime/knowledge/hfs/slots.yaml +185 -172
- package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
- package/runtime/knowledge/patterns/be/idempotency.yaml +3 -3
- package/runtime/knowledge/patterns/be/jobs.yaml +4 -4
- package/runtime/knowledge/patterns/be/projections.yaml +7 -5
- package/runtime/knowledge/patterns/be/queues.yaml +8 -8
- package/runtime/knowledge/patterns/be/reactors.yaml +2 -2
- package/runtime/knowledge/patterns/be/realtime.yaml +6 -6
- package/runtime/knowledge/patterns/be/saga.yaml +3 -3
- package/runtime/knowledge/patterns/be/webhooks.yaml +1 -1
- package/runtime/modules/kernel/command-policy.yaml +1 -1
- package/runtime/modules/kernel/failure-codes.yaml +65 -115
- package/runtime/modules/schemas/application-stacks.schema.yaml +5 -5
- package/runtime/scripts/hfs/architecture/canon-clones.mjs +1 -1
- package/runtime/scripts/hfs/architecture/entity-registry.mjs +53 -0
- package/runtime/scripts/hfs/architecture/{supabase-be.mjs → identity.mjs} +45 -15
- package/runtime/scripts/hfs/architecture/index.mjs +2 -3
- package/runtime/scripts/hfs/architecture/{supabase-ast.mjs → jwt-ast.mjs} +2 -103
- package/runtime/scripts/hfs/architecture/owners.mjs +11 -0
- package/runtime/scripts/hfs/architecture/register-once.mjs +1 -1
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +29 -4
- package/runtime/scripts/hfs/architecture/server-action.mjs +3 -4
- package/runtime/scripts/hfs/architecture/side-boundary.mjs +1 -1
- package/runtime/scripts/hfs/architecture/{supabase-keys.mjs → sql-keys.mjs} +1 -1
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +25 -27
- package/runtime/scripts/hfs/architecture/tiers.mjs +14 -3
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +7 -3
- package/runtime/scripts/hfs/check.mjs +6 -6
- package/runtime/scripts/hfs/coverage-scope.mjs +1 -1
- package/runtime/scripts/hfs/db-baseline-verb.mjs +48 -30
- package/runtime/scripts/hfs/db-plan-verb.mjs +17 -106
- package/runtime/scripts/hfs/db-restamp-verb.mjs +11 -10
- package/runtime/scripts/hfs/declaration-shape.mjs +15 -18
- package/runtime/scripts/hfs/manifest-shape.mjs +9 -5
- package/runtime/scripts/hfs/rule-params-shape.mjs +4 -1
- package/runtime/scripts/hfs/rules/a15-findings.mjs +1 -1
- package/runtime/scripts/hfs/rules/cli.mjs +1 -1
- package/runtime/scripts/hfs/rules/database-access.mjs +8 -74
- package/runtime/scripts/hfs/rules/database-baseline.mjs +3 -3
- package/runtime/scripts/hfs/rules/database-config.mjs +6 -214
- package/runtime/scripts/hfs/rules/database-constants.mjs +8 -10
- package/runtime/scripts/hfs/rules/database-migrations.mjs +3 -3
- package/runtime/scripts/hfs/rules/database-queues.mjs +4 -5
- package/runtime/scripts/hfs/rules/database-sql.mjs +1 -1
- package/runtime/scripts/hfs/rules/database.mjs +36 -39
- package/runtime/scripts/hfs/rules/edition.mjs +1 -1
- package/runtime/scripts/hfs/rules/{supabase-secrets.mjs → lite-secrets.mjs} +6 -6
- package/runtime/scripts/hfs/rules/script-hygiene.mjs +1 -10
- package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +5 -4
- package/runtime/scripts/hfs/rules/stacks-release.mjs +3 -2
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +2 -2
- package/runtime/scripts/hfs/rules/stacks.mjs +1 -1
- package/runtime/scripts/hfs/slot-app-view.mjs +1 -1
- package/runtime/scripts/hfs/slots.mjs +8 -2
- package/runtime/scripts/hfs/sql/pg-parse.mjs +2 -2
- package/runtime/scripts/lib/secret-patterns.mjs +2 -2
- package/runtime/scripts/lib/swarm-stack.mjs +7 -4
- package/scaffold/add-app.mjs +2 -10
- package/scaffold/add-cli-lite.mjs +6 -6
- package/scaffold/add-compose.mjs +1 -1
- package/scaffold/add-inbox-lite.mjs +4 -8
- package/scaffold/add-plan.mjs +1 -1
- package/scaffold/add-queue-lite.mjs +16 -24
- package/scaffold/add-table.mjs +34 -26
- package/scaffold/add.mjs +2 -2
- package/scaffold/app.mjs +19 -86
- package/scaffold/lite-exports.mjs +1 -1
- package/src/main.mjs +19 -20
- package/stack/target-binding.mjs +0 -60
- package/stack/urgent-floor.mjs +2 -2
- package/sync/index.mjs +32 -8
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +1 -17
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +6 -8
- package/templates/app/codegen-lite/scripts/codegen.mjs +2 -39
- package/templates/app/db-bootstrap/db/bootstrap.sql +139 -0
- package/templates/app/gitignore-lite +0 -3
- package/templates/app/package-scripts/db.json +9 -0
- package/templates/app/package-scripts/package.json +0 -1
- package/templates/app/package-scripts-lite/package.json +1 -1
- package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
- package/templates/app/skeleton/.starcistacks/production/README.md +1 -1
- package/templates/app/skeleton/README.md +1 -1
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +84 -19
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +5 -5
- package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +1 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +1 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +69 -6
- package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -7
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +6 -6
- package/templates/app/skeleton-lite/.starcistacks/production/environment.json +1 -1
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +2 -2
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +69 -6
- package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +17 -9
- package/templates/app/skeleton-lite/README.md +11 -11
- package/templates/app/skeleton-lite/{supabase → db}/migrations/__timestamp___baseline.sql +21 -1
- package/templates/app/tool-config-lite/prettierignore +0 -1
- package/templates/be/inbox/database-client.spec.ts.tpl +9 -5
- package/templates/be/inbox/database-client.ts.tpl +13 -4
- package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +47 -0
- package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +3 -1
- package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +2 -2
- package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +6 -1
- package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +39 -7
- package/templates/be/inbox/platform/migration.sql.tpl +22 -0
- package/templates/be/inbox/service.spec.ts.tpl +2 -2
- package/templates/be/inbox/service.ts.tpl +1 -2
- package/templates/be/patterns/jobs/platform-lite/job-claim.service.spec.ts.tpl +156 -0
- package/templates/be/patterns/jobs/platform-lite/job-claim.service.ts.tpl +114 -0
- package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +51 -0
- package/templates/be/patterns/queues/platform-lite/queue-outbox.service.spec.ts.tpl +62 -0
- package/templates/be/patterns/queues/platform-lite/queue-outbox.service.ts.tpl +20 -0
- package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +35 -0
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +4 -1
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +16 -5
- package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +8 -8
- package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +3 -3
- package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +2 -2
- package/templates/be/skeleton-lite/apps/api/Dockerfile +0 -1
- package/templates/be/skeleton-lite/apps/api/src/api.env.ts +9 -5
- package/templates/be/skeleton-lite/apps/api/src/api.options.ts +7 -4
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +4 -4
- package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +3 -3
- package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +9 -13
- package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +4 -2
- package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +2 -2
- package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +3 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +4 -4
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +3 -3
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +6 -6
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +3 -3
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/index.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.spec.ts +22 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.ts +25 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.config.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.decorators.ts +9 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.options.ts +35 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/errors/passport.error.ts +22 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.ts +23 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.ts +29 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.decorators.ts +22 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.options.ts +63 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.ts +11 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.spec.ts +32 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.ts +69 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +37 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +36 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/oauth-state.store.ts +59 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.functions.ts +24 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +2 -1
- package/templates/be/skeleton-lite/src/modules/platform/database/database.options.ts +4 -2
- package/templates/be/skeleton-lite/src/modules/platform/database/database.sql.ts +19 -6
- package/templates/be/skeleton-lite/src/modules/platform/database/entities.ts +7 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/migration-runner.service.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/database/primary.config.ts +2 -2
- package/templates/be/skeleton-lite/src/modules/platform/database/seed-runner.service.ts +2 -2
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +18 -5
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +1 -1
- package/templates/be/table/entities.index.ts.tpl +4 -0
- package/templates/be/table/entity.ts.tpl +21 -0
- package/templates/be/table/index.ts.tpl +1 -0
- package/templates/be/table/module.ts.tpl +1 -1
- package/templates/be/table/service.api.ts.tpl +10 -11
- package/templates/be/table/service.ts.tpl +5 -6
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/auth-door.ts +30 -0
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +46 -14
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/token-store.ts +31 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/config/index.ts +2 -5
- package/db/local-migrate.mjs +0 -194
- package/db/reset.mjs +0 -125
- package/emit/db-types.mjs +0 -271
- package/runtime/scripts/api/supabase/database-dump.mjs +0 -5
- package/runtime/scripts/api/supabase/database-push.mjs +0 -5
- package/runtime/scripts/api/supabase/database-query.mjs +0 -5
- package/runtime/scripts/api/supabase/lib.mjs +0 -55
- package/runtime/scripts/api/supabase/migration-repair.mjs +0 -5
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +0 -50
- package/runtime/scripts/hfs/architecture/supabase.mjs +0 -226
- package/scaffold/supabase-ports.mjs +0 -26
- package/templates/app/package-scripts/supabase.json +0 -7
- package/templates/app/skeleton-lite/supabase/config.toml +0 -48
- package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +0 -5
- package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +0 -33
- package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +0 -33
- package/templates/be/inbox/sql.ts.tpl +0 -16
- package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +0 -33
- package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +0 -12
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -16
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/index.ts +0 -6
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +0 -27
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.ts +0 -24
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.config.ts +0 -8
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.decorators.ts +0 -17
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.jwks.ts +0 -32
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.options.ts +0 -24
- package/templates/be/table/rows.ts.tpl +0 -4
- package/templates/be/table/sql.api.ts.tpl +0 -7
- package/templates/be/table/sql.ts.tpl +0 -4
- package/templates/fe/auth/supabase-client.ts.tpl +0 -38
- package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +0 -11
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +0 -18
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +0 -19
- /package/templates/app/skeleton-lite/{supabase → db}/seed.sql +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// `starci app add queue|job` in a lite app (ruling:R212/case-10): the queue is full's BullMQ adapter on the declared Redis, so lite
|
|
2
|
-
// generates full's queue and jobs trees with three differences. The outbox and job tables are a
|
|
2
|
+
// generates full's queue and jobs trees with three differences. The outbox and job tables are a db migration instead of the
|
|
3
3
|
// TypeORM entity, connection and migration; the SQL names the public schema; and the queue options read the Redis password of
|
|
4
4
|
// its sealed secret. The verb also declares redis in hfs.json sides.be.backing with its bullmq client, and renders the one
|
|
5
5
|
// declared Redis into every docker-swarm environment of the stack: the stateful component, its backup and restore record, the
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { parseYaml } from '../runtime/engine/yaml.mjs';
|
|
10
|
-
import { dbTypesPath, emitDbTypes } from '../emit/db-types.mjs';
|
|
11
10
|
import { configuredRecipients } from '../runtime/scripts/hfs/secret-init.mjs';
|
|
12
11
|
import { materializeDeclaredPlaceholders } from '../runtime/scripts/hfs/sealed-members.mjs';
|
|
13
12
|
import { REDIS_IMAGE, TEMPLATES_DIR, redisDevScripts, render } from '../sync/index.mjs';
|
|
@@ -35,13 +34,18 @@ const LITE_TEMPLATES = Object.freeze({
|
|
|
35
34
|
'queues/platform/queue-relay.service.ts.tpl': 'queues/platform-lite/queue-relay.service.ts.tpl',
|
|
36
35
|
'queues/platform/queue-relay.service.spec.ts.tpl': 'queues/platform-lite/queue-relay.service.spec.ts.tpl',
|
|
37
36
|
'queues/platform/queue.module.ts.tpl': 'queues/platform-lite/queue.module.ts.tpl',
|
|
38
|
-
'queues/platform/
|
|
37
|
+
'queues/platform/queue-outbox.service.ts.tpl': 'queues/platform-lite/queue-outbox.service.ts.tpl',
|
|
38
|
+
'queues/platform/queue-outbox.service.spec.ts.tpl': 'queues/platform-lite/queue-outbox.service.spec.ts.tpl',
|
|
39
39
|
'jobs/platform/jobs.index.ts.tpl': 'jobs/platform-lite/jobs.index.ts.tpl',
|
|
40
|
-
'jobs/platform/
|
|
40
|
+
'jobs/platform/job-claim.service.ts.tpl': 'jobs/platform-lite/job-claim.service.ts.tpl',
|
|
41
|
+
'jobs/platform/job-claim.service.spec.ts.tpl': 'jobs/platform-lite/job-claim.service.spec.ts.tpl',
|
|
41
42
|
});
|
|
42
43
|
|
|
43
|
-
/**
|
|
44
|
-
|
|
44
|
+
/**
|
|
45
|
+
* The persistence files lite leaves out: the TypeORM schema (its authority is db/migrations) and the SQL text and row files (the
|
|
46
|
+
* statements are database functions of the lite migration, called through callDbFunction; R334 and the be.persistence lite overlay).
|
|
47
|
+
*/
|
|
48
|
+
const TYPEORM_ONLY = /\/persistence\/(?:connection\.ts$|entities\/|migrations\/|[^/]+\.(?:sql|rows)\.ts$)/;
|
|
45
49
|
|
|
46
50
|
/** The e2e proof slot lite forbids (R212); a lite pattern proves itself with optional integration specs (the topic's liteAdapter proof). */
|
|
47
51
|
const E2E_SLOT = 'be.tests.e2e';
|
|
@@ -80,20 +84,20 @@ function skeletonFiles(repoRoot) {
|
|
|
80
84
|
return { created, patches: after === before ? [] : [{ file: errorFile, text: after }] };
|
|
81
85
|
}
|
|
82
86
|
|
|
83
|
-
/** The capability tables a platform capability the verb adds for the first time needs, as one forward-only
|
|
87
|
+
/** The capability tables a platform capability the verb adds for the first time needs, as one forward-only db migration. */
|
|
84
88
|
function migrationFiles({ repoRoot, beRoot, patterns, now }) {
|
|
85
89
|
const sections = [
|
|
86
90
|
['queue-outbox', 'queue', 'queues/platform-lite/queue-outbox.sql.tpl', 'queue'],
|
|
87
91
|
['jobs', 'fenced-job', 'jobs/platform-lite/jobs.sql.tpl', 'jobs'],
|
|
88
92
|
].filter(([, pattern, , capability]) => patterns.includes(pattern) && !fs.existsSync(path.join(beRoot, 'src', 'modules', 'platform', capability, 'index.ts')));
|
|
89
93
|
if (!sections.length) return [];
|
|
90
|
-
const folder = path.join(repoRoot, '
|
|
94
|
+
const folder = path.join(repoRoot, 'db', 'migrations');
|
|
91
95
|
const migrations = fs.existsSync(folder) ? fs.readdirSync(folder).filter((file) => MIGRATION.test(file)).sort(byCodeUnit) : [];
|
|
92
96
|
const stamp = new Date(now()).toISOString().replace(/\D/g, '').slice(0, 14);
|
|
93
97
|
const latest = migrations.at(-1)?.slice(0, 14);
|
|
94
98
|
if (latest && stamp <= latest) throw new ScaffoldError('HFS_ADD_MIGRATION_ORDER', `the current UTC timestamp ${stamp} is not later than the latest migration ${latest}; fix the clock or wait for the next second`);
|
|
95
99
|
const text = sections.map(([, , source]) => template(`be/patterns/${source}`)).join('\n');
|
|
96
|
-
return [{ relative: `
|
|
100
|
+
return [{ relative: `db/migrations/${stamp}_${sections.map(([name]) => name).join('-')}.sql`, text }];
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
/** The queue imports bullmq, which loads ioredis as an optional peer and cannot connect without it. */
|
|
@@ -320,10 +324,9 @@ function stackFiles(repoRoot, { project, consumers }) {
|
|
|
320
324
|
|
|
321
325
|
/**
|
|
322
326
|
* Plans the lite additions of `add queue|job` before any file is written; returns null outside lite or for another noun. The
|
|
323
|
-
* plan's apply() writes them and returns the app-relative paths it created or changed.
|
|
324
|
-
* database types through `emitTypes` (the local Supabase stack, as `add table`); false is the `--no-types` seam.
|
|
327
|
+
* plan's apply() writes them and returns the app-relative paths it created or changed.
|
|
325
328
|
*/
|
|
326
|
-
export function planLiteQueue({ repoRoot, repo, noun, patterns, now
|
|
329
|
+
export function planLiteQueue({ repoRoot, repo, noun, patterns, now }) {
|
|
327
330
|
if (repo.edition !== 'lite' || !['queue', 'job'].includes(noun)) return null;
|
|
328
331
|
const beRoot = path.join(repoRoot, 'be');
|
|
329
332
|
const migrations = migrationFiles({ repoRoot, beRoot, patterns, now });
|
|
@@ -345,22 +348,11 @@ export function planLiteQueue({ repoRoot, repo, noun, patterns, now, emitTypes =
|
|
|
345
348
|
const exported = registerLiteExports({ root: repoRoot, generator: 'queue' });
|
|
346
349
|
const sealed = sealRedisMembers(repoRoot, stack.environments);
|
|
347
350
|
const changed = patches.map((patch) => path.relative(repoRoot, patch.file).split(path.sep).join('/'));
|
|
348
|
-
|
|
349
|
-
return [...new Set([...created.map((entry) => entry.relative), ...changed, ...exported, ...sealed, ...types])];
|
|
351
|
+
return [...new Set([...created.map((entry) => entry.relative), ...changed, ...exported, ...sealed])];
|
|
350
352
|
},
|
|
351
353
|
};
|
|
352
354
|
}
|
|
353
355
|
|
|
354
|
-
/** The SQL of the new capabilities names public.queue and public.jobs, which the generated database types must declare. */
|
|
355
|
-
function writeTypes(repoRoot, emitTypes) {
|
|
356
|
-
const generated = emitTypes({ root: repoRoot });
|
|
357
|
-
if (typeof generated !== 'string') return [];
|
|
358
|
-
const target = path.join(repoRoot, ...dbTypesPath.split('/'));
|
|
359
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
360
|
-
fs.writeFileSync(target, generated);
|
|
361
|
-
return [dbTypesPath];
|
|
362
|
-
}
|
|
363
|
-
|
|
364
356
|
/** Seals a placeholder redis-password member to the app's configured recipient; without one, `starci app secret init` seals it. */
|
|
365
357
|
function sealRedisMembers(repoRoot, environments) {
|
|
366
358
|
if (!environments.length || !fs.existsSync(path.join(repoRoot, '.sops.yaml'))) return [];
|
package/scaffold/add-table.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { emitDbTypes, dbTypesPath } from "../emit/db-types.mjs";
|
|
4
3
|
import {
|
|
5
4
|
loadSlotManifest,
|
|
6
5
|
readRepoDeclaration,
|
|
@@ -288,7 +287,7 @@ const apiFeatureFiles = (values) => {
|
|
|
288
287
|
/**
|
|
289
288
|
* The fe data code of a table in one product app: one GraphQL operation file per operation (modules/api/graphql/{queries,
|
|
290
289
|
* mutations}, its request/data types from the generated wire types) and one SWR hook per operation (hooks/swr/api/graphql).
|
|
291
|
-
* Never a Server Action and never a
|
|
290
|
+
* Never a Server Action and never a database reader: the front end reaches data only through the back end.
|
|
292
291
|
*/
|
|
293
292
|
const feOperationFiles = (app, values) => {
|
|
294
293
|
const api = `fe/apps/${app}/src/modules/api/graphql`;
|
|
@@ -326,9 +325,8 @@ const withExport = (text, line) => {
|
|
|
326
325
|
};
|
|
327
326
|
|
|
328
327
|
/**
|
|
329
|
-
* Adds one
|
|
328
|
+
* Adds one db table migration and its domain capability; with `fe`, the table's api feature over GraphQL (the GraphQL
|
|
330
329
|
* server capability on first use) and, in every product Next app, its operation files and SWR hooks.
|
|
331
|
-
* `emitTypes` returns the generated database.types.ts text; false is the explicit `--no-types` seam.
|
|
332
330
|
*/
|
|
333
331
|
function tableContext({ root, name }) {
|
|
334
332
|
const requestedName = String(name);
|
|
@@ -341,11 +339,11 @@ function tableContext({ root, name }) {
|
|
|
341
339
|
const table = snakeOf(requestedName);
|
|
342
340
|
const manifest = loadSlotManifest();
|
|
343
341
|
const repo = readRepoDeclaration(manifest, root);
|
|
344
|
-
const connection = repo.sides?.be?.connections?.find((candidate) => candidate.provider === "
|
|
342
|
+
const connection = repo.sides?.be?.connections?.find((candidate) => candidate.provider === "postgres");
|
|
345
343
|
if (!connection) {
|
|
346
344
|
throw new ScaffoldError(
|
|
347
345
|
"HFS_ADD_TABLE_PROVIDER",
|
|
348
|
-
"add table needs a back-end connection whose provider is
|
|
346
|
+
"add table needs a back-end connection whose provider is postgres",
|
|
349
347
|
);
|
|
350
348
|
}
|
|
351
349
|
|
|
@@ -353,7 +351,7 @@ function tableContext({ root, name }) {
|
|
|
353
351
|
}
|
|
354
352
|
|
|
355
353
|
function nextMigration({ root, feature, requestedName, now }) {
|
|
356
|
-
const migrationsDir = path.join(root, "
|
|
354
|
+
const migrationsDir = path.join(root, "db", "migrations");
|
|
357
355
|
const migrations = fs.existsSync(migrationsDir)
|
|
358
356
|
? fs
|
|
359
357
|
.readdirSync(migrationsDir)
|
|
@@ -408,7 +406,7 @@ function tableFiles({ root, repo, feature, stamp, values, fe, apps }) {
|
|
|
408
406
|
const domain = `be/src/modules/domain/${feature}`;
|
|
409
407
|
const planned = [
|
|
410
408
|
{
|
|
411
|
-
relative: `
|
|
409
|
+
relative: `db/migrations/${stamp}_${feature}.sql`,
|
|
412
410
|
body: template("be/table/migration.sql.tpl", values),
|
|
413
411
|
},
|
|
414
412
|
{ relative: `${domain}/index.ts`, body: template("be/table/index.ts.tpl", values) },
|
|
@@ -417,16 +415,9 @@ function tableFiles({ root, repo, feature, stamp, values, fe, apps }) {
|
|
|
417
415
|
{ relative: `${domain}/${feature}.module-definition.ts`, body: template("be/table/module-definition.ts.tpl", values) },
|
|
418
416
|
{ relative: `${domain}/${feature}.options.ts`, body: template("be/table/options.ts.tpl", values) },
|
|
419
417
|
{ relative: `${domain}/${feature}.service.ts`, body: template(`be/table/service${api}.ts.tpl`, values) },
|
|
420
|
-
{ relative: `${domain}/persistence/${feature}.
|
|
421
|
-
{ relative: `${domain}/persistence
|
|
418
|
+
{ relative: `${domain}/persistence/entities/${feature}.entity.ts`, body: template("be/table/entity.ts.tpl", values) },
|
|
419
|
+
{ relative: `${domain}/persistence/entities/index.ts`, body: template("be/table/entities.index.ts.tpl", values) },
|
|
422
420
|
];
|
|
423
|
-
const databaseSql = "be/src/modules/platform/database/database.sql.ts";
|
|
424
|
-
if (repo.edition === "lite" && !fs.existsSync(at(root, databaseSql))) {
|
|
425
|
-
planned.splice(1, 0, {
|
|
426
|
-
relative: databaseSql,
|
|
427
|
-
body: render(fs.readFileSync(path.join(TEMPLATES_DIR, "be", "skeleton-lite", "src", "modules", "platform", "database", "database.sql.ts"), "utf8"), {}),
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
421
|
if (fe) {
|
|
431
422
|
planned.push(...apiFeatureFiles(values));
|
|
432
423
|
if (!fs.existsSync(at(root, `${GRAPHQL_PLATFORM}/index.ts`))) planned.push(...graphqlPlatformFiles());
|
|
@@ -450,7 +441,7 @@ function tableOwner({ root, repo, connection, name, requestedName, planned }) {
|
|
|
450
441
|
);
|
|
451
442
|
const owner = repo.sides.be.apps.find((app) => app.name === connection.owner && app.kind === "api");
|
|
452
443
|
if (!owner)
|
|
453
|
-
throw new ScaffoldError("HFS_ADD_NO_API_OWNER", `add table needs the
|
|
444
|
+
throw new ScaffoldError("HFS_ADD_NO_API_OWNER", `add table needs the database connection owner ${connection.owner} to be a declared api app`);
|
|
454
445
|
const ownerRelative = `be/apps/${owner.name}/src/app.module.ts`;
|
|
455
446
|
if (!fs.existsSync(at(root, ownerRelative)))
|
|
456
447
|
throw new ScaffoldError("HFS_ADD_WIRE_MISSING", `add table registers ${name} in ${ownerRelative}, which does not exist`);
|
|
@@ -471,17 +462,13 @@ function tableEditor(root, touched) {
|
|
|
471
462
|
return edit;
|
|
472
463
|
}
|
|
473
464
|
|
|
474
|
-
function writeTableFiles({ root, planned,
|
|
465
|
+
function writeTableFiles({ root, planned, edit }, created) {
|
|
475
466
|
for (const file of planned) {
|
|
476
467
|
const target = at(root, file.relative);
|
|
477
468
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
478
469
|
fs.writeFileSync(target, file.body);
|
|
479
470
|
created.push(target);
|
|
480
471
|
}
|
|
481
|
-
if (typeof emitTypes === "function") {
|
|
482
|
-
const generated = emitTypes({ root });
|
|
483
|
-
if (typeof generated === "string") edit(dbTypesPath, () => generated);
|
|
484
|
-
}
|
|
485
472
|
|
|
486
473
|
}
|
|
487
474
|
|
|
@@ -504,8 +491,30 @@ function wireTableFrontends({ root, feature, values, apps, edit }) {
|
|
|
504
491
|
}
|
|
505
492
|
}
|
|
506
493
|
|
|
494
|
+
/** The lite entity list: platform/database/entities.ts names the capability's `<c>Entities` in PRIMARY_ENTITIES (BE-PERSISTENCE-3). */
|
|
495
|
+
const PRIMARY_ENTITIES_FILE = "be/src/modules/platform/database/entities.ts";
|
|
496
|
+
const PRIMARY_ENTITIES = /(export const PRIMARY_ENTITIES[^=]*=\s*\[)([\s\S]*?)(\])/;
|
|
497
|
+
|
|
498
|
+
/** `text` of entities.ts with `<c>Entities` imported from the capability barrel and spread into PRIMARY_ENTITIES, once. */
|
|
499
|
+
function withPrimaryEntities(text, { feature, nameCamel }) {
|
|
500
|
+
const list = `${nameCamel}Entities`;
|
|
501
|
+
if (!PRIMARY_ENTITIES.test(text))
|
|
502
|
+
throw new ScaffoldError("HFS_ADD_WIRE_MISSING", `add table names ${list} in PRIMARY_ENTITIES of ${PRIMARY_ENTITIES_FILE}, which declares no PRIMARY_ENTITIES array`);
|
|
503
|
+
if (new RegExp(`\\.\\.\\.${list}\\b`).test(text)) return text;
|
|
504
|
+
const imported = `import { ${list} } from "@modules/domain/${feature}"\n`;
|
|
505
|
+
const withList = text.replace(PRIMARY_ENTITIES, (all, head, items, tail) => {
|
|
506
|
+
const entries = items.split(",").map((item) => item.trim()).filter(Boolean);
|
|
507
|
+
return `${head}${[...entries, `...${list}`].join(", ")}${tail}`;
|
|
508
|
+
});
|
|
509
|
+
const lines = withList.split("\n");
|
|
510
|
+
const lastImport = lines.reduce((at, line, index) => (line.startsWith("import ") ? index : at), -1);
|
|
511
|
+
if (lastImport >= 0) lines.splice(lastImport + 1, 0, imported.trimEnd());
|
|
512
|
+
return lastImport >= 0 ? lines.join("\n") : `${imported}${withList}`;
|
|
513
|
+
}
|
|
514
|
+
|
|
507
515
|
function wireTable({ root, repo, feature, values, ownerRelative, composition, fe, apps, edit }) {
|
|
508
516
|
const kindsHome = kindsHomeOf(root, ownerRelative);
|
|
517
|
+
if (repo.edition === "lite") edit(PRIMARY_ENTITIES_FILE, (text) => withPrimaryEntities(text, { feature, nameCamel: values.nameCamel }));
|
|
509
518
|
for (const patch of composition) edit(patch.relative, () => patch.text);
|
|
510
519
|
edit(kindsHome, (text) => wireKinds(text, kindsHome, { kinds: `${snakeOf(feature).toUpperCase()}_ERROR_KINDS`, specifier: `@modules/domain/${feature}` }));
|
|
511
520
|
tableRegistrations({ root, repo, fe, edit });
|
|
@@ -538,7 +547,7 @@ function applyTable(context) {
|
|
|
538
547
|
}
|
|
539
548
|
|
|
540
549
|
/** Adds one table through typed domain/API groups and restores every touched byte on failure. */
|
|
541
|
-
export function addTable({ root, name, fe = false,
|
|
550
|
+
export function addTable({ root, name, fe = false, now = Date.now }) {
|
|
542
551
|
const context = tableContext({ root, name });
|
|
543
552
|
const stamp = nextMigration({ ...context, now });
|
|
544
553
|
const values = tableValues(context);
|
|
@@ -546,9 +555,8 @@ export function addTable({ root, name, fe = false, emitTypes = emitDbTypes, now
|
|
|
546
555
|
const planned = tableFiles({ ...context, stamp, values, apps, fe });
|
|
547
556
|
const owner = tableOwner({ ...context, planned });
|
|
548
557
|
const composition = tableComposition({ ...context, ...owner, fe });
|
|
549
|
-
applyTable({ ...context, ...owner, planned, composition, apps, values, fe
|
|
558
|
+
applyTable({ ...context, ...owner, planned, composition, apps, values, fe });
|
|
550
559
|
return {
|
|
551
560
|
created: [...planned.map((file) => file.relative), ...composition.filter((patch) => patch.exists === false).map((patch) => patch.relative)],
|
|
552
|
-
types: typeof emitTypes === "function" ? dbTypesPath : "skipped",
|
|
553
561
|
};
|
|
554
562
|
}
|
package/scaffold/add.mjs
CHANGED
|
@@ -52,7 +52,7 @@ const FE_OWNERS = Object.freeze({
|
|
|
52
52
|
*/
|
|
53
53
|
export function addKind({ repoRoot, noun, name, options = {}, now = Date.now }) {
|
|
54
54
|
if (noun === 'table') {
|
|
55
|
-
const result = addTable({ root: repoRoot, name, fe: options.fe === true,
|
|
55
|
+
const result = addTable({ root: repoRoot, name, fe: options.fe === true, now });
|
|
56
56
|
return { ...result, registered: { patterns: [], kinds: [] } };
|
|
57
57
|
}
|
|
58
58
|
if (noun === 'app') {
|
|
@@ -81,7 +81,7 @@ function registerFeatureExports({ repoRoot, repo, noun, values }) {
|
|
|
81
81
|
|
|
82
82
|
function writeKind({ repoRoot, noun, name, now, options, manifest, repo, spec, declarationFile, values, forms, beRoot, patterns, planned }) {
|
|
83
83
|
if (noun === 'api' || noun === 'webhook') preflightFeatureComposition({ repoRoot, repo, noun, name });
|
|
84
|
-
const liteQueue = planLiteQueue({ repoRoot, repo, noun, patterns, now
|
|
84
|
+
const liteQueue = planLiteQueue({ repoRoot, repo, noun, patterns, now });
|
|
85
85
|
const bootstrapCreated = noun === 'cli' && repo.edition === 'lite' ? ensureLiteCli({ root: repoRoot, manifest, repo }) : [];
|
|
86
86
|
for (const file of planned) {
|
|
87
87
|
fs.mkdirSync(path.dirname(file.absolute), { recursive: true });
|
package/scaffold/app.mjs
CHANGED
|
@@ -18,10 +18,8 @@ import { spawnSync } from 'node:child_process';
|
|
|
18
18
|
import { loadSlotManifest, resolveRepoDeclaration } from '../runtime/scripts/hfs/slots.mjs';
|
|
19
19
|
import { parseYaml } from '../runtime/engine/yaml.mjs';
|
|
20
20
|
import { HARNESS_PACKAGE, canonPins } from '../runtime/scripts/hfs/canon-pins.mjs';
|
|
21
|
-
import { dbTypesPath, generateDbTypes } from '../emit/db-types.mjs';
|
|
22
21
|
import { TEMPLATES_DIR, appSource, imageFiles, render, renderTargets, writeTargets } from '../sync/index.mjs';
|
|
23
22
|
import { ScaffoldError } from './service.mjs';
|
|
24
|
-
import { SUPABASE_PORT_VARIABLES, supabasePortVars } from './supabase-ports.mjs';
|
|
25
23
|
import { testWorldFiles } from './test-world.mjs';
|
|
26
24
|
import { FE_APP_SCRIPTS, PACKAGE_MANAGER, WORKSPACES, WORKSPACE_LINT, feAppPackageName } from '../runtime/scripts/hfs/rules/monorepo.mjs';
|
|
27
25
|
import { isLinkLike } from '../runtime/scripts/api/fs/is-link-like.mjs';
|
|
@@ -128,23 +126,14 @@ const STARTER_SIDES = Object.freeze({
|
|
|
128
126
|
fe: Object.freeze({ grammar: 'core', apps: [{ name: 'landing', kind: 'next' }, { name: 'app', kind: 'next' }], reads: ['be/contracts/'], optionalSlots: ['fe.package.ui', 'fe.package.i18n'] }),
|
|
129
127
|
});
|
|
130
128
|
|
|
131
|
-
/** The lite starter is the upgrade-safe subset: one API, one web app and one
|
|
129
|
+
/** The lite starter is the upgrade-safe subset: one API, one web app and one SQL schema authority (db/migrations). */
|
|
132
130
|
const LITE_STARTER_SIDES = Object.freeze({
|
|
133
131
|
be: Object.freeze({
|
|
134
132
|
apps: [{ name: 'api', kind: 'api' }],
|
|
135
133
|
kinds: ['api'],
|
|
136
|
-
connections: [{ name: 'primary', envPrefix: 'PRIMARY_DB', owner: 'api', isolation: 'schema', provider: '
|
|
137
|
-
reads: ['supabase/types/'],
|
|
134
|
+
connections: [{ name: 'primary', envPrefix: 'PRIMARY_DB', owner: 'api', isolation: 'schema', provider: 'postgres' }],
|
|
138
135
|
}),
|
|
139
|
-
fe: Object.freeze({ grammar: 'core', apps: [{ name: 'web', kind: 'next' }], reads: ['be/contracts/'
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
/** The checked local Supabase auth posture of every newly scaffolded lite app. */
|
|
143
|
-
const LITE_STARTER_SUPABASE = Object.freeze({
|
|
144
|
-
enableSignup: false,
|
|
145
|
-
jwtExpiry: 3600,
|
|
146
|
-
siteUrl: 'http://127.0.0.1:3000',
|
|
147
|
-
redirectUrls: ['http://127.0.0.1:3000/'],
|
|
136
|
+
fe: Object.freeze({ grammar: 'core', apps: [{ name: 'web', kind: 'next' }], reads: ['be/contracts/'] }),
|
|
148
137
|
});
|
|
149
138
|
|
|
150
139
|
/**
|
|
@@ -175,16 +164,17 @@ const STARTER_DEPENDENCIES = Object.freeze({
|
|
|
175
164
|
const LITE_STARTER_DEPENDENCIES = Object.freeze({
|
|
176
165
|
dependencies: {
|
|
177
166
|
'@apollo/server': null, '@nestjs/apollo': null, '@nestjs/common': null, '@nestjs/core': null, '@nestjs/cqrs': null, '@nestjs/graphql': null,
|
|
178
|
-
'@nestjs/platform-express': null, '@nestjs/typeorm': null, '@
|
|
167
|
+
'@nestjs/platform-express': null, '@nestjs/typeorm': null, '@nestjs/passport': null, passport: null, 'passport-google-oauth20': null, 'passport-local': null,
|
|
168
|
+
'@aws-sdk/client-s3': null, '@aws-sdk/s3-request-presigner': null, bcryptjs: null,
|
|
179
169
|
'class-transformer': null, 'class-validator': null, graphql: null, jose: null, pg: null,
|
|
180
170
|
'reflect-metadata': null, rxjs: null, tslib: null, typeorm: null, zod: null,
|
|
181
171
|
},
|
|
182
172
|
devDependencies: {
|
|
183
173
|
turbo: null, '@nestjs/testing': null, '@starci/eslint-canon-be': null, '@starci/eslint-canon-fe': null,
|
|
184
174
|
'@starci/jest-preset': null, '@starci/prettier-config': null, '@starci/stylelint-canon': null, '@starci/tsconfig': null, 'starci-harness': null, '@tailwindcss/postcss': null,
|
|
185
|
-
'@types/express': null, '@types/jest': null, '@types/node': null, '@types/react': null, '@types/react-dom': null,
|
|
175
|
+
'@types/express': null, '@types/jest': null, '@types/node': null, '@types/passport-google-oauth20': null, '@types/passport-local': null, '@types/react': null, '@types/react-dom': null,
|
|
186
176
|
eslint: null, 'eslint-plugin-react-hooks': null, husky: null, jest: null, 'postcss-value-parser': null, prettier: null,
|
|
187
|
-
stylelint: null,
|
|
177
|
+
stylelint: null, tailwindcss: null, 'ts-jest': null, 'ts-node-dev': null, 'tsc-alias': null,
|
|
188
178
|
'tsconfig-paths': null, typescript: null,
|
|
189
179
|
},
|
|
190
180
|
});
|
|
@@ -201,12 +191,12 @@ const FE_APP_DEPENDENCIES = Object.freeze({
|
|
|
201
191
|
});
|
|
202
192
|
|
|
203
193
|
/**
|
|
204
|
-
* The lite fe workspace: the same base without the shared packages, plus
|
|
205
|
-
*
|
|
194
|
+
* The lite fe workspace: the same base without the shared packages, plus server-only for its own next-intl request config. It
|
|
195
|
+
* holds no identity SDK: modules/auth talks only to the app's own back-end auth doors.
|
|
206
196
|
*/
|
|
207
197
|
const LITE_FE_APP_DEPENDENCIES = Object.freeze({
|
|
208
198
|
'@heroicons/react': null, '@heroui/react': null, '@heroui/styles': null, '@starci/grammar': null, '@starci/heroicons': null, '@xyflow/react': null, 'react-aria-components': null,
|
|
209
|
-
|
|
199
|
+
next: null, 'next-intl': null, react: null, 'react-dom': null, 'server-only': null,
|
|
210
200
|
});
|
|
211
201
|
|
|
212
202
|
/**
|
|
@@ -229,51 +219,9 @@ export const starterDeclaration = (name, manifest = loadSlotManifest(), edition
|
|
|
229
219
|
kind: 'app',
|
|
230
220
|
...(edition === 'full' ? {} : { edition }),
|
|
231
221
|
project: name,
|
|
232
|
-
...(edition === 'lite' ? { supabase: structuredClone(LITE_STARTER_SUPABASE) } : {}),
|
|
233
222
|
sides: structuredClone(edition === 'lite' ? LITE_STARTER_SIDES : STARTER_SIDES),
|
|
234
223
|
});
|
|
235
224
|
|
|
236
|
-
/** Opting into a server PKCE callback also selects Supabase Auth and declares its route slot. */
|
|
237
|
-
function enableAuthCallback(declaration) {
|
|
238
|
-
requireAuthCallbackConnection(declaration);
|
|
239
|
-
declaration.sides.fe.optionalSlots = [...(declaration.sides.fe.optionalSlots ?? []), 'fe.route.callback'];
|
|
240
|
-
declaration.supabase ??= structuredClone(LITE_STARTER_SUPABASE);
|
|
241
|
-
declaration.supabase.redirectUrls = ['http://127.0.0.1:3000/auth/callback'];
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/** Callback-specific source files are excluded from the browser-only starter. */
|
|
245
|
-
const CALLBACK_SOURCE_FILES = Object.freeze(new Set([
|
|
246
|
-
'src/app/auth/callback/route.ts', 'src/modules/auth/server.ts', 'src/modules/auth/pkce-client.ts',
|
|
247
|
-
]));
|
|
248
|
-
export const hasAuthCallback = repo => (repo.sides.fe.optionalSlots ?? []).includes('fe.route.callback');
|
|
249
|
-
|
|
250
|
-
function requireAuthCallbackConnection(repo) {
|
|
251
|
-
if (!(repo.sides.be?.connections ?? []).some(entry => entry.provider === 'supabase'))
|
|
252
|
-
throw new ScaffoldError('HFS_ADD_CONNECTION_REQUIRED', 'the auth callback requires an existing Supabase connection; the full starter has none');
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/** Apply the callback's cookie-backed auth owner and its config readers to product apps only. */
|
|
256
|
-
export function callbackSourceFiles(files, repo) {
|
|
257
|
-
const enabled = hasAuthCallback(repo);
|
|
258
|
-
if (enabled) requireAuthCallbackConnection(repo);
|
|
259
|
-
const selected = files.filter(file => {
|
|
260
|
-
const [, , app, ...parts] = file.path.split('/');
|
|
261
|
-
const relative = parts.join('/');
|
|
262
|
-
if (relative === 'src/modules/auth/pkce-client.ts') return false;
|
|
263
|
-
return MARKETING_FE_APPS.has(app) || enabled || !CALLBACK_SOURCE_FILES.has(relative);
|
|
264
|
-
});
|
|
265
|
-
if (!enabled) return selected;
|
|
266
|
-
const config = fs.readFileSync(path.join(TEMPLATES_DIR, 'fe/skeleton-lite/apps/web/src/modules/config/index.ts'), 'utf8');
|
|
267
|
-
const client = render(fs.readFileSync(path.join(TEMPLATES_DIR, 'fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts'), 'utf8').replace(/\r\n/g, '\n'), PARTIAL_VARIABLES);
|
|
268
|
-
return selected.map(file => {
|
|
269
|
-
const [, , app, ...parts] = file.path.split('/');
|
|
270
|
-
if (MARKETING_FE_APPS.has(app)) return file;
|
|
271
|
-
const relative = parts.join('/');
|
|
272
|
-
if (relative === 'src/modules/config/index.ts') return { ...file, content: config };
|
|
273
|
-
return relative === 'src/modules/auth/client.ts' ? { ...file, content: client } : file;
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
|
|
277
225
|
/** A dependency section at the canon pins (a null range takes the pin, which must exist; every pinned name is null). */
|
|
278
226
|
function pinnedSection(entries, pins) {
|
|
279
227
|
const pinned = (dependency, range) => {
|
|
@@ -329,10 +277,9 @@ const workspaceSection = (entries, project, pins) => pinnedSection(Object.fromEn
|
|
|
329
277
|
* The package.json of the fe app workspace `app` of the app `project`: @<project>/<app>, private, the workspace scripts, its own
|
|
330
278
|
* dependencies. A product app (`core`, it carries the shared app core) also takes every core runtime package of the fe stack.
|
|
331
279
|
*/
|
|
332
|
-
function feAppManifest(project, app, pins, edition, { core = true,
|
|
280
|
+
function feAppManifest(project, app, pins, edition, { core = true, stack = coreFeStackPackages() } = {}) {
|
|
333
281
|
const base = edition === 'lite' ? LITE_FE_APP_DEPENDENCIES : FE_APP_DEPENDENCIES;
|
|
334
282
|
const dependencies = { ...base, ...(core ? Object.fromEntries(stack.app.filter(name => !Object.hasOwn(base, name)).map(name => [name, null])) : {}) };
|
|
335
|
-
if (core && authCallback) Object.assign(dependencies, { '@supabase/ssr': null, '@supabase/supabase-js': null, 'server-only': null });
|
|
336
283
|
return { name: feAppPackageName(project, app), version: '0.0.0', private: true, scripts: { ...FE_APP_SCRIPTS }, dependencies: workspaceSection(dependencies, project, pins) };
|
|
337
284
|
}
|
|
338
285
|
|
|
@@ -381,8 +328,8 @@ function nextAppTsconfig() {
|
|
|
381
328
|
};
|
|
382
329
|
}
|
|
383
330
|
|
|
384
|
-
/** The names a skeleton file may fill: {{project}}, {{app}}, {{appPascal}} (the side's app), {{sonarGate}}
|
|
385
|
-
const SKELETON_VARIABLES = Object.freeze(['project', 'app', 'appPascal', 'sonarGate', 'registryVisibility'
|
|
331
|
+
/** The names a skeleton file may fill: {{project}}, {{app}}, {{appPascal}} (the side's app), {{sonarGate}} and {{registryVisibility}} (the image visibility written once into deploy.registry). */
|
|
332
|
+
const SKELETON_VARIABLES = Object.freeze(['project', 'app', 'appPascal', 'sonarGate', 'registryVisibility']);
|
|
386
333
|
|
|
387
334
|
/**
|
|
388
335
|
* A skeleton file with its variables filled. Only the skeleton names are variables: every other `{{name}}` is source the app keeps
|
|
@@ -411,7 +358,7 @@ const LITE_DEFERRED_GENERATOR_FILES = new Set([
|
|
|
411
358
|
'src/modules/platform/database/seed-runner.service.ts',
|
|
412
359
|
'src/modules/platform/primitives/sequence.policy.ts',
|
|
413
360
|
]);
|
|
414
|
-
/** The TypeORM schema files of a platform capability: lite's schema authority is
|
|
361
|
+
/** The TypeORM schema files of a platform capability: lite's schema authority is db/migrations (be.persistence lite overlay). */
|
|
415
362
|
const LITE_TYPEORM_SCHEMA = /^src\/modules\/platform\/[^/]+\/persistence\/(?:connection\.ts$|entities\/|migrations\/)/;
|
|
416
363
|
const liteBaseSource = (rel) => !rel.startsWith('apps/cli/')
|
|
417
364
|
&& !rel.startsWith('src/features/cli/')
|
|
@@ -509,7 +456,7 @@ export function skeletonOf(scope, app, vars) {
|
|
|
509
456
|
for (const entry of scope === 'fe' ? app.sides.fe.apps : []) {
|
|
510
457
|
if (!unique.some(file => file.path.startsWith(`fe/apps/${entry.name}/`))) throw new ScaffoldError('HFS_SCAFFOLD_SKELETON_MISSING', `no skeleton templates for the fe app ${entry.name}`);
|
|
511
458
|
}
|
|
512
|
-
return
|
|
459
|
+
return unique;
|
|
513
460
|
}
|
|
514
461
|
|
|
515
462
|
/** The npm step that resolves the lockfile of a new app, exactly as the error names it. */
|
|
@@ -622,17 +569,6 @@ function scaffoldWrites(root, original) {
|
|
|
622
569
|
};
|
|
623
570
|
}
|
|
624
571
|
|
|
625
|
-
/** Generate the lite database types and translate generation errors into the scaffold failure contract. */
|
|
626
|
-
function emitScaffoldDatabaseTypes(root, name, writes, emitTypes) {
|
|
627
|
-
try {
|
|
628
|
-
const text = emitTypes({ root });
|
|
629
|
-
if (typeof text !== 'string' || !text.trim()) throw new Error('the Supabase CLI returned no database types');
|
|
630
|
-
writes.write(dbTypesPath, text);
|
|
631
|
-
} catch (error) {
|
|
632
|
-
throw new ScaffoldError('HFS_SCAFFOLD_TYPES_FAILED', `Supabase database types could not be generated for ${root} (${String(error?.message ?? error)}). Start the local Supabase stack and run starci app scaffold ${name} --edition lite again`);
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
572
|
/**
|
|
637
573
|
* `starci app scaffold <name>`: writes the new app under `into`, resolves its lockfile with npm (`lock`, npmLock), and returns
|
|
638
574
|
* `{ root, files }` (app-relative paths, sorted). A new path is first made an empty repository with unborn main (`initGit`), so the
|
|
@@ -660,17 +596,15 @@ function bornRepository(root, initGit) {
|
|
|
660
596
|
return fs.lstatSync(root);
|
|
661
597
|
}
|
|
662
598
|
|
|
663
|
-
export function scaffoldApp({ name, into, presets, harness, edition = 'full',
|
|
599
|
+
export function scaffoldApp({ name, into, presets, harness, edition = 'full', registryVisibility = 'public', manifest = loadSlotManifest(), pins = canonPins({ harness }), lock = npmLock, initGit = gitInit, now = () => new Date() }) {
|
|
664
600
|
if (!NAME.test(String(name))) throw new ScaffoldError('HFS_SCAFFOLD_NAME_INVALID', `the app name ${name} must be kebab-case (a project name: ${NAME})`);
|
|
665
601
|
const root = path.resolve(into, name);
|
|
666
602
|
const existing = fs.lstatSync(root, { throwIfNoEntry: false });
|
|
667
603
|
if (existing && !emptyUnbornGit(root, existing)) throw new ScaffoldError('HFS_SCAFFOLD_EXISTS', `${root} already exists; only a plain directory containing solely .git with unborn main and an empty index can be scaffolded`);
|
|
668
604
|
if (!['public', 'private'].includes(registryVisibility)) throw new ScaffoldError('HFS_SCAFFOLD_REGISTRY_VISIBILITY', `--registry-visibility accepts public or private (got ${registryVisibility})`);
|
|
669
|
-
if (typeof authCallback !== 'boolean') throw new ScaffoldError('HFS_SCAFFOLD_NAME_INVALID', 'authCallback must be a boolean');
|
|
670
605
|
if (!pins[HARNESS_PACKAGE]) throw new ScaffoldError('HFS_SCAFFOLD_PIN_MISSING', `${HARNESS_PACKAGE} has no pin: an app pins the CLI that scaffolds it, so run the scaffold through it (starci app scaffold)`);
|
|
671
606
|
const original = existing ?? bornRepository(root, initGit);
|
|
672
607
|
const declaration = starterDeclaration(name, manifest, edition);
|
|
673
|
-
if (authCallback) enableAuthCallback(declaration);
|
|
674
608
|
const app = resolveRepoDeclaration(manifest, declaration);
|
|
675
609
|
const pkg = packageManifest(name, pins, app.edition, app.sides.fe.grammar);
|
|
676
610
|
const timestamp = now().toISOString().replace(/\D/g, '').slice(0, 14);
|
|
@@ -683,14 +617,14 @@ export function scaffoldApp({ name, into, presets, harness, edition = 'full', au
|
|
|
683
617
|
path: `fe/apps/${entry.name}/src/modules/types/ambient/styles.d.ts`,
|
|
684
618
|
content: '/** Declares stylesheets loaded by the Next bundler; non-CSS side-effect imports remain checked. */\ndeclare module "*.css" {}\n',
|
|
685
619
|
})),
|
|
686
|
-
...app.sides.fe.apps.map(entry => ({ path: `fe/apps/${entry.name}/package.json`, content: packageJsonText(feAppManifest(name, entry.name, pins, app.edition, { core: coreFeApps([entry]).length === 1
|
|
620
|
+
...app.sides.fe.apps.map(entry => ({ path: `fe/apps/${entry.name}/package.json`, content: packageJsonText(feAppManifest(name, entry.name, pins, app.edition, { core: coreFeApps([entry]).length === 1 })) })),
|
|
687
621
|
...imageFiles(app),
|
|
688
622
|
...(app.edition === 'lite' ? [] : Object.keys(FE_PACKAGES).flatMap(pkg => [
|
|
689
623
|
{ path: `fe/packages/${name}-${pkg}/package.json`, content: packageJsonText(fePackageManifest(name, pkg, pins)) },
|
|
690
624
|
{ path: `fe/packages/${name}-${pkg}/tsconfig.json`, content: jsonText(fePackageTsconfig()) },
|
|
691
625
|
{ path: `fe/packages/${name}-${pkg}/tsconfig.build.json`, content: jsonText(fePackageBuildTsconfig()) },
|
|
692
626
|
])),
|
|
693
|
-
...['app', 'be', 'fe'].flatMap(scope => skeletonOf(scope, app, { sonarGate: sonarGateName(parseYaml(fs.readFileSync(SONAR_GATE_FILE, 'utf8')), app.edition), timestamp, registryVisibility
|
|
627
|
+
...['app', 'be', 'fe'].flatMap(scope => skeletonOf(scope, app, { sonarGate: sonarGateName(parseYaml(fs.readFileSync(SONAR_GATE_FILE, 'utf8')), app.edition), timestamp, registryVisibility })),
|
|
694
628
|
// A full app has a test world (integration, e2e and contract are rows of its release gate); lite has none (ruling:R212/case-3, hosting amended by owner:lite-production-swarm).
|
|
695
629
|
...(app.edition === 'lite' ? [] : testWorldFiles(app)),
|
|
696
630
|
];
|
|
@@ -700,12 +634,11 @@ export function scaffoldApp({ name, into, presets, harness, edition = 'full', au
|
|
|
700
634
|
const targets = renderTargets(declaration, presets, { manifest, source: appSource(root) });
|
|
701
635
|
for (const target of targets) writes.claim(target.path);
|
|
702
636
|
writeTargets(root, targets);
|
|
703
|
-
if (app.edition === 'lite') emitScaffoldDatabaseTypes(root, name, writes, emitTypes);
|
|
704
637
|
const secretFiles = initializeScaffoldSecrets(root, writes);
|
|
705
638
|
let locked;
|
|
706
639
|
try { locked = lock(root); } finally { writes.lockfile(); }
|
|
707
640
|
if (!locked.ok) throw new ScaffoldError('HFS_SCAFFOLD_LOCK_FAILED', `\`${LOCK_STEP}\` could not resolve the lockfile of ${root} (${locked.detail}). Check the network and the npm registry, then run starci app scaffold ${name} again`);
|
|
708
|
-
return { root, files: [...new Set([...files.map(file => file.path), ...targets.map(target => target.path), ...secretFiles,
|
|
641
|
+
return { root, files: [...new Set([...files.map(file => file.path), ...targets.map(target => target.path), ...secretFiles, 'package-lock.json'])].sort(byCodeUnit) };
|
|
709
642
|
} catch (error) {
|
|
710
643
|
const held = writes.rollback();
|
|
711
644
|
if (held.length) error.message += `; cleanup retained replaced or occupied entries: ${held.join(', ')}`;
|