@mandujs/core 0.53.2 → 0.54.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/README.md +654 -654
- package/package.json +2 -2
- package/src/a11y/__tests__/run-audit.test.ts +333 -333
- package/src/a11y/fix-hints.ts +76 -76
- package/src/a11y/index.ts +18 -18
- package/src/a11y/run-audit.ts +394 -394
- package/src/a11y/types.ts +125 -125
- package/src/auth/__tests__/login.test.ts +1 -1
- package/src/auth/__tests__/password.test.ts +122 -122
- package/src/auth/__tests__/tokens.test.ts +274 -274
- package/src/auth/__tests__/verification.test.ts +274 -274
- package/src/auth/index.ts +76 -76
- package/src/auth/login.ts +225 -225
- package/src/auth/password.ts +120 -120
- package/src/auth/reset.ts +243 -243
- package/src/auth/tokens.ts +612 -612
- package/src/auth/verification.ts +253 -253
- package/src/brain/__tests__/redactor.test.ts +94 -94
- package/src/brain/adapters/__tests__/_helpers.ts +83 -83
- package/src/brain/adapters/__tests__/anthropic-oauth.test.ts +196 -196
- package/src/brain/adapters/__tests__/chatgpt-auth.test.ts +193 -193
- package/src/brain/adapters/__tests__/openai-oauth.test.ts +209 -209
- package/src/brain/adapters/__tests__/resolver.test.ts +143 -143
- package/src/brain/adapters/anthropic-oauth.ts +1 -1
- package/src/brain/adapters/chatgpt-auth.ts +300 -300
- package/src/brain/adapters/index.ts +319 -319
- package/src/brain/adapters/oauth-flow.ts +439 -439
- package/src/brain/consent.ts +240 -240
- package/src/brain/credentials.ts +396 -396
- package/src/bundler/__tests__/build-runner.ts +113 -113
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -619
- package/src/bundler/__tests__/extended-watch.test.ts +711 -711
- package/src/bundler/__tests__/fast-refresh.test.ts +10 -10
- package/src/bundler/__tests__/hdr.test.ts +353 -353
- package/src/bundler/__tests__/hmr-client.test.ts +532 -532
- package/src/bundler/__tests__/manifest-schema.test.ts +266 -266
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -138
- package/src/bundler/__tests__/reverse-import-graph.test.ts +519 -519
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -573
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -286
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -455
- package/src/bundler/budget.ts +404 -404
- package/src/bundler/build.test.ts +179 -179
- package/src/bundler/build.ts +55 -55
- package/src/bundler/dev.ts +42 -42
- package/src/bundler/manifest-schema.ts +301 -301
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +263 -263
- package/src/bundler/plugins/__tests__/react-compiler-config.test.ts +83 -83
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -110
- package/src/bundler/plugins/block-generated-imports.ts +155 -155
- package/src/bundler/plugins/index.ts +83 -83
- package/src/bundler/plugins/react-compiler-config.ts +108 -108
- package/src/bundler/plugins/react-compiler-lint.ts +253 -253
- package/src/bundler/plugins/react-compiler.ts +162 -162
- package/src/bundler/reverse-import-graph.ts +339 -339
- package/src/bundler/safe-build.test.ts +201 -201
- package/src/bundler/safe-build.ts +103 -103
- package/src/bundler/scenario-matrix.ts +229 -229
- package/src/bundler/types.ts +10 -10
- package/src/bundler/vendor-cache-types.ts +130 -130
- package/src/bundler/vendor-cache.ts +526 -526
- package/src/change/snapshot.ts +18 -18
- package/src/client/Form.tsx +105 -105
- package/src/client/Link.tsx +9 -9
- package/src/client/__tests__/use-sse.test.ts +153 -153
- package/src/client/globals.ts +1 -1
- package/src/client/hooks.ts +362 -362
- package/src/client/hydrate.ts +340 -340
- package/src/client/prefetch-helper.ts +55 -55
- package/src/client/router.ts +11 -11
- package/src/client/runtime.ts +47 -47
- package/src/client/serialize.ts +404 -404
- package/src/client/use-fetch.ts +6 -6
- package/src/client/use-head.ts +197 -197
- package/src/client/use-sse.ts +378 -378
- package/src/client/window-state.ts +101 -101
- package/src/components/Image.tsx +162 -162
- package/src/config/validate.ts +3 -3
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/collection.ts +8 -8
- package/src/content/content-layer.ts +7 -7
- package/src/content/data-store.ts +245 -245
- package/src/content/frontmatter.ts +189 -189
- package/src/content/loader-context.ts +171 -171
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +172 -172
- package/src/content/loaders/glob.ts +253 -253
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/prebuild.test.ts +571 -571
- package/src/content/prebuild.ts +636 -636
- package/src/content/schema.ts +20 -20
- package/src/content/sidebar.ts +630 -630
- package/src/content/slug.ts +110 -110
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/define.ts +11 -11
- package/src/contract/index.ts +1 -1
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +410 -410
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/route-helpers.ts +1 -1
- package/src/contract/rpc.ts +443 -443
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/db/__tests__/db.test.ts +482 -482
- package/src/db/index.ts +138 -138
- package/src/db/migrations/history-table.ts +345 -345
- package/src/db/migrations/index.ts +3 -3
- package/src/db/migrations/lock.ts +324 -324
- package/src/db/migrations/runner.ts +650 -650
- package/src/deploy/cache.ts +140 -140
- package/src/deploy/compile/vercel.ts +344 -344
- package/src/deploy/index.ts +87 -87
- package/src/deploy/inference/brain.ts +268 -268
- package/src/deploy/inference/context.ts +182 -182
- package/src/deploy/inference/filling-extract.ts +245 -245
- package/src/deploy/inference/heuristic.ts +182 -182
- package/src/deploy/intent.ts +173 -173
- package/src/deploy/plan.ts +178 -178
- package/src/design/__tests__/agents-link.test.ts +109 -109
- package/src/design/__tests__/extract-patch-diff.test.ts +265 -265
- package/src/design/__tests__/lint.test.ts +110 -110
- package/src/design/__tests__/parser.test.ts +195 -195
- package/src/design/__tests__/tailwind-theme.test.ts +229 -229
- package/src/design/agents-link.ts +165 -165
- package/src/design/diff.ts +138 -138
- package/src/design/extract.ts +285 -285
- package/src/design/index.ts +102 -102
- package/src/design/lint.ts +209 -209
- package/src/design/parser.ts +555 -555
- package/src/design/patch.ts +241 -241
- package/src/design/scaffold.ts +147 -147
- package/src/design/tailwind-theme.ts +441 -441
- package/src/design/types.ts +210 -210
- package/src/desktop/__tests__/webview-fallback.test.ts +254 -254
- package/src/desktop/__tests__/window.test.ts +248 -248
- package/src/desktop/__tests__/worker.test.ts +266 -266
- package/src/desktop/index.ts +43 -43
- package/src/desktop/types.ts +158 -158
- package/src/desktop/worker.ts +180 -180
- package/src/dev-error-overlay/__tests__/overlay-injector.test.ts +241 -241
- package/src/dev-error-overlay/index.ts +30 -30
- package/src/dev-error-overlay/overlay-injector.ts +243 -243
- package/src/dev-error-overlay/overlay-styles.ts +52 -52
- package/src/dev-error-overlay/types.ts +66 -66
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +25 -25
- package/src/devtools/client/catchers/error-catcher.ts +344 -344
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/mandu-character.tsx +331 -331
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +30 -30
- package/src/devtools/client/components/panel/islands-panel.tsx +320 -320
- package/src/devtools/client/components/panel/network-panel.tsx +291 -291
- package/src/devtools/client/components/panel/panel-container.tsx +500 -500
- package/src/devtools/client/components/panel/preview-panel.tsx +46 -46
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +265 -265
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +450 -450
- package/src/devtools/types.ts +35 -35
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +233 -233
- package/src/devtools/worker/worker-manager.ts +410 -410
- package/src/diagnose/__tests__/checks.test.ts +451 -380
- package/src/diagnose/checks.ts +832 -720
- package/src/diagnose/index.ts +17 -16
- package/src/diagnose/run.ts +93 -91
- package/src/diagnose/types.ts +53 -53
- package/src/email/__tests__/email.test.ts +355 -355
- package/src/email/index.ts +282 -282
- package/src/email/smtp.ts +64 -64
- package/src/error/domains.ts +265 -265
- package/src/error/result.ts +60 -60
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/__tests__/session-sqlite.test.ts +454 -454
- package/src/filling/auth.ts +308 -308
- package/src/filling/cookie-codec.ts +299 -299
- package/src/filling/deps.ts +265 -265
- package/src/filling/session-sqlite.ts +617 -617
- package/src/filling/sse.ts +5 -5
- package/src/filling/ws.ts +78 -78
- package/src/generator/index.ts +3 -3
- package/src/guard/__tests__/design-inline-class.test.ts +219 -219
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -347
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/define-rule.ts +243 -243
- package/src/guard/design-inline-class.ts +393 -393
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -36
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/reporter.ts +442 -442
- package/src/guard/rule-presets.ts +379 -379
- package/src/guard/semantic-slots.ts +1 -1
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/tsgolint-bridge.ts +512 -512
- package/src/guard/types.ts +348 -348
- package/src/guard/watcher.ts +405 -405
- package/src/i18n/define.ts +126 -126
- package/src/i18n/index.ts +52 -52
- package/src/i18n/message-registry.ts +173 -173
- package/src/i18n/types.ts +112 -112
- package/src/id/index.ts +105 -105
- package/src/index.ts +2 -2
- package/src/kitchen/api/agent-devtools-api.ts +779 -544
- package/src/kitchen/api/errors-grouping.ts +126 -0
- package/src/kitchen/kitchen-handler.ts +192 -63
- package/src/kitchen/kitchen-ui.ts +842 -464
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/middleware/bridge.ts +147 -147
- package/src/middleware/compose.ts +134 -134
- package/src/middleware/compress.ts +62 -62
- package/src/middleware/cors.ts +47 -47
- package/src/middleware/csrf.ts +328 -328
- package/src/middleware/define.ts +132 -132
- package/src/middleware/jwt.ts +134 -134
- package/src/middleware/logger.ts +58 -58
- package/src/middleware/oauth/__tests__/oauth.test.ts +1 -1
- package/src/middleware/oauth/index.ts +505 -505
- package/src/middleware/oauth/providers.ts +115 -115
- package/src/middleware/rate-limit/index.ts +522 -522
- package/src/middleware/rate-limit/sqlite-store.ts +382 -382
- package/src/middleware/scheduler-cron.ts +96 -96
- package/src/middleware/secure/__tests__/secure.test.ts +360 -360
- package/src/middleware/secure/csp.ts +193 -193
- package/src/middleware/session.ts +174 -174
- package/src/middleware/timeout.ts +55 -55
- package/src/observability/logger-adapter.ts +36 -36
- package/src/observability/sqlite-store.ts +254 -254
- package/src/openapi/generator.ts +1 -1
- package/src/openapi/openapi.test.ts +43 -43
- package/src/perf/__tests__/user-marks.test.ts +354 -354
- package/src/perf/index.ts +133 -133
- package/src/plugins/__tests__/lifecycle-integration.test.ts +272 -272
- package/src/plugins/__tests__/runner.test.ts +409 -409
- package/src/plugins/define.ts +124 -124
- package/src/plugins/examples/dep-check-plugin.ts +80 -80
- package/src/plugins/examples/prerender-cache-plugin.ts +111 -111
- package/src/plugins/examples/sitemap-plugin.ts +65 -65
- package/src/plugins/runner.ts +361 -361
- package/src/plugins/types.ts +368 -368
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +32 -32
- package/src/resource/ddl/__tests__/diff.test.ts +639 -639
- package/src/resource/ddl/__tests__/emit.test.ts +823 -823
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -499
- package/src/resource/ddl/emit.ts +559 -559
- package/src/resource/ddl/persistence-types.ts +218 -218
- package/src/resource/ddl/type-map.ts +223 -223
- package/src/resource/ddl/types.ts +232 -232
- package/src/resource/generator-repo.ts +630 -630
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-patterns.test.ts +96 -96
- package/src/router/fs-routes.ts +6 -6
- package/src/routes/index.ts +74 -74
- package/src/routes/metadata-routes.ts +427 -427
- package/src/routes/types.ts +341 -341
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -141
- package/src/runtime/__tests__/hdr-client.test.ts +223 -223
- package/src/runtime/__tests__/http-errors.test.ts +117 -117
- package/src/runtime/__tests__/not-found.test.ts +152 -152
- package/src/runtime/adapter.ts +47 -47
- package/src/runtime/boundary.tsx +252 -252
- package/src/runtime/cache.ts +494 -494
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/fast-refresh-runtime.ts +322 -322
- package/src/runtime/handler.ts +65 -65
- package/src/runtime/handlers.ts +300 -300
- package/src/runtime/http-errors.ts +113 -113
- package/src/runtime/image-handler.ts +1 -1
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/middleware.ts +264 -264
- package/src/runtime/not-found.ts +93 -93
- package/src/runtime/openapi-endpoint.ts +236 -236
- package/src/runtime/ppr.ts +74 -74
- package/src/runtime/registry.ts +171 -171
- package/src/runtime/router.ts +105 -105
- package/src/runtime/server.ts +67 -67
- package/src/runtime/shims.ts +48 -48
- package/src/runtime/ssr.ts +24 -31
- package/src/runtime/streaming-ssr.ts +32 -37
- package/src/runtime/trace.ts +144 -144
- package/src/scheduler/validate.ts +169 -169
- package/src/seo/index.ts +219 -219
- package/src/seo/integration/ssr.ts +306 -306
- package/src/seo/render/basic.ts +435 -435
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +197 -197
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +127 -127
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +104 -104
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +588 -588
- package/src/slot/validator.ts +39 -39
- package/src/storage/s3/__tests__/s3.test.ts +479 -479
- package/src/storage/s3/index.ts +412 -412
- package/src/testing/__tests__/assertions.test.ts +632 -632
- package/src/testing/__tests__/reporter.test.ts +454 -454
- package/src/testing/assertions.ts +986 -986
- package/src/testing/db.ts +157 -157
- package/src/testing/mocks.ts +203 -203
- package/src/testing/session.ts +190 -190
- package/src/types/branded.ts +56 -56
- package/src/types/index.ts +1 -1
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/watcher.ts +18 -18
|
@@ -1,499 +1,499 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Phase 4c — Agent B tests for `snapshot.ts`.
|
|
3
|
-
*
|
|
4
|
-
* Coverage targets:
|
|
5
|
-
* - Normalization correctness (provider gate, table name, column name,
|
|
6
|
-
* primary key resolution, default normalization)
|
|
7
|
-
* - Error paths (conflicting providers, zero/multi PK, unsupported defaults)
|
|
8
|
-
* - JSON serialization determinism (sorted keys, 2-space indent)
|
|
9
|
-
* - Roundtrip parse/serialize
|
|
10
|
-
* - Version rejection, malformed JSON
|
|
11
|
-
* - SHA-256 stability under key-order permutation
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { describe, test, expect } from "bun:test";
|
|
15
|
-
import type { ParsedResource } from "../../parser";
|
|
16
|
-
import type { ResourceDefinition } from "../../schema";
|
|
17
|
-
import type { DdlIndex } from "../types";
|
|
18
|
-
import {
|
|
19
|
-
snapshotFromResources,
|
|
20
|
-
serializeSnapshot,
|
|
21
|
-
parseSnapshot,
|
|
22
|
-
hashSnapshot,
|
|
23
|
-
toSnakeCase,
|
|
24
|
-
} from "../snapshot";
|
|
25
|
-
|
|
26
|
-
// --------------------------------------------------------------------
|
|
27
|
-
// Builders — keep test data minimal + explicit
|
|
28
|
-
// --------------------------------------------------------------------
|
|
29
|
-
|
|
30
|
-
function makeParsed(def: ResourceDefinition, file = `/virtual/${def.name}.resource.ts`): ParsedResource {
|
|
31
|
-
return {
|
|
32
|
-
definition: def,
|
|
33
|
-
filePath: file,
|
|
34
|
-
fileName: def.name,
|
|
35
|
-
resourceName: def.name,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function pkPostgresUser(): ResourceDefinition {
|
|
40
|
-
return {
|
|
41
|
-
name: "user",
|
|
42
|
-
fields: {
|
|
43
|
-
id: { type: "uuid", required: true, primary: true } as ResourceDefinition["fields"][string],
|
|
44
|
-
email: { type: "email", required: true },
|
|
45
|
-
passwordHash: { type: "string", required: true },
|
|
46
|
-
createdAt: { type: "date", required: true, default: "now" },
|
|
47
|
-
},
|
|
48
|
-
options: {
|
|
49
|
-
persistence: {
|
|
50
|
-
provider: "postgres",
|
|
51
|
-
},
|
|
52
|
-
} as unknown as ResourceDefinition["options"],
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// --------------------------------------------------------------------
|
|
57
|
-
// Basic inclusion / exclusion
|
|
58
|
-
// --------------------------------------------------------------------
|
|
59
|
-
|
|
60
|
-
describe("snapshotFromResources — inclusion policy", () => {
|
|
61
|
-
test("1. empty resources array → snapshot with empty resources[]", () => {
|
|
62
|
-
const snap = snapshotFromResources([]);
|
|
63
|
-
expect(snap.version).toBe(1);
|
|
64
|
-
expect(snap.resources).toEqual([]);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test("2. non-persistent resource is excluded from snapshot", () => {
|
|
68
|
-
const resource = makeParsed({
|
|
69
|
-
name: "widget",
|
|
70
|
-
fields: { id: { type: "uuid", required: true } },
|
|
71
|
-
// no options.persistence → skip entirely
|
|
72
|
-
});
|
|
73
|
-
const snap = snapshotFromResources([resource]);
|
|
74
|
-
expect(snap.resources).toEqual([]);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("3. mixing postgres + mysql providers throws TypeError", () => {
|
|
78
|
-
const a = makeParsed({
|
|
79
|
-
name: "user",
|
|
80
|
-
fields: { id: { type: "uuid", required: true, primary: true } as any },
|
|
81
|
-
options: { persistence: { provider: "postgres" } } as any,
|
|
82
|
-
});
|
|
83
|
-
const b = makeParsed({
|
|
84
|
-
name: "post",
|
|
85
|
-
fields: { id: { type: "uuid", required: true, primary: true } as any },
|
|
86
|
-
options: { persistence: { provider: "mysql" } } as any,
|
|
87
|
-
});
|
|
88
|
-
expect(() => snapshotFromResources([a, b])).toThrow(TypeError);
|
|
89
|
-
expect(() => snapshotFromResources([a, b])).toThrow(/Mixed SQL providers/);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// --------------------------------------------------------------------
|
|
94
|
-
// Field + table name normalization
|
|
95
|
-
// --------------------------------------------------------------------
|
|
96
|
-
|
|
97
|
-
describe("snapshotFromResources — table + column naming", () => {
|
|
98
|
-
test("4. camelCase field key becomes snake_case column name", () => {
|
|
99
|
-
const snap = snapshotFromResources([makeParsed(pkPostgresUser())]);
|
|
100
|
-
const user = snap.resources.find((r) => r.name === "users")!;
|
|
101
|
-
const passwordHash = user.fields.find((f) => f.name === "password_hash");
|
|
102
|
-
expect(passwordHash).toBeDefined();
|
|
103
|
-
expect(user.fields.find((f) => f.name === "created_at")).toBeDefined();
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("5. autoPlural: false keeps singular table name", () => {
|
|
107
|
-
const def = pkPostgresUser();
|
|
108
|
-
def.options = { autoPlural: false, persistence: { provider: "postgres" } } as any;
|
|
109
|
-
const snap = snapshotFromResources([makeParsed(def)]);
|
|
110
|
-
expect(snap.resources[0].name).toBe("user");
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test("6. persistence.tableName beats auto-plural", () => {
|
|
114
|
-
const def = pkPostgresUser();
|
|
115
|
-
def.options = {
|
|
116
|
-
persistence: { provider: "postgres", tableName: "app_user" },
|
|
117
|
-
} as any;
|
|
118
|
-
const snap = snapshotFromResources([makeParsed(def)]);
|
|
119
|
-
expect(snap.resources[0].name).toBe("app_user");
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("7. fieldOverrides[key].columnName beats snake_case default", () => {
|
|
123
|
-
const def = pkPostgresUser();
|
|
124
|
-
def.options = {
|
|
125
|
-
persistence: {
|
|
126
|
-
provider: "postgres",
|
|
127
|
-
fieldOverrides: { passwordHash: { columnName: "pwd" } },
|
|
128
|
-
},
|
|
129
|
-
} as any;
|
|
130
|
-
const snap = snapshotFromResources([makeParsed(def)]);
|
|
131
|
-
const f = snap.resources[0].fields.find((f) => f.name === "pwd");
|
|
132
|
-
expect(f).toBeDefined();
|
|
133
|
-
expect(snap.resources[0].fields.find((f) => f.name === "password_hash")).toBeUndefined();
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test("17. duplicate table name after pluralization throws (both names listed)", () => {
|
|
137
|
-
// Two resources both pluralize to "users".
|
|
138
|
-
const defA: ResourceDefinition = {
|
|
139
|
-
name: "user",
|
|
140
|
-
fields: { id: { type: "uuid", required: true, primary: true } as any },
|
|
141
|
-
options: { persistence: { provider: "postgres" } } as any,
|
|
142
|
-
};
|
|
143
|
-
const defB: ResourceDefinition = {
|
|
144
|
-
name: "account",
|
|
145
|
-
fields: { id: { type: "uuid", required: true, primary: true } as any },
|
|
146
|
-
options: { persistence: { provider: "postgres", tableName: "users" } } as any,
|
|
147
|
-
};
|
|
148
|
-
expect(() => snapshotFromResources([makeParsed(defA), makeParsed(defB)])).toThrow(
|
|
149
|
-
/Duplicate table name "users".*user.*account/s
|
|
150
|
-
);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
test("pluralizer handles -y → -ies", () => {
|
|
154
|
-
const def: ResourceDefinition = {
|
|
155
|
-
name: "category",
|
|
156
|
-
fields: { id: { type: "uuid", required: true, primary: true } as any },
|
|
157
|
-
options: { persistence: { provider: "postgres" } } as any,
|
|
158
|
-
};
|
|
159
|
-
const snap = snapshotFromResources([makeParsed(def)]);
|
|
160
|
-
expect(snap.resources[0].name).toBe("categories");
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test("pluralizer handles -s → -ses", () => {
|
|
164
|
-
const def: ResourceDefinition = {
|
|
165
|
-
name: "bus",
|
|
166
|
-
fields: { id: { type: "uuid", required: true, primary: true } as any },
|
|
167
|
-
options: { persistence: { provider: "postgres" } } as any,
|
|
168
|
-
};
|
|
169
|
-
const snap = snapshotFromResources([makeParsed(def)]);
|
|
170
|
-
expect(snap.resources[0].name).toBe("buses");
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
// --------------------------------------------------------------------
|
|
175
|
-
// Primary key resolution
|
|
176
|
-
// --------------------------------------------------------------------
|
|
177
|
-
|
|
178
|
-
describe("snapshotFromResources — primary key", () => {
|
|
179
|
-
test("8. zero primary-key fields throws", () => {
|
|
180
|
-
const def: ResourceDefinition = {
|
|
181
|
-
name: "user",
|
|
182
|
-
fields: {
|
|
183
|
-
id: { type: "uuid", required: true }, // no primary flag
|
|
184
|
-
email: { type: "email", required: true },
|
|
185
|
-
},
|
|
186
|
-
options: { persistence: { provider: "postgres" } } as any,
|
|
187
|
-
};
|
|
188
|
-
expect(() => snapshotFromResources([makeParsed(def)])).toThrow(/exactly one primary key/);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test("9. multiple primary:true fields throws (composite PK not supported)", () => {
|
|
192
|
-
const def: ResourceDefinition = {
|
|
193
|
-
name: "user",
|
|
194
|
-
fields: {
|
|
195
|
-
id: { type: "uuid", required: true, primary: true } as any,
|
|
196
|
-
email: { type: "email", required: true, primary: true } as any,
|
|
197
|
-
},
|
|
198
|
-
options: { persistence: { provider: "postgres" } } as any,
|
|
199
|
-
};
|
|
200
|
-
expect(() => snapshotFromResources([makeParsed(def)])).toThrow(/Composite primary keys/);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test("persistence.primaryKey string resolves PK without field-level flag", () => {
|
|
204
|
-
const def: ResourceDefinition = {
|
|
205
|
-
name: "user",
|
|
206
|
-
fields: {
|
|
207
|
-
id: { type: "uuid", required: true }, // no field-level primary
|
|
208
|
-
email: { type: "email", required: true },
|
|
209
|
-
},
|
|
210
|
-
options: { persistence: { provider: "postgres", primaryKey: "id" } } as any,
|
|
211
|
-
};
|
|
212
|
-
const snap = snapshotFromResources([makeParsed(def)]);
|
|
213
|
-
const pk = snap.resources[0].fields.find((f) => f.primary);
|
|
214
|
-
expect(pk?.name).toBe("id");
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
test("persistence.primaryKey [string] (single-element array) resolves PK", () => {
|
|
218
|
-
const def: ResourceDefinition = {
|
|
219
|
-
name: "user",
|
|
220
|
-
fields: { id: { type: "uuid", required: true } },
|
|
221
|
-
options: { persistence: { provider: "postgres", primaryKey: ["id"] } } as any,
|
|
222
|
-
};
|
|
223
|
-
const snap = snapshotFromResources([makeParsed(def)]);
|
|
224
|
-
expect(snap.resources[0].fields[0].primary).toBe(true);
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
// --------------------------------------------------------------------
|
|
229
|
-
// Default value normalization
|
|
230
|
-
// --------------------------------------------------------------------
|
|
231
|
-
|
|
232
|
-
describe("snapshotFromResources — default normalization", () => {
|
|
233
|
-
test("10. default values map correctly to DdlDefault", () => {
|
|
234
|
-
const def: ResourceDefinition = {
|
|
235
|
-
name: "user",
|
|
236
|
-
fields: {
|
|
237
|
-
id: { type: "uuid", required: true, primary: true } as any,
|
|
238
|
-
createdAt: { type: "date", required: true, default: "now" },
|
|
239
|
-
deletedAt: { type: "date", required: false, default: null },
|
|
240
|
-
age: { type: "number", required: false, default: 42 },
|
|
241
|
-
active: { type: "boolean", required: false, default: true },
|
|
242
|
-
nickname: { type: "string", required: false, default: "anon" },
|
|
243
|
-
},
|
|
244
|
-
options: { persistence: { provider: "postgres" } } as any,
|
|
245
|
-
};
|
|
246
|
-
const snap = snapshotFromResources([makeParsed(def)]);
|
|
247
|
-
const byName = new Map(snap.resources[0].fields.map((f) => [f.name, f]));
|
|
248
|
-
expect(byName.get("created_at")!.default).toEqual({ kind: "now" });
|
|
249
|
-
expect(byName.get("deleted_at")!.default).toEqual({ kind: "null" });
|
|
250
|
-
expect(byName.get("age")!.default).toEqual({ kind: "literal", value: 42 });
|
|
251
|
-
expect(byName.get("active")!.default).toEqual({ kind: "literal", value: true });
|
|
252
|
-
expect(byName.get("nickname")!.default).toEqual({ kind: "literal", value: "anon" });
|
|
253
|
-
// id has no default
|
|
254
|
-
expect(byName.get("id")!.default).toBeUndefined();
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
test("'current_timestamp' also normalizes to { kind: 'now' }", () => {
|
|
258
|
-
const def: ResourceDefinition = {
|
|
259
|
-
name: "user",
|
|
260
|
-
fields: {
|
|
261
|
-
id: { type: "uuid", required: true, primary: true } as any,
|
|
262
|
-
created_at: { type: "date", required: true, default: "current_timestamp" },
|
|
263
|
-
},
|
|
264
|
-
options: { persistence: { provider: "postgres" } } as any,
|
|
265
|
-
};
|
|
266
|
-
const snap = snapshotFromResources([makeParsed(def)]);
|
|
267
|
-
const f = snap.resources[0].fields.find((f) => f.name === "created_at")!;
|
|
268
|
-
expect(f.default).toEqual({ kind: "now" });
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
test("11. function default throws TypeError", () => {
|
|
272
|
-
const def: ResourceDefinition = {
|
|
273
|
-
name: "user",
|
|
274
|
-
fields: {
|
|
275
|
-
id: { type: "uuid", required: true, primary: true } as any,
|
|
276
|
-
createdAt: { type: "date", required: true, default: () => new Date() } as any,
|
|
277
|
-
},
|
|
278
|
-
options: { persistence: { provider: "postgres" } } as any,
|
|
279
|
-
};
|
|
280
|
-
expect(() => snapshotFromResources([makeParsed(def)])).toThrow(/unsupported default type/);
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
test("object default throws TypeError (not a scalar)", () => {
|
|
284
|
-
const def: ResourceDefinition = {
|
|
285
|
-
name: "user",
|
|
286
|
-
fields: {
|
|
287
|
-
id: { type: "uuid", required: true, primary: true } as any,
|
|
288
|
-
meta: { type: "json", required: false, default: {} } as any,
|
|
289
|
-
},
|
|
290
|
-
options: { persistence: { provider: "postgres" } } as any,
|
|
291
|
-
};
|
|
292
|
-
expect(() => snapshotFromResources([makeParsed(def)])).toThrow(/unsupported default type/);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
test("non-finite number default throws", () => {
|
|
296
|
-
const def: ResourceDefinition = {
|
|
297
|
-
name: "user",
|
|
298
|
-
fields: {
|
|
299
|
-
id: { type: "uuid", required: true, primary: true } as any,
|
|
300
|
-
score: { type: "number", required: false, default: Infinity },
|
|
301
|
-
},
|
|
302
|
-
options: { persistence: { provider: "postgres" } } as any,
|
|
303
|
-
};
|
|
304
|
-
expect(() => snapshotFromResources([makeParsed(def)])).toThrow(/non-finite default/);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
test("fieldOverrides[key].default bypasses field.default magic", () => {
|
|
308
|
-
const def: ResourceDefinition = {
|
|
309
|
-
name: "user",
|
|
310
|
-
fields: {
|
|
311
|
-
id: { type: "uuid", required: true, primary: true } as any,
|
|
312
|
-
createdAt: { type: "date", required: true, default: "now" },
|
|
313
|
-
},
|
|
314
|
-
options: {
|
|
315
|
-
persistence: {
|
|
316
|
-
provider: "postgres",
|
|
317
|
-
fieldOverrides: {
|
|
318
|
-
createdAt: { default: { kind: "sql", expr: "timezone('UTC', now())" } },
|
|
319
|
-
},
|
|
320
|
-
},
|
|
321
|
-
} as any,
|
|
322
|
-
};
|
|
323
|
-
const snap = snapshotFromResources([makeParsed(def)]);
|
|
324
|
-
const f = snap.resources[0].fields.find((f) => f.name === "created_at")!;
|
|
325
|
-
expect(f.default).toEqual({ kind: "sql", expr: "timezone('UTC', now())" });
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
// --------------------------------------------------------------------
|
|
330
|
-
// Serialization + roundtrip
|
|
331
|
-
// --------------------------------------------------------------------
|
|
332
|
-
|
|
333
|
-
describe("serializeSnapshot / parseSnapshot", () => {
|
|
334
|
-
test("12. roundtrip preserves bytes", () => {
|
|
335
|
-
const snap = snapshotFromResources([makeParsed(pkPostgresUser())]);
|
|
336
|
-
const serialized = serializeSnapshot(snap);
|
|
337
|
-
const reparsed = parseSnapshot(serialized);
|
|
338
|
-
const reserialized = serializeSnapshot(reparsed);
|
|
339
|
-
expect(reserialized).toBe(serialized);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
test("13. serialization uses 2-space indent and sorted keys", () => {
|
|
343
|
-
const snap = snapshotFromResources([makeParsed(pkPostgresUser())]);
|
|
344
|
-
const json = serializeSnapshot(snap);
|
|
345
|
-
// 2-space indent → keys appear after " " on their own line.
|
|
346
|
-
expect(json).toContain(' "generatedAt":');
|
|
347
|
-
expect(json).toContain(' "provider":');
|
|
348
|
-
expect(json).toContain(' "resources":');
|
|
349
|
-
expect(json).toContain(' "version":');
|
|
350
|
-
// Sorted keys — "generatedAt" precedes "provider" precedes "resources"
|
|
351
|
-
// precedes "version" alphabetically.
|
|
352
|
-
const generatedIdx = json.indexOf('"generatedAt"');
|
|
353
|
-
const providerIdx = json.indexOf('"provider"');
|
|
354
|
-
const resourcesIdx = json.indexOf('"resources"');
|
|
355
|
-
const versionIdx = json.indexOf('"version"');
|
|
356
|
-
expect(generatedIdx).toBeLessThan(providerIdx);
|
|
357
|
-
expect(providerIdx).toBeLessThan(resourcesIdx);
|
|
358
|
-
expect(resourcesIdx).toBeLessThan(versionIdx);
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
test("14. parseSnapshot rejects version: 2 with clear message", () => {
|
|
362
|
-
const v2 = JSON.stringify({
|
|
363
|
-
version: 2,
|
|
364
|
-
provider: "postgres",
|
|
365
|
-
resources: [],
|
|
366
|
-
generatedAt: new Date().toISOString(),
|
|
367
|
-
});
|
|
368
|
-
expect(() => parseSnapshot(v2)).toThrow(/Unsupported snapshot version/);
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
test("15. parseSnapshot rejects malformed JSON", () => {
|
|
372
|
-
expect(() => parseSnapshot("{not-json")).toThrow(/Invalid snapshot JSON/);
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
test("parseSnapshot rejects missing fields", () => {
|
|
376
|
-
expect(() =>
|
|
377
|
-
parseSnapshot(JSON.stringify({ version: 1, provider: "postgres" }))
|
|
378
|
-
).toThrow(/Snapshot.resources/);
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
test("parseSnapshot rejects invalid provider", () => {
|
|
382
|
-
expect(() =>
|
|
383
|
-
parseSnapshot(JSON.stringify({ version: 1, provider: "oracle", resources: [], generatedAt: "now" }))
|
|
384
|
-
).toThrow(/invalid provider/);
|
|
385
|
-
});
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
// --------------------------------------------------------------------
|
|
389
|
-
// Hash stability
|
|
390
|
-
// --------------------------------------------------------------------
|
|
391
|
-
|
|
392
|
-
describe("hashSnapshot", () => {
|
|
393
|
-
test("16. hash is stable across object-key permutations (serialization is the canonicalizer)", () => {
|
|
394
|
-
const now = new Date("2026-01-01T00:00:00Z").toISOString();
|
|
395
|
-
// Same snapshot expressed two different ways — different key order.
|
|
396
|
-
const a = {
|
|
397
|
-
version: 1 as const,
|
|
398
|
-
provider: "postgres" as const,
|
|
399
|
-
generatedAt: now,
|
|
400
|
-
resources: [
|
|
401
|
-
{
|
|
402
|
-
name: "users",
|
|
403
|
-
fields: [
|
|
404
|
-
{ name: "id", type: "uuid" as const, nullable: false, primary: true, unique: false, indexed: false },
|
|
405
|
-
],
|
|
406
|
-
indexes: [],
|
|
407
|
-
},
|
|
408
|
-
],
|
|
409
|
-
};
|
|
410
|
-
const b = {
|
|
411
|
-
// Different key insertion order — must produce same hash.
|
|
412
|
-
provider: "postgres" as const,
|
|
413
|
-
resources: [
|
|
414
|
-
{
|
|
415
|
-
fields: [
|
|
416
|
-
{ unique: false, primary: true, nullable: false, name: "id", type: "uuid" as const, indexed: false },
|
|
417
|
-
],
|
|
418
|
-
name: "users",
|
|
419
|
-
indexes: [],
|
|
420
|
-
},
|
|
421
|
-
],
|
|
422
|
-
generatedAt: now,
|
|
423
|
-
version: 1 as const,
|
|
424
|
-
};
|
|
425
|
-
expect(hashSnapshot(a)).toBe(hashSnapshot(b));
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
test("hash is hex string of SHA-256 (64 chars)", () => {
|
|
429
|
-
const snap = snapshotFromResources([makeParsed(pkPostgresUser())]);
|
|
430
|
-
const h = hashSnapshot(snap);
|
|
431
|
-
expect(h).toMatch(/^[0-9a-f]{64}$/);
|
|
432
|
-
});
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
// --------------------------------------------------------------------
|
|
436
|
-
// toSnakeCase — exported for reuse by Agent D generator
|
|
437
|
-
// --------------------------------------------------------------------
|
|
438
|
-
|
|
439
|
-
describe("toSnakeCase", () => {
|
|
440
|
-
test("camelCase", () => {
|
|
441
|
-
expect(toSnakeCase("passwordHash")).toBe("password_hash");
|
|
442
|
-
});
|
|
443
|
-
test("PascalCase", () => {
|
|
444
|
-
expect(toSnakeCase("PasswordHash")).toBe("password_hash");
|
|
445
|
-
});
|
|
446
|
-
test("runs of caps stay together", () => {
|
|
447
|
-
expect(toSnakeCase("HTTPRequestId")).toBe("http_request_id");
|
|
448
|
-
});
|
|
449
|
-
test("already snake is untouched", () => {
|
|
450
|
-
expect(toSnakeCase("password_hash")).toBe("password_hash");
|
|
451
|
-
});
|
|
452
|
-
test("single letter", () => {
|
|
453
|
-
expect(toSnakeCase("x")).toBe("x");
|
|
454
|
-
});
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
// --------------------------------------------------------------------
|
|
458
|
-
// Indexes normalization sanity
|
|
459
|
-
// --------------------------------------------------------------------
|
|
460
|
-
|
|
461
|
-
describe("snapshotFromResources — indexes", () => {
|
|
462
|
-
test("index field keys resolve to snake_case column names", () => {
|
|
463
|
-
const def: ResourceDefinition = {
|
|
464
|
-
name: "user",
|
|
465
|
-
fields: {
|
|
466
|
-
id: { type: "uuid", required: true, primary: true } as any,
|
|
467
|
-
createdAt: { type: "date", required: true },
|
|
468
|
-
},
|
|
469
|
-
options: {
|
|
470
|
-
persistence: {
|
|
471
|
-
provider: "postgres",
|
|
472
|
-
indexes: [{ name: "idx_user_created", fields: ["createdAt"], unique: false } as DdlIndex],
|
|
473
|
-
},
|
|
474
|
-
} as any,
|
|
475
|
-
};
|
|
476
|
-
const snap = snapshotFromResources([makeParsed(def)]);
|
|
477
|
-
expect(snap.resources[0].indexes[0].fields).toEqual(["created_at"]);
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
test("duplicate index names throw", () => {
|
|
481
|
-
const def: ResourceDefinition = {
|
|
482
|
-
name: "user",
|
|
483
|
-
fields: {
|
|
484
|
-
id: { type: "uuid", required: true, primary: true } as any,
|
|
485
|
-
email: { type: "email", required: true },
|
|
486
|
-
},
|
|
487
|
-
options: {
|
|
488
|
-
persistence: {
|
|
489
|
-
provider: "postgres",
|
|
490
|
-
indexes: [
|
|
491
|
-
{ name: "dup", fields: ["email"], unique: false } as DdlIndex,
|
|
492
|
-
{ name: "dup", fields: ["id"], unique: true } as DdlIndex,
|
|
493
|
-
],
|
|
494
|
-
},
|
|
495
|
-
} as any,
|
|
496
|
-
};
|
|
497
|
-
expect(() => snapshotFromResources([makeParsed(def)])).toThrow(/Duplicate index name/);
|
|
498
|
-
});
|
|
499
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4c — Agent B tests for `snapshot.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Coverage targets:
|
|
5
|
+
* - Normalization correctness (provider gate, table name, column name,
|
|
6
|
+
* primary key resolution, default normalization)
|
|
7
|
+
* - Error paths (conflicting providers, zero/multi PK, unsupported defaults)
|
|
8
|
+
* - JSON serialization determinism (sorted keys, 2-space indent)
|
|
9
|
+
* - Roundtrip parse/serialize
|
|
10
|
+
* - Version rejection, malformed JSON
|
|
11
|
+
* - SHA-256 stability under key-order permutation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, test, expect } from "bun:test";
|
|
15
|
+
import type { ParsedResource } from "../../parser";
|
|
16
|
+
import type { ResourceDefinition } from "../../schema";
|
|
17
|
+
import type { DdlIndex } from "../types";
|
|
18
|
+
import {
|
|
19
|
+
snapshotFromResources,
|
|
20
|
+
serializeSnapshot,
|
|
21
|
+
parseSnapshot,
|
|
22
|
+
hashSnapshot,
|
|
23
|
+
toSnakeCase,
|
|
24
|
+
} from "../snapshot";
|
|
25
|
+
|
|
26
|
+
// --------------------------------------------------------------------
|
|
27
|
+
// Builders — keep test data minimal + explicit
|
|
28
|
+
// --------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
function makeParsed(def: ResourceDefinition, file = `/virtual/${def.name}.resource.ts`): ParsedResource {
|
|
31
|
+
return {
|
|
32
|
+
definition: def,
|
|
33
|
+
filePath: file,
|
|
34
|
+
fileName: def.name,
|
|
35
|
+
resourceName: def.name,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function pkPostgresUser(): ResourceDefinition {
|
|
40
|
+
return {
|
|
41
|
+
name: "user",
|
|
42
|
+
fields: {
|
|
43
|
+
id: { type: "uuid", required: true, primary: true } as ResourceDefinition["fields"][string],
|
|
44
|
+
email: { type: "email", required: true },
|
|
45
|
+
passwordHash: { type: "string", required: true },
|
|
46
|
+
createdAt: { type: "date", required: true, default: "now" },
|
|
47
|
+
},
|
|
48
|
+
options: {
|
|
49
|
+
persistence: {
|
|
50
|
+
provider: "postgres",
|
|
51
|
+
},
|
|
52
|
+
} as unknown as ResourceDefinition["options"],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// --------------------------------------------------------------------
|
|
57
|
+
// Basic inclusion / exclusion
|
|
58
|
+
// --------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
describe("snapshotFromResources — inclusion policy", () => {
|
|
61
|
+
test("1. empty resources array → snapshot with empty resources[]", () => {
|
|
62
|
+
const snap = snapshotFromResources([]);
|
|
63
|
+
expect(snap.version).toBe(1);
|
|
64
|
+
expect(snap.resources).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("2. non-persistent resource is excluded from snapshot", () => {
|
|
68
|
+
const resource = makeParsed({
|
|
69
|
+
name: "widget",
|
|
70
|
+
fields: { id: { type: "uuid", required: true } },
|
|
71
|
+
// no options.persistence → skip entirely
|
|
72
|
+
});
|
|
73
|
+
const snap = snapshotFromResources([resource]);
|
|
74
|
+
expect(snap.resources).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("3. mixing postgres + mysql providers throws TypeError", () => {
|
|
78
|
+
const a = makeParsed({
|
|
79
|
+
name: "user",
|
|
80
|
+
fields: { id: { type: "uuid", required: true, primary: true } as any },
|
|
81
|
+
options: { persistence: { provider: "postgres" } } as any,
|
|
82
|
+
});
|
|
83
|
+
const b = makeParsed({
|
|
84
|
+
name: "post",
|
|
85
|
+
fields: { id: { type: "uuid", required: true, primary: true } as any },
|
|
86
|
+
options: { persistence: { provider: "mysql" } } as any,
|
|
87
|
+
});
|
|
88
|
+
expect(() => snapshotFromResources([a, b])).toThrow(TypeError);
|
|
89
|
+
expect(() => snapshotFromResources([a, b])).toThrow(/Mixed SQL providers/);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// --------------------------------------------------------------------
|
|
94
|
+
// Field + table name normalization
|
|
95
|
+
// --------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
describe("snapshotFromResources — table + column naming", () => {
|
|
98
|
+
test("4. camelCase field key becomes snake_case column name", () => {
|
|
99
|
+
const snap = snapshotFromResources([makeParsed(pkPostgresUser())]);
|
|
100
|
+
const user = snap.resources.find((r) => r.name === "users")!;
|
|
101
|
+
const passwordHash = user.fields.find((f) => f.name === "password_hash");
|
|
102
|
+
expect(passwordHash).toBeDefined();
|
|
103
|
+
expect(user.fields.find((f) => f.name === "created_at")).toBeDefined();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("5. autoPlural: false keeps singular table name", () => {
|
|
107
|
+
const def = pkPostgresUser();
|
|
108
|
+
def.options = { autoPlural: false, persistence: { provider: "postgres" } } as any;
|
|
109
|
+
const snap = snapshotFromResources([makeParsed(def)]);
|
|
110
|
+
expect(snap.resources[0].name).toBe("user");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("6. persistence.tableName beats auto-plural", () => {
|
|
114
|
+
const def = pkPostgresUser();
|
|
115
|
+
def.options = {
|
|
116
|
+
persistence: { provider: "postgres", tableName: "app_user" },
|
|
117
|
+
} as any;
|
|
118
|
+
const snap = snapshotFromResources([makeParsed(def)]);
|
|
119
|
+
expect(snap.resources[0].name).toBe("app_user");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("7. fieldOverrides[key].columnName beats snake_case default", () => {
|
|
123
|
+
const def = pkPostgresUser();
|
|
124
|
+
def.options = {
|
|
125
|
+
persistence: {
|
|
126
|
+
provider: "postgres",
|
|
127
|
+
fieldOverrides: { passwordHash: { columnName: "pwd" } },
|
|
128
|
+
},
|
|
129
|
+
} as any;
|
|
130
|
+
const snap = snapshotFromResources([makeParsed(def)]);
|
|
131
|
+
const f = snap.resources[0].fields.find((f) => f.name === "pwd");
|
|
132
|
+
expect(f).toBeDefined();
|
|
133
|
+
expect(snap.resources[0].fields.find((f) => f.name === "password_hash")).toBeUndefined();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("17. duplicate table name after pluralization throws (both names listed)", () => {
|
|
137
|
+
// Two resources both pluralize to "users".
|
|
138
|
+
const defA: ResourceDefinition = {
|
|
139
|
+
name: "user",
|
|
140
|
+
fields: { id: { type: "uuid", required: true, primary: true } as any },
|
|
141
|
+
options: { persistence: { provider: "postgres" } } as any,
|
|
142
|
+
};
|
|
143
|
+
const defB: ResourceDefinition = {
|
|
144
|
+
name: "account",
|
|
145
|
+
fields: { id: { type: "uuid", required: true, primary: true } as any },
|
|
146
|
+
options: { persistence: { provider: "postgres", tableName: "users" } } as any,
|
|
147
|
+
};
|
|
148
|
+
expect(() => snapshotFromResources([makeParsed(defA), makeParsed(defB)])).toThrow(
|
|
149
|
+
/Duplicate table name "users".*user.*account/s
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("pluralizer handles -y → -ies", () => {
|
|
154
|
+
const def: ResourceDefinition = {
|
|
155
|
+
name: "category",
|
|
156
|
+
fields: { id: { type: "uuid", required: true, primary: true } as any },
|
|
157
|
+
options: { persistence: { provider: "postgres" } } as any,
|
|
158
|
+
};
|
|
159
|
+
const snap = snapshotFromResources([makeParsed(def)]);
|
|
160
|
+
expect(snap.resources[0].name).toBe("categories");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("pluralizer handles -s → -ses", () => {
|
|
164
|
+
const def: ResourceDefinition = {
|
|
165
|
+
name: "bus",
|
|
166
|
+
fields: { id: { type: "uuid", required: true, primary: true } as any },
|
|
167
|
+
options: { persistence: { provider: "postgres" } } as any,
|
|
168
|
+
};
|
|
169
|
+
const snap = snapshotFromResources([makeParsed(def)]);
|
|
170
|
+
expect(snap.resources[0].name).toBe("buses");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// --------------------------------------------------------------------
|
|
175
|
+
// Primary key resolution
|
|
176
|
+
// --------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
describe("snapshotFromResources — primary key", () => {
|
|
179
|
+
test("8. zero primary-key fields throws", () => {
|
|
180
|
+
const def: ResourceDefinition = {
|
|
181
|
+
name: "user",
|
|
182
|
+
fields: {
|
|
183
|
+
id: { type: "uuid", required: true }, // no primary flag
|
|
184
|
+
email: { type: "email", required: true },
|
|
185
|
+
},
|
|
186
|
+
options: { persistence: { provider: "postgres" } } as any,
|
|
187
|
+
};
|
|
188
|
+
expect(() => snapshotFromResources([makeParsed(def)])).toThrow(/exactly one primary key/);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("9. multiple primary:true fields throws (composite PK not supported)", () => {
|
|
192
|
+
const def: ResourceDefinition = {
|
|
193
|
+
name: "user",
|
|
194
|
+
fields: {
|
|
195
|
+
id: { type: "uuid", required: true, primary: true } as any,
|
|
196
|
+
email: { type: "email", required: true, primary: true } as any,
|
|
197
|
+
},
|
|
198
|
+
options: { persistence: { provider: "postgres" } } as any,
|
|
199
|
+
};
|
|
200
|
+
expect(() => snapshotFromResources([makeParsed(def)])).toThrow(/Composite primary keys/);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("persistence.primaryKey string resolves PK without field-level flag", () => {
|
|
204
|
+
const def: ResourceDefinition = {
|
|
205
|
+
name: "user",
|
|
206
|
+
fields: {
|
|
207
|
+
id: { type: "uuid", required: true }, // no field-level primary
|
|
208
|
+
email: { type: "email", required: true },
|
|
209
|
+
},
|
|
210
|
+
options: { persistence: { provider: "postgres", primaryKey: "id" } } as any,
|
|
211
|
+
};
|
|
212
|
+
const snap = snapshotFromResources([makeParsed(def)]);
|
|
213
|
+
const pk = snap.resources[0].fields.find((f) => f.primary);
|
|
214
|
+
expect(pk?.name).toBe("id");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("persistence.primaryKey [string] (single-element array) resolves PK", () => {
|
|
218
|
+
const def: ResourceDefinition = {
|
|
219
|
+
name: "user",
|
|
220
|
+
fields: { id: { type: "uuid", required: true } },
|
|
221
|
+
options: { persistence: { provider: "postgres", primaryKey: ["id"] } } as any,
|
|
222
|
+
};
|
|
223
|
+
const snap = snapshotFromResources([makeParsed(def)]);
|
|
224
|
+
expect(snap.resources[0].fields[0].primary).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// --------------------------------------------------------------------
|
|
229
|
+
// Default value normalization
|
|
230
|
+
// --------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
describe("snapshotFromResources — default normalization", () => {
|
|
233
|
+
test("10. default values map correctly to DdlDefault", () => {
|
|
234
|
+
const def: ResourceDefinition = {
|
|
235
|
+
name: "user",
|
|
236
|
+
fields: {
|
|
237
|
+
id: { type: "uuid", required: true, primary: true } as any,
|
|
238
|
+
createdAt: { type: "date", required: true, default: "now" },
|
|
239
|
+
deletedAt: { type: "date", required: false, default: null },
|
|
240
|
+
age: { type: "number", required: false, default: 42 },
|
|
241
|
+
active: { type: "boolean", required: false, default: true },
|
|
242
|
+
nickname: { type: "string", required: false, default: "anon" },
|
|
243
|
+
},
|
|
244
|
+
options: { persistence: { provider: "postgres" } } as any,
|
|
245
|
+
};
|
|
246
|
+
const snap = snapshotFromResources([makeParsed(def)]);
|
|
247
|
+
const byName = new Map(snap.resources[0].fields.map((f) => [f.name, f]));
|
|
248
|
+
expect(byName.get("created_at")!.default).toEqual({ kind: "now" });
|
|
249
|
+
expect(byName.get("deleted_at")!.default).toEqual({ kind: "null" });
|
|
250
|
+
expect(byName.get("age")!.default).toEqual({ kind: "literal", value: 42 });
|
|
251
|
+
expect(byName.get("active")!.default).toEqual({ kind: "literal", value: true });
|
|
252
|
+
expect(byName.get("nickname")!.default).toEqual({ kind: "literal", value: "anon" });
|
|
253
|
+
// id has no default
|
|
254
|
+
expect(byName.get("id")!.default).toBeUndefined();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("'current_timestamp' also normalizes to { kind: 'now' }", () => {
|
|
258
|
+
const def: ResourceDefinition = {
|
|
259
|
+
name: "user",
|
|
260
|
+
fields: {
|
|
261
|
+
id: { type: "uuid", required: true, primary: true } as any,
|
|
262
|
+
created_at: { type: "date", required: true, default: "current_timestamp" },
|
|
263
|
+
},
|
|
264
|
+
options: { persistence: { provider: "postgres" } } as any,
|
|
265
|
+
};
|
|
266
|
+
const snap = snapshotFromResources([makeParsed(def)]);
|
|
267
|
+
const f = snap.resources[0].fields.find((f) => f.name === "created_at")!;
|
|
268
|
+
expect(f.default).toEqual({ kind: "now" });
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("11. function default throws TypeError", () => {
|
|
272
|
+
const def: ResourceDefinition = {
|
|
273
|
+
name: "user",
|
|
274
|
+
fields: {
|
|
275
|
+
id: { type: "uuid", required: true, primary: true } as any,
|
|
276
|
+
createdAt: { type: "date", required: true, default: () => new Date() } as any,
|
|
277
|
+
},
|
|
278
|
+
options: { persistence: { provider: "postgres" } } as any,
|
|
279
|
+
};
|
|
280
|
+
expect(() => snapshotFromResources([makeParsed(def)])).toThrow(/unsupported default type/);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("object default throws TypeError (not a scalar)", () => {
|
|
284
|
+
const def: ResourceDefinition = {
|
|
285
|
+
name: "user",
|
|
286
|
+
fields: {
|
|
287
|
+
id: { type: "uuid", required: true, primary: true } as any,
|
|
288
|
+
meta: { type: "json", required: false, default: {} } as any,
|
|
289
|
+
},
|
|
290
|
+
options: { persistence: { provider: "postgres" } } as any,
|
|
291
|
+
};
|
|
292
|
+
expect(() => snapshotFromResources([makeParsed(def)])).toThrow(/unsupported default type/);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("non-finite number default throws", () => {
|
|
296
|
+
const def: ResourceDefinition = {
|
|
297
|
+
name: "user",
|
|
298
|
+
fields: {
|
|
299
|
+
id: { type: "uuid", required: true, primary: true } as any,
|
|
300
|
+
score: { type: "number", required: false, default: Infinity },
|
|
301
|
+
},
|
|
302
|
+
options: { persistence: { provider: "postgres" } } as any,
|
|
303
|
+
};
|
|
304
|
+
expect(() => snapshotFromResources([makeParsed(def)])).toThrow(/non-finite default/);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("fieldOverrides[key].default bypasses field.default magic", () => {
|
|
308
|
+
const def: ResourceDefinition = {
|
|
309
|
+
name: "user",
|
|
310
|
+
fields: {
|
|
311
|
+
id: { type: "uuid", required: true, primary: true } as any,
|
|
312
|
+
createdAt: { type: "date", required: true, default: "now" },
|
|
313
|
+
},
|
|
314
|
+
options: {
|
|
315
|
+
persistence: {
|
|
316
|
+
provider: "postgres",
|
|
317
|
+
fieldOverrides: {
|
|
318
|
+
createdAt: { default: { kind: "sql", expr: "timezone('UTC', now())" } },
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
} as any,
|
|
322
|
+
};
|
|
323
|
+
const snap = snapshotFromResources([makeParsed(def)]);
|
|
324
|
+
const f = snap.resources[0].fields.find((f) => f.name === "created_at")!;
|
|
325
|
+
expect(f.default).toEqual({ kind: "sql", expr: "timezone('UTC', now())" });
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// --------------------------------------------------------------------
|
|
330
|
+
// Serialization + roundtrip
|
|
331
|
+
// --------------------------------------------------------------------
|
|
332
|
+
|
|
333
|
+
describe("serializeSnapshot / parseSnapshot", () => {
|
|
334
|
+
test("12. roundtrip preserves bytes", () => {
|
|
335
|
+
const snap = snapshotFromResources([makeParsed(pkPostgresUser())]);
|
|
336
|
+
const serialized = serializeSnapshot(snap);
|
|
337
|
+
const reparsed = parseSnapshot(serialized);
|
|
338
|
+
const reserialized = serializeSnapshot(reparsed);
|
|
339
|
+
expect(reserialized).toBe(serialized);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("13. serialization uses 2-space indent and sorted keys", () => {
|
|
343
|
+
const snap = snapshotFromResources([makeParsed(pkPostgresUser())]);
|
|
344
|
+
const json = serializeSnapshot(snap);
|
|
345
|
+
// 2-space indent → keys appear after " " on their own line.
|
|
346
|
+
expect(json).toContain(' "generatedAt":');
|
|
347
|
+
expect(json).toContain(' "provider":');
|
|
348
|
+
expect(json).toContain(' "resources":');
|
|
349
|
+
expect(json).toContain(' "version":');
|
|
350
|
+
// Sorted keys — "generatedAt" precedes "provider" precedes "resources"
|
|
351
|
+
// precedes "version" alphabetically.
|
|
352
|
+
const generatedIdx = json.indexOf('"generatedAt"');
|
|
353
|
+
const providerIdx = json.indexOf('"provider"');
|
|
354
|
+
const resourcesIdx = json.indexOf('"resources"');
|
|
355
|
+
const versionIdx = json.indexOf('"version"');
|
|
356
|
+
expect(generatedIdx).toBeLessThan(providerIdx);
|
|
357
|
+
expect(providerIdx).toBeLessThan(resourcesIdx);
|
|
358
|
+
expect(resourcesIdx).toBeLessThan(versionIdx);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("14. parseSnapshot rejects version: 2 with clear message", () => {
|
|
362
|
+
const v2 = JSON.stringify({
|
|
363
|
+
version: 2,
|
|
364
|
+
provider: "postgres",
|
|
365
|
+
resources: [],
|
|
366
|
+
generatedAt: new Date().toISOString(),
|
|
367
|
+
});
|
|
368
|
+
expect(() => parseSnapshot(v2)).toThrow(/Unsupported snapshot version/);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("15. parseSnapshot rejects malformed JSON", () => {
|
|
372
|
+
expect(() => parseSnapshot("{not-json")).toThrow(/Invalid snapshot JSON/);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("parseSnapshot rejects missing fields", () => {
|
|
376
|
+
expect(() =>
|
|
377
|
+
parseSnapshot(JSON.stringify({ version: 1, provider: "postgres" }))
|
|
378
|
+
).toThrow(/Snapshot.resources/);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("parseSnapshot rejects invalid provider", () => {
|
|
382
|
+
expect(() =>
|
|
383
|
+
parseSnapshot(JSON.stringify({ version: 1, provider: "oracle", resources: [], generatedAt: "now" }))
|
|
384
|
+
).toThrow(/invalid provider/);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// --------------------------------------------------------------------
|
|
389
|
+
// Hash stability
|
|
390
|
+
// --------------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
describe("hashSnapshot", () => {
|
|
393
|
+
test("16. hash is stable across object-key permutations (serialization is the canonicalizer)", () => {
|
|
394
|
+
const now = new Date("2026-01-01T00:00:00Z").toISOString();
|
|
395
|
+
// Same snapshot expressed two different ways — different key order.
|
|
396
|
+
const a = {
|
|
397
|
+
version: 1 as const,
|
|
398
|
+
provider: "postgres" as const,
|
|
399
|
+
generatedAt: now,
|
|
400
|
+
resources: [
|
|
401
|
+
{
|
|
402
|
+
name: "users",
|
|
403
|
+
fields: [
|
|
404
|
+
{ name: "id", type: "uuid" as const, nullable: false, primary: true, unique: false, indexed: false },
|
|
405
|
+
],
|
|
406
|
+
indexes: [],
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
};
|
|
410
|
+
const b = {
|
|
411
|
+
// Different key insertion order — must produce same hash.
|
|
412
|
+
provider: "postgres" as const,
|
|
413
|
+
resources: [
|
|
414
|
+
{
|
|
415
|
+
fields: [
|
|
416
|
+
{ unique: false, primary: true, nullable: false, name: "id", type: "uuid" as const, indexed: false },
|
|
417
|
+
],
|
|
418
|
+
name: "users",
|
|
419
|
+
indexes: [],
|
|
420
|
+
},
|
|
421
|
+
],
|
|
422
|
+
generatedAt: now,
|
|
423
|
+
version: 1 as const,
|
|
424
|
+
};
|
|
425
|
+
expect(hashSnapshot(a)).toBe(hashSnapshot(b));
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("hash is hex string of SHA-256 (64 chars)", () => {
|
|
429
|
+
const snap = snapshotFromResources([makeParsed(pkPostgresUser())]);
|
|
430
|
+
const h = hashSnapshot(snap);
|
|
431
|
+
expect(h).toMatch(/^[0-9a-f]{64}$/);
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// --------------------------------------------------------------------
|
|
436
|
+
// toSnakeCase — exported for reuse by Agent D generator
|
|
437
|
+
// --------------------------------------------------------------------
|
|
438
|
+
|
|
439
|
+
describe("toSnakeCase", () => {
|
|
440
|
+
test("camelCase", () => {
|
|
441
|
+
expect(toSnakeCase("passwordHash")).toBe("password_hash");
|
|
442
|
+
});
|
|
443
|
+
test("PascalCase", () => {
|
|
444
|
+
expect(toSnakeCase("PasswordHash")).toBe("password_hash");
|
|
445
|
+
});
|
|
446
|
+
test("runs of caps stay together", () => {
|
|
447
|
+
expect(toSnakeCase("HTTPRequestId")).toBe("http_request_id");
|
|
448
|
+
});
|
|
449
|
+
test("already snake is untouched", () => {
|
|
450
|
+
expect(toSnakeCase("password_hash")).toBe("password_hash");
|
|
451
|
+
});
|
|
452
|
+
test("single letter", () => {
|
|
453
|
+
expect(toSnakeCase("x")).toBe("x");
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// --------------------------------------------------------------------
|
|
458
|
+
// Indexes normalization sanity
|
|
459
|
+
// --------------------------------------------------------------------
|
|
460
|
+
|
|
461
|
+
describe("snapshotFromResources — indexes", () => {
|
|
462
|
+
test("index field keys resolve to snake_case column names", () => {
|
|
463
|
+
const def: ResourceDefinition = {
|
|
464
|
+
name: "user",
|
|
465
|
+
fields: {
|
|
466
|
+
id: { type: "uuid", required: true, primary: true } as any,
|
|
467
|
+
createdAt: { type: "date", required: true },
|
|
468
|
+
},
|
|
469
|
+
options: {
|
|
470
|
+
persistence: {
|
|
471
|
+
provider: "postgres",
|
|
472
|
+
indexes: [{ name: "idx_user_created", fields: ["createdAt"], unique: false } as DdlIndex],
|
|
473
|
+
},
|
|
474
|
+
} as any,
|
|
475
|
+
};
|
|
476
|
+
const snap = snapshotFromResources([makeParsed(def)]);
|
|
477
|
+
expect(snap.resources[0].indexes[0].fields).toEqual(["created_at"]);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test("duplicate index names throw", () => {
|
|
481
|
+
const def: ResourceDefinition = {
|
|
482
|
+
name: "user",
|
|
483
|
+
fields: {
|
|
484
|
+
id: { type: "uuid", required: true, primary: true } as any,
|
|
485
|
+
email: { type: "email", required: true },
|
|
486
|
+
},
|
|
487
|
+
options: {
|
|
488
|
+
persistence: {
|
|
489
|
+
provider: "postgres",
|
|
490
|
+
indexes: [
|
|
491
|
+
{ name: "dup", fields: ["email"], unique: false } as DdlIndex,
|
|
492
|
+
{ name: "dup", fields: ["id"], unique: true } as DdlIndex,
|
|
493
|
+
],
|
|
494
|
+
},
|
|
495
|
+
} as any,
|
|
496
|
+
};
|
|
497
|
+
expect(() => snapshotFromResources([makeParsed(def)])).toThrow(/Duplicate index name/);
|
|
498
|
+
});
|
|
499
|
+
});
|