@ultimat3/cli 22.15.0 → 24.0.0
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/CLAUDE.md +36 -13
- package/README.md +31 -8
- package/package.json +29 -28
- package/src/admin-catalog-keys.ts +16 -0
- package/src/admin-registration.ts +192 -0
- package/src/affected.ts +27 -35
- package/src/api-registration.ts +125 -11
- package/src/app-artifacts.ts +1 -1
- package/src/app-auth.ts +4 -27
- package/src/app-boundaries.ts +6 -20
- package/src/app-config-load.ts +51 -0
- package/src/app-env.ts +5 -11
- package/src/app-load.ts +55 -11
- package/src/app-manifest.ts +16 -8
- package/src/app-mcp.ts +10 -23
- package/src/app-permissions-borrowed.ts +129 -0
- package/src/app-permissions-site.ts +24 -0
- package/src/app-permissions.ts +0 -0
- package/src/app-reload-graph.ts +7 -30
- package/src/app-transport.ts +219 -0
- package/src/boundary-findings.ts +16 -4
- package/src/browser-transport.ts +160 -0
- package/src/budgets.ts +27 -34
- package/src/cdp-shot-driver.ts +3 -1
- package/src/cmd-auth-spec.ts +16 -0
- package/src/cmd-auth.ts +65 -0
- package/src/cmd-build-spec.ts +10 -3
- package/src/cmd-build.ts +107 -11
- package/src/cmd-db-backfill.ts +25 -12
- package/src/cmd-db.ts +66 -16
- package/src/cmd-deploy-helm.ts +7 -6
- package/src/cmd-deploy.ts +4 -4
- package/src/cmd-dev.ts +9 -8
- package/src/cmd-doctor-spec.ts +2 -9
- package/src/cmd-doctor.ts +75 -24
- package/src/cmd-env.ts +26 -2
- package/src/cmd-generate.ts +195 -47
- package/src/cmd-help.ts +19 -6
- package/src/cmd-i18n.ts +10 -9
- package/src/cmd-jobs-spec.ts +19 -3
- package/src/cmd-jobs.ts +136 -7
- package/src/cmd-manifest-spec.ts +1 -1
- package/src/cmd-manifest.ts +6 -25
- package/src/cmd-mcp.ts +46 -15
- package/src/cmd-new.ts +15 -1
- package/src/cmd-planned.ts +15 -8
- package/src/cmd-routes.ts +27 -3
- package/src/cmd-secrets-ring.ts +76 -0
- package/src/cmd-secrets-spec.ts +10 -2
- package/src/cmd-secrets.ts +95 -18
- package/src/cmd-shot-matrix.ts +1 -3
- package/src/cmd-shot.ts +12 -3
- package/src/cmd-tasks.ts +84 -15
- package/src/cmd-test-spec.ts +1 -7
- package/src/cmd-test.ts +13 -29
- package/src/cmd-verify.ts +13 -5
- package/src/comment-safe.ts +16 -0
- package/src/coverage-floor.ts +334 -0
- package/src/coverage-lcov.ts +193 -0
- package/src/coverage-source.ts +136 -0
- package/src/db-object-drift.ts +30 -0
- package/src/db-replay-engine.ts +115 -0
- package/src/db-schema-dump.ts +150 -0
- package/src/db-schema-refresh.ts +122 -0
- package/src/db-subscribes.ts +12 -4
- package/src/dev-dashboard-guard.ts +76 -0
- package/src/dev-dashboard.ts +51 -29
- package/src/dev-lock.ts +4 -7
- package/src/dev-route-table.ts +9 -0
- package/src/dispatch.ts +3 -6
- package/src/doctor-app-url.ts +39 -0
- package/src/doctor-auth.ts +95 -0
- package/src/doctor-coverage.ts +39 -0
- package/src/doctor-guards.ts +54 -0
- package/src/doctor-sealed.ts +132 -0
- package/src/document-graph.ts +82 -0
- package/src/drift-replica-identity.ts +42 -0
- package/src/error-catalog.ts +6 -9
- package/src/error-codes.ts +55 -3
- package/src/error-unthrown.ts +29 -11
- package/src/errors.ts +54 -83
- package/src/framework-schema-apply.ts +78 -0
- package/src/framework-schema-stamp.ts +96 -0
- package/src/framework-schema.ts +28 -2
- package/src/generate-files.ts +79 -12
- package/src/generate-grant-findings.ts +76 -0
- package/src/generate-grants.ts +77 -3
- package/src/generate-kinds.ts +52 -1
- package/src/generate-shadow.ts +46 -0
- package/src/generate-write.ts +62 -40
- package/src/generated-imports.ts +59 -0
- package/src/gh-target.ts +12 -7
- package/src/guard-sources.ts +104 -0
- package/src/guards.ts +23 -7
- package/src/handle-registration.ts +217 -0
- package/src/i18n-audit.ts +4 -1
- package/src/i18n-registration.ts +36 -0
- package/src/image-prepare.ts +51 -8
- package/src/import-closure.ts +165 -0
- package/src/index.ts +12 -5
- package/src/island-bundle.ts +3 -15
- package/src/island-store.ts +24 -16
- package/src/jobs-drain.ts +95 -21
- package/src/jobs-json.ts +50 -0
- package/src/jobs-report.ts +7 -4
- package/src/json-merge.ts +46 -0
- package/src/loopback-host.ts +13 -0
- package/src/manifest-staleness.ts +62 -0
- package/src/mcp-errors.ts +41 -4
- package/src/mcp-host.ts +16 -9
- package/src/mcp-test-run.ts +38 -0
- package/src/messages.ts +35 -0
- package/src/metrics-endpoint.ts +95 -24
- package/src/migration-extensions.ts +29 -0
- package/src/module-imports.ts +47 -0
- package/src/output.ts +24 -9
- package/src/page-navigation.ts +7 -23
- package/src/page-speculation.ts +5 -20
- package/src/permission-grants.ts +41 -9
- package/src/prerender-out.ts +34 -7
- package/src/prerender.ts +8 -5
- package/src/publish-closure.ts +101 -0
- package/src/pwa-artifacts.ts +26 -94
- package/src/registry.ts +2 -0
- package/src/revalidated-response.ts +4 -3
- package/src/role-load.ts +143 -0
- package/src/role-replicator.ts +16 -0
- package/src/role-start.ts +27 -5
- package/src/role-sync.ts +55 -20
- package/src/role-wake.ts +45 -0
- package/src/runtime-admin.ts +141 -0
- package/src/runtime-assets.ts +11 -7
- package/src/runtime-bindings.ts +4 -4
- package/src/runtime-cache.ts +38 -47
- package/src/runtime-hooks.ts +5 -2
- package/src/runtime-idempotency-scope.ts +23 -0
- package/src/runtime-isr.ts +36 -0
- package/src/runtime-jobs.ts +8 -33
- package/src/runtime-mfa-warning.ts +29 -0
- package/src/runtime-notify-retention.ts +10 -46
- package/src/runtime-overrides.ts +1 -1
- package/src/runtime-purge.ts +38 -16
- package/src/runtime-queue.ts +39 -11
- package/src/runtime-realtime.ts +9 -45
- package/src/runtime-render.ts +68 -35
- package/src/runtime-services.ts +22 -18
- package/src/runtime-storage.ts +10 -5
- package/src/scaffold-typecheck.ts +1 -0
- package/src/schema-diff.ts +16 -0
- package/src/schema-drift.ts +45 -15
- package/src/schema-dump-drift.ts +64 -0
- package/src/seo-routes.ts +80 -9
- package/src/serve-boot.ts +114 -158
- package/src/serve-drain.ts +9 -33
- package/src/serve-env.ts +9 -6
- package/src/serve-prebuilt-paths.ts +19 -0
- package/src/serve-prebuilt.ts +84 -0
- package/src/serve-web.ts +169 -0
- package/src/serve.ts +6 -9
- package/src/server-barrels.ts +54 -0
- package/src/shot-locale.ts +5 -23
- package/src/site-assets.ts +3 -5
- package/src/site-config.ts +9 -48
- package/src/site-seo.ts +1 -1
- package/src/sitemap-lastmod.ts +5 -0
- package/src/solid-loader.ts +12 -3
- package/src/source-edit.ts +49 -0
- package/src/source-files.ts +6 -0
- package/src/stored-object-headers.ts +43 -0
- package/src/sync-url.ts +2 -2
- package/src/tasks-facts.ts +3 -26
- package/src/templates/action.ts +147 -11
- package/src/templates/admin-catalog.ts +58 -0
- package/src/templates/admin-page.ts +36 -8
- package/src/templates/admin.ts +57 -17
- package/src/templates/backfill.ts +121 -87
- package/src/templates/entity.ts +140 -44
- package/src/templates/guard-animated-layout-property.ts +4 -11
- package/src/templates/guard-bare-error.ts +9 -13
- package/src/templates/guard-focus-visible.ts +4 -11
- package/src/templates/guard-image-dimensions.ts +6 -6
- package/src/templates/guard-island-without-states.ts +2 -4
- package/src/templates/guard-raw-breakpoint.ts +210 -0
- package/src/templates/guard-raw-colour.ts +4 -11
- package/src/templates/guard-raw-length.ts +375 -0
- package/src/templates/guard-raw-motion.ts +213 -0
- package/src/templates/guard-raw-shadow.ts +187 -0
- package/src/templates/guard-raw-z-index.ts +166 -0
- package/src/templates/guard-repo-raw-sql.ts +345 -0
- package/src/templates/guard-semantic-interactive.ts +6 -6
- package/src/templates/guard-undeclared-custom-property.ts +217 -0
- package/src/templates/guard-undefined-style-class.ts +215 -0
- package/src/templates/guard-untranslated-string.ts +6 -6
- package/src/templates/guard-unzoned-date.ts +9 -13
- package/src/templates/index.ts +3 -2
- package/src/templates/island.ts +34 -51
- package/src/templates/job-body-test.ts +121 -0
- package/src/templates/job.ts +23 -8
- package/src/templates/naming.ts +28 -0
- package/src/templates/policy.ts +16 -9
- package/src/templates/query.ts +242 -26
- package/src/templates/resource-create.ts +62 -18
- package/src/templates/resource-form-island.ts +58 -80
- package/src/templates/resource-page.ts +269 -0
- package/src/templates/resource-service.ts +96 -0
- package/src/templates/resource.ts +86 -61
- package/src/templates/route.ts +41 -35
- package/src/templates/scaffold-api.ts +55 -1
- package/src/templates/scaffold-app.ts +177 -53
- package/src/templates/scaffold-auth.ts +83 -19
- package/src/templates/scaffold-browser-client.ts +90 -0
- package/src/templates/scaffold-container-compose.ts +10 -1
- package/src/templates/scaffold-container.ts +20 -2
- package/src/templates/scaffold-dashboard-bare.ts +64 -2
- package/src/templates/scaffold-dashboard-example.ts +124 -31
- package/src/templates/scaffold-dashboard-shared.ts +1 -20
- package/src/templates/scaffold-dashboard.ts +1 -2
- package/src/templates/scaffold-db-client.ts +144 -0
- package/src/templates/scaffold-db-package.ts +90 -8
- package/src/templates/scaffold-docs.ts +50 -5
- package/src/templates/scaffold-guards.ts +54 -11
- package/src/templates/scaffold-helm-fences.ts +76 -0
- package/src/templates/scaffold-helm-templates.ts +32 -7
- package/src/templates/scaffold-helm.ts +31 -1
- package/src/templates/scaffold-http.ts +9 -7
- package/src/templates/scaffold-repo.ts +61 -3
- package/src/templates/scaffold-roles.ts +56 -9
- package/src/templates/scaffold-shell.ts +74 -105
- package/src/templates/scaffold-site.ts +45 -19
- package/src/templates/scaffold-ui-package.ts +18 -0
- package/src/templates/slice-foundation.ts +46 -8
- package/src/test-counts.ts +14 -2
- package/src/test-ignores.ts +25 -0
- package/src/test-passes.ts +0 -15
- package/src/test-select.ts +5 -14
- package/src/test-shards.ts +41 -93
- package/src/test-workers.ts +26 -4
- package/src/theme-boot.ts +6 -31
- package/src/transport-calls.ts +105 -0
- package/src/trusted-hops-error.ts +17 -0
- package/src/unscanned-admin.ts +54 -0
- package/src/unthrown-codes.ts +26 -0
- package/src/verify-checks.ts +43 -53
- package/src/verify-coverage-run.ts +190 -0
- package/src/verify-deadline.ts +353 -0
- package/src/verify-floor.ts +36 -9
- package/src/verify-merge.ts +171 -45
- package/src/verify-part-step.ts +90 -0
- package/src/verify-progress.ts +25 -0
- package/src/verify-role-load-probe.ts +54 -0
- package/src/verify-role-load.ts +148 -0
- package/src/verify-run.ts +116 -37
- package/src/verify-shard.ts +33 -11
- package/src/verify-stalled.ts +152 -0
- package/src/verify-step.ts +42 -1
- package/src/verify-tests.ts +83 -8
- package/src/workspace-checks.ts +50 -8
- package/src/workspace-dep-edit.ts +127 -0
- package/src/build-errors.ts +0 -54
- package/src/cmd-dev-fixture.ts +0 -204
- package/src/dev-live-fixture.ts +0 -122
- package/src/policy-fixture.ts +0 -78
- package/src/realtime-browser-probe-fixture.ts +0 -9
- package/src/role-start-fixture.ts +0 -78
- package/src/scaffold-fixture.ts +0 -160
- package/src/templates/island-fixture.ts +0 -76
- package/src/verify-errors.ts +0 -49
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// buildpack, a Render blueprint or a fly.toml would be the primitive that never ships.
|
|
8
8
|
|
|
9
9
|
import { SECRETS_KEY_FILE } from '@ultimat3/core';
|
|
10
|
+
import { PREBUILT_COMMAND } from '../serve-prebuilt-paths';
|
|
10
11
|
import type { GeneratedFile, NameSet } from './naming';
|
|
11
12
|
import { composeProdFile, PROD_ENV_FILE } from './scaffold-container-compose';
|
|
12
13
|
import { helmFiles } from './scaffold-helm';
|
|
@@ -29,14 +30,16 @@ const dockerfile = (
|
|
|
29
30
|
# install refuses. \`bunfig.toml\` survives because its \`[install]\` section is input to the install,
|
|
30
31
|
# and so does a \`.tgz\`: a \`file:\` tarball dependency (a vendored or private package) is resolved
|
|
31
32
|
# from the file itself, and without it the frozen install cannot find what the lockfile names.
|
|
32
|
-
|
|
33
|
+
# The tag names the series for the reader; the digest is what builds — a tag is a pointer its
|
|
34
|
+
# publisher can move. Dependabot's docker ecosystem moves the digest together with the tag.
|
|
35
|
+
FROM oven/bun:1.4-alpine@sha256:d888c0ae6c86d7866ff10c5aafdd9077b36aee6455b33dd270fb93c0dd5cef6f AS manifests
|
|
33
36
|
WORKDIR /app
|
|
34
37
|
COPY . .
|
|
35
38
|
RUN find . ! -type d ! -name package.json ! -name bunfig.toml ! -name '*.tgz' -delete \\
|
|
36
39
|
&& find . -type d -empty -delete
|
|
37
40
|
|
|
38
41
|
# ---------- deps: runtime dependencies only, cached on the lockfile and the manifests ----------
|
|
39
|
-
FROM oven/bun:1.4-alpine AS deps
|
|
42
|
+
FROM oven/bun:1.4-alpine@sha256:d888c0ae6c86d7866ff10c5aafdd9077b36aee6455b33dd270fb93c0dd5cef6f AS deps
|
|
40
43
|
WORKDIR /app
|
|
41
44
|
COPY --from=manifests /app ./
|
|
42
45
|
COPY bun.lock ./
|
|
@@ -56,6 +59,16 @@ FROM deps AS runtime
|
|
|
56
59
|
WORKDIR /app
|
|
57
60
|
COPY . .
|
|
58
61
|
|
|
62
|
+
# ---------- prebuilt: every island chunk and compiled stylesheet, made once, here ----------
|
|
63
|
+
# What a web pod otherwise makes on EVERY boot — Babel over every island, Sass over every
|
|
64
|
+
# stylesheet: seconds of CPU and a compiler's worth of memory, per replica, per start. It runs in
|
|
65
|
+
# the image build because the store is valid only for the Bun and the paths that wrote it; above
|
|
66
|
+
# BUILD_ID, so two images differing only by their stamp share this layer; and above
|
|
67
|
+
# NODE_ENV=production, because it imports the app with no deployment environment and app.config.ts
|
|
68
|
+
# must ask for none — exactly as on a fresh clone. An image without this line still serves, and
|
|
69
|
+
# every boot logs X_IMAGE_NOT_PREBUILT naming it.
|
|
70
|
+
RUN ${PREBUILT_COMMAND}
|
|
71
|
+
|
|
59
72
|
# The immutable content hash this image serves. Stamped by CI (\`--build-arg BUILD_ID=$(git rev-parse HEAD)\`);
|
|
60
73
|
# without one the server computes the same hash from the manifest at boot. Never \`latest\`.
|
|
61
74
|
ARG BUILD_ID=
|
|
@@ -153,6 +166,11 @@ docker run --rm -e ROLE=migrate -e DATABASE_URL=postgres://... ${app.kebab}:dev
|
|
|
153
166
|
docker run -p 3000:3000 -e DATABASE_URL=postgres://... ${app.kebab}:dev
|
|
154
167
|
\`\`\`
|
|
155
168
|
|
|
169
|
+
The image build runs \`x build --target prebuilt\` after \`COPY . .\`: every island chunk and every
|
|
170
|
+
compiled stylesheet is made once, there, by the image's own Bun — so a pod builds nothing at boot.
|
|
171
|
+
Without that line every boot runs Babel and Sass over the whole app and logs
|
|
172
|
+
\`X_IMAGE_NOT_PREBUILT\`, naming it.
|
|
173
|
+
|
|
156
174
|
## One box, every role
|
|
157
175
|
|
|
158
176
|
\`\`\`sh
|
|
@@ -33,7 +33,7 @@ ${themeIsland}
|
|
|
33
33
|
|
|
34
34
|
${routeConfig('')}
|
|
35
35
|
|
|
36
|
-
export function
|
|
36
|
+
export function Page() {
|
|
37
37
|
const t = useT();
|
|
38
38
|
const locale = currentLocale();
|
|
39
39
|
// Read at render, not at import: the registries are filled by the boot scan, after this module.
|
|
@@ -174,9 +174,71 @@ unitTest('formatCount follows the locale', () => {
|
|
|
174
174
|
});
|
|
175
175
|
`;
|
|
176
176
|
|
|
177
|
-
|
|
177
|
+
const barePageTest = (
|
|
178
|
+
app: NameSet,
|
|
179
|
+
): string => `// The dashboard, rendered as a request renders it. Its facts are the framework's registries, so
|
|
180
|
+
// the test registers the one route it renders — the boot scan's job in a running app — and reads
|
|
181
|
+
// the tiles and the route table back off the markup.
|
|
182
|
+
${sortedImports([
|
|
183
|
+
`import { catalogs, useT } from '@${app.kebab}/i18n';`,
|
|
184
|
+
"import { frameworkVersion } from '@ultimat3/core';",
|
|
185
|
+
"import { clearRoutes, registerRoute } from '@ultimat3/render';",
|
|
186
|
+
"import { afterAll, beforeAll, expect, renderRoute, unitTest } from '@ultimat3/testing';",
|
|
187
|
+
])}
|
|
188
|
+
import { roles } from '../../shared/roles';
|
|
189
|
+
import * as page from './page';
|
|
190
|
+
|
|
191
|
+
const url = 'https://example.test/dashboard';
|
|
192
|
+
|
|
193
|
+
/** The figure a tile shows, read off the markup by the \`stat\` the page gave it. */
|
|
194
|
+
const stat = (html: string, name: string): string | undefined =>
|
|
195
|
+
html.match(new RegExp(\`data-stat="\${name}">([^<]*)<\`))?.[1];
|
|
196
|
+
|
|
197
|
+
// The route table is process-global and a test process starts with none: this file registers the
|
|
198
|
+
// page it renders, and takes it back out so no later file inherits a route it never declared.
|
|
199
|
+
beforeAll(() => {
|
|
200
|
+
registerRoute({ file: 'apps/web/app/dashboard/page.tsx', config: page.config });
|
|
201
|
+
});
|
|
202
|
+
afterAll(clearRoutes);
|
|
203
|
+
|
|
204
|
+
unitTest('the dashboard is gated, renders per request, and hydrates one island', async () => {
|
|
205
|
+
const view = await renderRoute(page, { url });
|
|
206
|
+
expect(page.config.render).toBe('ssr');
|
|
207
|
+
expect(page.config.policy?.permission).toBe('dashboard:read');
|
|
208
|
+
expect(page.config.offline).toBe('runtime');
|
|
209
|
+
expect(page.config.budget.js).toBe('60kb');
|
|
210
|
+
// What the render emitted, beside what the config declares: the theme toggle, and only it.
|
|
211
|
+
expect(view.islands.map((island) => [island.moduleId, island.strategy])).toEqual([
|
|
212
|
+
['shared-theme-toggle', 'visible'],
|
|
213
|
+
]);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
unitTest('the tiles are the framework registries, counted', async () => {
|
|
217
|
+
const view = await renderRoute(page, { url });
|
|
218
|
+
expect(stat(view.html, 'routes')).toBe('1');
|
|
219
|
+
expect(stat(view.html, 'locales')).toBe(String(catalogs.locales.length));
|
|
220
|
+
expect(stat(view.html, 'roles')).toBe(String(Object.keys(roles).length));
|
|
221
|
+
expect(stat(view.html, 'version')).toBe(frameworkVersion());
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
unitTest('the table is the route table: path, surface and render mode per route', async () => {
|
|
225
|
+
const t = useT();
|
|
226
|
+
const view = await renderRoute(page, { url });
|
|
227
|
+
expect(view.html.match(/<tr data-row=/g)).toHaveLength(1);
|
|
228
|
+
expect(view.html).toMatch(
|
|
229
|
+
/<tr data-row="\\/dashboard"><td>\\/dashboard<\\/td><td>app<\\/td><td>ssr<\\/td>/,
|
|
230
|
+
);
|
|
231
|
+
expect(view.text).toContain(t('app.dashboard.routesCaption'));
|
|
232
|
+
expect(view.meta.title).toBe(t('app.dashboard.title'));
|
|
233
|
+
// The sidebar knows where it is.
|
|
234
|
+
expect(view.html).toMatch(/<a href="\\/dashboard" aria-current="page"/);
|
|
235
|
+
});
|
|
236
|
+
`;
|
|
237
|
+
|
|
238
|
+
/** The bare dashboard's page, view module and the tests beside both. */
|
|
178
239
|
export const bareDashboardFiles = (app: NameSet): readonly GeneratedFile[] => [
|
|
179
240
|
{ path: `${DASHBOARD_DIR}/page.tsx`, contents: barePage(app) },
|
|
180
241
|
{ path: `${DASHBOARD_DIR}/dashboard-view.ts`, contents: bareView() },
|
|
181
242
|
{ path: `${DASHBOARD_DIR}/dashboard-view.test.ts`, contents: bareViewTest() },
|
|
243
|
+
{ path: `${DASHBOARD_DIR}/page.test.ts`, contents: barePageTest(app) },
|
|
182
244
|
];
|
|
@@ -34,7 +34,6 @@ ${sortedImports([
|
|
|
34
34
|
"} from '@ultimat3/ui';",
|
|
35
35
|
].join('\n'),
|
|
36
36
|
])}
|
|
37
|
-
import { DEMO_ORG_ID } from '../../shared/demo-org';
|
|
38
37
|
import { Shell } from '../../shared/shell';
|
|
39
38
|
import * as repo from '../post/repo';
|
|
40
39
|
import {
|
|
@@ -47,14 +46,6 @@ import {
|
|
|
47
46
|
} from './dashboard-view';
|
|
48
47
|
import styles from './page.module.scss';
|
|
49
48
|
|
|
50
|
-
/**
|
|
51
|
-
* Whose posts. The route's policy decides who may open this page; which org's rows it aggregates
|
|
52
|
-
* is a decision the load makes, and until this app issues sessions the only org with rows is the
|
|
53
|
-
* one \`packages/db/src/seed.ts\` writes. The day sessions exist, this becomes the actor's org and
|
|
54
|
-
* the read becomes \`postList.as(actor, …)\` — the query already declares the tenancy rule.
|
|
55
|
-
*/
|
|
56
|
-
const DEMO_ORG = DEMO_ORG_ID;
|
|
57
|
-
|
|
58
49
|
/** The stat row counts what it can see. Past this many posts, write an aggregate query. */
|
|
59
50
|
const ROW_LIMIT = 500;
|
|
60
51
|
|
|
@@ -68,29 +59,40 @@ export interface DashboardData {
|
|
|
68
59
|
readonly now: string;
|
|
69
60
|
}
|
|
70
61
|
|
|
71
|
-
|
|
62
|
+
/**
|
|
63
|
+
* What \`load\` answers, with the read as a parameter. The one branch a running app never takes is
|
|
64
|
+
* the build's: \`x build --target static\` renders every route once to measure its JS budget with
|
|
65
|
+
* no database wired, and that render gets the real empty branches — which weigh the same markup —
|
|
66
|
+
* instead of a route the budget step cannot measure. Any other failure still propagates.
|
|
67
|
+
*/
|
|
68
|
+
export async function dashboardData(
|
|
69
|
+
read: () => Promise<readonly Parameters<typeof toPostRow>[0][]>,
|
|
70
|
+
): Promise<DashboardData> {
|
|
72
71
|
const now = new Date().toISOString();
|
|
73
72
|
try {
|
|
74
|
-
|
|
75
|
-
return { rows: rows.map(toPostRow), now };
|
|
73
|
+
return { rows: (await read()).map(toPostRow), now };
|
|
76
74
|
} catch (error) {
|
|
77
|
-
// \`x build --target static\` renders every route once to measure its JS budget, with no
|
|
78
|
-
// database wired — so this is that measurement pass, not a request. Empty rows render the
|
|
79
|
-
// real empty branches, which measure the same markup. Any other failure still propagates.
|
|
80
75
|
if (isUltimateError(error) && error.code === 'X_DB_UNAVAILABLE') return { rows: [], now };
|
|
81
76
|
throw error;
|
|
82
77
|
}
|
|
83
78
|
}
|
|
84
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Whose posts: the ACTOR's org. The repo names none — the typed handle scopes every read to the
|
|
82
|
+
* request's actor — so the viewer \`auth/dev-actor.ts\` resolves sees the rows
|
|
83
|
+
* \`packages/db/src/seed.ts\` wrote, and a real session sees its own org's the day one exists.
|
|
84
|
+
*/
|
|
85
|
+
const load = (): Promise<DashboardData> => dashboardData(() => repo.list(ROW_LIMIT));
|
|
86
|
+
|
|
85
87
|
${themeIsland}
|
|
86
88
|
|
|
87
89
|
${routeConfig('\n load,')}
|
|
88
90
|
|
|
89
|
-
export interface
|
|
91
|
+
export interface PageProps {
|
|
90
92
|
readonly data: DashboardData;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
export function
|
|
95
|
+
export function Page(props: PageProps) {
|
|
94
96
|
const t = useT();
|
|
95
97
|
const locale = currentLocale();
|
|
96
98
|
const now = new Date(props.data.now);
|
|
@@ -176,20 +178,15 @@ export interface PostRow {
|
|
|
176
178
|
}
|
|
177
179
|
|
|
178
180
|
/**
|
|
179
|
-
* Duck-typed rather than the entity's own row: the page only ever reads these three columns.
|
|
180
|
-
*
|
|
181
|
-
* BOTH spellings of the timestamp, and that is a measured fact rather than caution: the repo's
|
|
182
|
-
* \`select *\` hands back column names as Postgres has them (\`created_at\`) while its row type
|
|
183
|
-
* says \`createdAt\`, so the typed property is \`undefined\` on a real row. Until the repo aliases
|
|
184
|
-
* its columns — dz-showcase's does — the page reads whichever one arrived.
|
|
181
|
+
* Duck-typed rather than the entity's own row: the page only ever reads these three columns. The
|
|
182
|
+
* repo reads through the typed handle, so \`createdAt\` arrives as the \`Date\` the entity declares.
|
|
185
183
|
*/
|
|
186
184
|
export function toPostRow(row: {
|
|
187
185
|
readonly id: string;
|
|
188
186
|
readonly title: string;
|
|
189
|
-
readonly createdAt
|
|
190
|
-
readonly created_at?: Date | string | undefined;
|
|
187
|
+
readonly createdAt: Date | string;
|
|
191
188
|
}): PostRow {
|
|
192
|
-
const at = row.createdAt
|
|
189
|
+
const at = row.createdAt;
|
|
193
190
|
return {
|
|
194
191
|
id: row.id,
|
|
195
192
|
title: row.title,
|
|
@@ -303,13 +300,10 @@ unitTest('bucketByDay is one bar per day, oldest first, quiet days included', ()
|
|
|
303
300
|
expect(bucketByDay([], NOW, 3).map((point) => point.value)).toEqual([0, 0, 0]);
|
|
304
301
|
});
|
|
305
302
|
|
|
306
|
-
unitTest('toPostRow serialises a Date
|
|
303
|
+
unitTest('toPostRow serialises a Date and passes ISO text through', () => {
|
|
307
304
|
const at = new Date('2026-01-02T03:04:05.000Z');
|
|
308
305
|
expect(toPostRow({ id: 'x', title: 't', createdAt: at }).createdAt).toBe(at.toISOString());
|
|
309
306
|
expect(toPostRow({ id: 'x', title: 't', createdAt: '2026-01-01' }).createdAt).toBe('2026-01-01');
|
|
310
|
-
// What \`select *\` really returns: the snake_case column, and no \`createdAt\` at all.
|
|
311
|
-
expect(toPostRow({ id: 'x', title: 't', created_at: at }).createdAt).toBe(at.toISOString());
|
|
312
|
-
expect(toPostRow({ id: 'x', title: 't' }).createdAt).toBe('');
|
|
313
307
|
});
|
|
314
308
|
|
|
315
309
|
unitTest('formatCount follows the locale', () => {
|
|
@@ -318,9 +312,108 @@ unitTest('formatCount follows the locale', () => {
|
|
|
318
312
|
});
|
|
319
313
|
`;
|
|
320
314
|
|
|
321
|
-
|
|
315
|
+
const examplePageTest = (
|
|
316
|
+
app: NameSet,
|
|
317
|
+
): string => `// The dashboard, rendered as a request renders it: \`load\` reads the actor's org through the
|
|
318
|
+
// slice's repo — the in-memory driver under test — and the page turns the rows into tiles, a chart
|
|
319
|
+
// and a table. Losing the policy is the other regression worth a test: the page still renders, to
|
|
320
|
+
// anyone.
|
|
321
|
+
${sortedImports([
|
|
322
|
+
`import { driver } from '@${app.kebab}/db';`,
|
|
323
|
+
`import { useT } from '@${app.kebab}/i18n';`,
|
|
324
|
+
"import { createContext, frozenClock, runWithContext } from '@ultimat3/core';",
|
|
325
|
+
"import { testActor } from '@ultimat3/policy';",
|
|
326
|
+
"import { dbUnavailable } from '@ultimat3/db';",
|
|
327
|
+
"import { afterEach, expect, renderRoute, unitTest } from '@ultimat3/testing';",
|
|
328
|
+
])}
|
|
329
|
+
import { DEMO_ORG_ID } from '../../shared/demo-org';
|
|
330
|
+
import * as repo from '../post/repo';
|
|
331
|
+
import * as page from './page';
|
|
332
|
+
|
|
333
|
+
const url = 'https://example.test/dashboard';
|
|
334
|
+
const viewer = testActor('viewer', { orgId: DEMO_ORG_ID }).actor;
|
|
335
|
+
const stranger = testActor('stranger', { orgId: '00000000-0000-4000-8000-000000000009' }).actor;
|
|
336
|
+
|
|
337
|
+
const DAY_MS = 86_400_000;
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* One post in the viewer's org, written the way the app writes one — \`daysAgo\` days before now.
|
|
341
|
+
* The request's clock is what stamps \`createdAt\`, so the test chooses the instant.
|
|
342
|
+
*/
|
|
343
|
+
const post = (title: string, daysAgo: number) => {
|
|
344
|
+
const clock = frozenClock(new Date(Date.now() - daysAgo * DAY_MS));
|
|
345
|
+
const draft = { orgId: DEMO_ORG_ID, title, price: { minor: 0, currency: 'USD' } };
|
|
346
|
+
return runWithContext(createContext({ actor: viewer, clock }), () => repo.insert(draft));
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
/** The figure a tile shows, read off the markup by the \`stat\` the page gave it. */
|
|
350
|
+
const stat = (html: string, name: string): string | undefined =>
|
|
351
|
+
html.match(new RegExp(\`data-stat="\${name}">([^<]*)<\`))?.[1];
|
|
352
|
+
|
|
353
|
+
// One store per process: without this, one test's rows are the next test's fixtures.
|
|
354
|
+
afterEach(() => {
|
|
355
|
+
driver.reset?.();
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
unitTest('the dashboard is gated, renders per request, and hydrates one island', async () => {
|
|
359
|
+
const view = await renderRoute(page, { url, actor: viewer });
|
|
360
|
+
expect(page.config.render).toBe('ssr');
|
|
361
|
+
expect(page.config.policy?.permission).toBe('dashboard:read');
|
|
362
|
+
expect(page.config.offline).toBe('runtime');
|
|
363
|
+
expect(page.config.budget.js).toBe('60kb');
|
|
364
|
+
// What the render emitted, beside what the config declares: the theme toggle, and only it.
|
|
365
|
+
expect(view.islands.map((island) => [island.moduleId, island.strategy])).toEqual([
|
|
366
|
+
['shared-theme-toggle', 'visible'],
|
|
367
|
+
]);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
unitTest('it counts and lists the posts of the org that is looking', async () => {
|
|
371
|
+
await post('Last month', 30);
|
|
372
|
+
await post('This week', 3);
|
|
373
|
+
await post('Today', 0);
|
|
374
|
+
const view = await renderRoute(page, { url, actor: viewer });
|
|
375
|
+
// Newest first, as the repo orders them — and each tile counts its own window.
|
|
376
|
+
expect(view.data.rows.map((row) => row.title)).toEqual(['Today', 'This week', 'Last month']);
|
|
377
|
+
expect(stat(view.html, 'posts-total')).toBe('3');
|
|
378
|
+
expect(stat(view.html, 'posts-week')).toBe('2');
|
|
379
|
+
expect(stat(view.html, 'posts-today')).toBe('1');
|
|
380
|
+
// One table row per post, and one bar per day of the window whether or not it has posts.
|
|
381
|
+
expect(view.html.match(/<tr data-row=/g)).toHaveLength(3);
|
|
382
|
+
expect(view.html.match(/data-bar="true"/g)).toHaveLength(14);
|
|
383
|
+
expect(view.text).toContain('This week');
|
|
384
|
+
// The sidebar knows where it is.
|
|
385
|
+
expect(view.html).toMatch(/<a href="\\/dashboard" aria-current="page"/);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
unitTest('another org sees none of them: the page says so instead of an empty table', async () => {
|
|
389
|
+
const t = useT();
|
|
390
|
+
await post('Not theirs', 0);
|
|
391
|
+
const view = await renderRoute(page, { url, actor: stranger });
|
|
392
|
+
expect(view.data.rows).toEqual([]);
|
|
393
|
+
expect(stat(view.html, 'posts-total')).toBe('0');
|
|
394
|
+
expect(view.html).not.toContain('<tr data-row=');
|
|
395
|
+
expect(view.text).toContain(t('app.dashboard.emptyTitle'));
|
|
396
|
+
expect(view.meta.title).toBe(t('app.dashboard.title'));
|
|
397
|
+
expect(view.meta.description).toBe(t('app.dashboard.description'));
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
unitTest('the build measures the empty page: no database is not a failure there', async () => {
|
|
401
|
+
const measured = await page.dashboardData(() => Promise.reject(dbUnavailable('no database')));
|
|
402
|
+
// The clock is frozen under test, so the instant the windows are cut at is this one.
|
|
403
|
+
expect(measured).toEqual({ rows: [], now: new Date().toISOString() });
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
unitTest('any other failure of the read propagates', async () => {
|
|
407
|
+
const cause = new TypeError('the read broke');
|
|
408
|
+
const read = (): Promise<never> => Promise.reject(cause);
|
|
409
|
+
expect(await page.dashboardData(read).catch((error: unknown) => error)).toBe(cause);
|
|
410
|
+
});
|
|
411
|
+
`;
|
|
412
|
+
|
|
413
|
+
/** The example dashboard's page, view module and the tests beside both. */
|
|
322
414
|
export const exampleDashboardFiles = (app: NameSet): readonly GeneratedFile[] => [
|
|
323
415
|
{ path: `${DASHBOARD_DIR}/page.tsx`, contents: examplePage(app) },
|
|
324
416
|
{ path: `${DASHBOARD_DIR}/dashboard-view.ts`, contents: exampleView() },
|
|
325
417
|
{ path: `${DASHBOARD_DIR}/dashboard-view.test.ts`, contents: exampleViewTest() },
|
|
418
|
+
{ path: `${DASHBOARD_DIR}/page.test.ts`, contents: examplePageTest(app) },
|
|
326
419
|
];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// What both dashboards share — `scaffold-dashboard.ts` picks one of `scaffold-dashboard-example.ts`
|
|
2
2
|
// and `scaffold-dashboard-bare.ts` per invocation, and everything the two must agree on lives here:
|
|
3
|
-
// the route declaration, the theme island
|
|
3
|
+
// the route declaration, the theme island and the stylesheet.
|
|
4
4
|
|
|
5
5
|
export const DASHBOARD_DIR = 'apps/web/app/dashboard';
|
|
6
6
|
|
|
@@ -56,22 +56,3 @@ export const dashboardStyle = (): string => `@use '@ultimat3/ui/tokens' as token
|
|
|
56
56
|
color: tokens.role('fg');
|
|
57
57
|
}
|
|
58
58
|
`;
|
|
59
|
-
|
|
60
|
-
export const dashboardPageTest =
|
|
61
|
-
(): string => `// The dashboard renders per request, is gated by a policy, hydrates its one island, and stays
|
|
62
|
-
// under its budget. Losing the policy is the interesting regression: the page still renders, to
|
|
63
|
-
// anyone.
|
|
64
|
-
import { expect, unitTest } from '@ultimat3/testing';
|
|
65
|
-
import { config } from './page';
|
|
66
|
-
|
|
67
|
-
unitTest('the dashboard renders on the server, is gated, and has an offline strategy', () => {
|
|
68
|
-
expect(config.render).toBe('ssr');
|
|
69
|
-
expect(config.policy?.permission).toBe('dashboard:read');
|
|
70
|
-
expect(config.offline).toBe('runtime');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
unitTest('the dashboard hydrates its one island inside a stated budget', () => {
|
|
74
|
-
expect(config.hydrate).toBe('visible');
|
|
75
|
-
expect(config.budget.js).toBe('60kb');
|
|
76
|
-
});
|
|
77
|
-
`;
|
|
@@ -15,13 +15,12 @@
|
|
|
15
15
|
import type { GeneratedFile, NameSet } from './naming';
|
|
16
16
|
import { bareDashboardFiles } from './scaffold-dashboard-bare';
|
|
17
17
|
import { exampleDashboardFiles } from './scaffold-dashboard-example';
|
|
18
|
-
import { DASHBOARD_DIR,
|
|
18
|
+
import { DASHBOARD_DIR, dashboardStyle } from './scaffold-dashboard-shared';
|
|
19
19
|
|
|
20
20
|
/** `apps/web/app/dashboard/`, in the shape the invocation earns. */
|
|
21
21
|
export function dashboardFiles(app: NameSet, example: boolean): readonly GeneratedFile[] {
|
|
22
22
|
return [
|
|
23
23
|
...(example ? exampleDashboardFiles(app) : bareDashboardFiles(app)),
|
|
24
24
|
{ path: `${DASHBOARD_DIR}/page.module.scss`, contents: dashboardStyle() },
|
|
25
|
-
{ path: `${DASHBOARD_DIR}/page.test.ts`, contents: dashboardPageTest() },
|
|
26
25
|
];
|
|
27
26
|
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// The generated app's `packages/db/src/client.ts`: the ONE typed database handle, and the entity
|
|
2
|
+
// set `x g entity` / `x g resource` add to. Its own module rather than a section of
|
|
3
|
+
// `scaffold-db-package.ts` because `handle-registration.ts` writes the same file into an app that
|
|
4
|
+
// predates it — one template, two writers.
|
|
5
|
+
|
|
6
|
+
import { sortedImports } from './imports';
|
|
7
|
+
import type { GeneratedFile, NameSet } from './naming';
|
|
8
|
+
import { names } from './naming';
|
|
9
|
+
|
|
10
|
+
/** Where the handle lives, relative to the app root. The generators' one anchor. */
|
|
11
|
+
export const HANDLE_FILE = 'packages/db/src/client.ts';
|
|
12
|
+
|
|
13
|
+
/** The scope a pure call that names no app writes — `x g` and `x new` always name theirs. */
|
|
14
|
+
export const PLACEHOLDER_DB_MODULE = '@app/db';
|
|
15
|
+
|
|
16
|
+
/** One entity in the handle's set. */
|
|
17
|
+
export interface HandleEntry {
|
|
18
|
+
/** The key on the handle — the table's name, camelCase: `db.posts`. */
|
|
19
|
+
readonly key: string;
|
|
20
|
+
/** The entity's exported binding: `post`. */
|
|
21
|
+
readonly binding: string;
|
|
22
|
+
/** Where the binding is imported from: `@myapp/web/app/post/entity`. */
|
|
23
|
+
readonly specifier: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The line an entity takes in the set. Spelled once: the template and the registrar both write it. */
|
|
27
|
+
export const handleEntryLine = (entry: HandleEntry): string => ` ${entry.key}: ${entry.binding},`;
|
|
28
|
+
|
|
29
|
+
/** The import that line needs. */
|
|
30
|
+
export const handleImportLine = (entry: HandleEntry): string =>
|
|
31
|
+
`import { ${entry.binding} } from '${entry.specifier}';`;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The web workspace's package name, from the db package's: both carry the app's scope, which is
|
|
35
|
+
* how `x new` names them and how the root tsconfig's `paths` resolves them.
|
|
36
|
+
*/
|
|
37
|
+
export const webModuleOf = (dbModule: string): string =>
|
|
38
|
+
`${dbModule.slice(0, dbModule.lastIndexOf('/'))}/web`;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The entry for the entity `x g entity <rawName>` writes into `<surfaceDir>/<feature>/entity.ts`.
|
|
42
|
+
* The key is `NameSet.plural` — the same word `repo.ts` reads the table under.
|
|
43
|
+
*/
|
|
44
|
+
export function handleEntryFor(
|
|
45
|
+
rawName: string,
|
|
46
|
+
target: { readonly surfaceDir: string; readonly feature: string; readonly dbModule: string },
|
|
47
|
+
): HandleEntry {
|
|
48
|
+
const name = names(rawName);
|
|
49
|
+
// `apps/web/app` → `app`: the web workspace's own root is what its package name stands for.
|
|
50
|
+
const inWorkspace = target.surfaceDir.split('/').slice(2).join('/');
|
|
51
|
+
return {
|
|
52
|
+
key: name.plural,
|
|
53
|
+
binding: name.camel,
|
|
54
|
+
specifier: `${webModuleOf(target.dbModule)}/${inWorkspace}/${target.feature}/entity`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const entitySet = (entries: readonly HandleEntry[]): string =>
|
|
59
|
+
entries.length === 0
|
|
60
|
+
? 'const entities = {};'
|
|
61
|
+
: `const entities = {\n${entries.map(handleEntryLine).join('\n')}\n};`;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The handle, over `entries`. The set is a named, one-entry-per-line object on purpose: it is the
|
|
65
|
+
* anchor `x g entity` inserts a line into, and an expanded object literal is a shape Biome leaves
|
|
66
|
+
* alone at any length — an inline `database({ a, b }, …)` re-wraps as it grows.
|
|
67
|
+
*/
|
|
68
|
+
export const dbClientSource = (
|
|
69
|
+
entries: readonly HandleEntry[],
|
|
70
|
+
): string => `// The one typed database handle. \`db.<table>\` exists because the entity is in the set below —
|
|
71
|
+
// nobody writes a repository class per entity, and nobody can reach a table that is not listed.
|
|
72
|
+
//
|
|
73
|
+
// \`x g entity\` and \`x g resource\` add their entity to \`entities\`. One written by hand is the
|
|
74
|
+
// same two lines: its import, and \`<tableName>: <entity>,\` in the set.
|
|
75
|
+
|
|
76
|
+
${sortedImports([
|
|
77
|
+
"import { storeMode } from '@ultimat3/core';",
|
|
78
|
+
"import { type Driver, database, memoryDriver, postgresDriver } from '@ultimat3/entity';",
|
|
79
|
+
...entries.map(handleImportLine),
|
|
80
|
+
])}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* One driver, NAMED rather than defaulted, and exported so a test resets the same object the app
|
|
84
|
+
* reads through (\`driver.reset?.()\`).
|
|
85
|
+
*
|
|
86
|
+
* \`postgresDriver()\` takes no connection: it resolves \`@ultimat3/db\`'s process client, which is
|
|
87
|
+
* the embedded PGlite under \`x dev\` and the \`DATABASE_URL\` pool in a container. \`bun test\`
|
|
88
|
+
* installs no client, so a test gets the in-memory driver — the same contract, no database.
|
|
89
|
+
* \`storeMode\` is core's one answer to that question, shared with every other seam that has a
|
|
90
|
+
* memory and a database store.
|
|
91
|
+
*/
|
|
92
|
+
export const selectDriver = (env: Readonly<Record<string, string | undefined>>): Driver =>
|
|
93
|
+
storeMode(env) === 'memory' ? memoryDriver() : postgresDriver();
|
|
94
|
+
|
|
95
|
+
export const driver = selectDriver(Bun.env);
|
|
96
|
+
|
|
97
|
+
/** Every entity the handle serves, keyed by the name its table takes on \`db\`. */
|
|
98
|
+
${entitySet(entries)}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Only a feature's \`repo.ts\`, a \`query\`'s \`sql\` or a seed may use this. A route importing it is
|
|
102
|
+
* \`X_BOUNDARY_ROUTE_TO_DB\`.
|
|
103
|
+
*/
|
|
104
|
+
export const db = database(entities, { driver });
|
|
105
|
+
|
|
106
|
+
export type Db = typeof db;
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
const clientTest = (
|
|
110
|
+
app: NameSet,
|
|
111
|
+
): string => `// Which store the handle reads: memory under \`bun test\`, the process's Postgres client
|
|
112
|
+
// everywhere else. A handle that silently took the memory driver in a container would serve an
|
|
113
|
+
// empty database with a green gate.
|
|
114
|
+
import { expect, unitTest } from '@ultimat3/testing';
|
|
115
|
+
import { db, driver, selectDriver } from './client';
|
|
116
|
+
|
|
117
|
+
unitTest('${app.kebab} reads memory under test and Postgres everywhere else', () => {
|
|
118
|
+
// Only the in-memory driver can empty itself — a Postgres driver that could truncate an app's
|
|
119
|
+
// rows from a test eventually would — so \`reset\` is what tells the two apart.
|
|
120
|
+
expect(selectDriver({ NODE_ENV: 'test' }).reset).toBeDefined();
|
|
121
|
+
expect(selectDriver({ NODE_ENV: 'production' }).reset).toBeUndefined();
|
|
122
|
+
expect(selectDriver({ ULTIMATE_ENV: 'development' }).reset).toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
unitTest('the handle is built over the driver this module exports', () => {
|
|
126
|
+
expect(driver.reset).toBeDefined();
|
|
127
|
+
expect(typeof db).toBe('object');
|
|
128
|
+
});
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* `packages/db/src/client.ts` and its test. With the example slice the set holds `post`; under
|
|
133
|
+
* `--no-example` it is empty and the first `x g entity` fills it.
|
|
134
|
+
*/
|
|
135
|
+
export const dbClientFiles = (app: NameSet, example: boolean): readonly GeneratedFile[] => {
|
|
136
|
+
const dbModule = `@${app.kebab}/db`;
|
|
137
|
+
const entries = example
|
|
138
|
+
? [handleEntryFor('post', { surfaceDir: 'apps/web/app', feature: 'post', dbModule })]
|
|
139
|
+
: [];
|
|
140
|
+
return [
|
|
141
|
+
{ path: HANDLE_FILE, contents: dbClientSource(entries) },
|
|
142
|
+
{ path: 'packages/db/src/client.test.ts', contents: clientTest(app) },
|
|
143
|
+
];
|
|
144
|
+
};
|