@rawsql-ts/ztd-cli 0.14.4 → 0.16.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.
Files changed (130) hide show
  1. package/README.md +25 -11
  2. package/dist/commands/init.js +27 -10
  3. package/dist/commands/init.js.map +1 -1
  4. package/dist/commands/lint.d.ts +59 -0
  5. package/dist/commands/lint.js +338 -0
  6. package/dist/commands/lint.js.map +1 -0
  7. package/dist/index.js +2 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/utils/sqlLintHelpers.d.ts +18 -0
  10. package/dist/utils/sqlLintHelpers.js +270 -0
  11. package/dist/utils/sqlLintHelpers.js.map +1 -0
  12. package/package.json +11 -4
  13. package/templates/AGENTS.md +95 -53
  14. package/templates/README.md +45 -67
  15. package/templates/dist/drivers/pg-testkit/src/driver/PgTestkitClient.d.ts +38 -0
  16. package/templates/dist/drivers/pg-testkit/src/driver/PgTestkitClient.js +117 -0
  17. package/templates/dist/drivers/pg-testkit/src/driver/PgTestkitClient.js.map +1 -0
  18. package/templates/dist/drivers/pg-testkit/src/driver/createPgTestkitPool.d.ts +4 -0
  19. package/templates/dist/drivers/pg-testkit/src/driver/createPgTestkitPool.js +71 -0
  20. package/templates/dist/drivers/pg-testkit/src/driver/createPgTestkitPool.js.map +1 -0
  21. package/templates/dist/drivers/pg-testkit/src/index.d.ts +5 -0
  22. package/templates/dist/drivers/pg-testkit/src/index.js +11 -0
  23. package/templates/dist/drivers/pg-testkit/src/index.js.map +1 -0
  24. package/templates/dist/drivers/pg-testkit/src/proxy/wrapPgClient.d.ts +3 -0
  25. package/templates/dist/drivers/pg-testkit/src/proxy/wrapPgClient.js +79 -0
  26. package/templates/dist/drivers/pg-testkit/src/proxy/wrapPgClient.js.map +1 -0
  27. package/templates/dist/drivers/pg-testkit/src/types.d.ts +69 -0
  28. package/templates/dist/drivers/pg-testkit/src/types.js +3 -0
  29. package/templates/dist/drivers/pg-testkit/src/types.js.map +1 -0
  30. package/templates/dist/drivers/pg-testkit/src/utils/fixtureState.d.ts +15 -0
  31. package/templates/dist/drivers/pg-testkit/src/utils/fixtureState.js +34 -0
  32. package/templates/dist/drivers/pg-testkit/src/utils/fixtureState.js.map +1 -0
  33. package/templates/dist/drivers/pg-testkit/src/utils/fixtureValidation.d.ts +12 -0
  34. package/templates/dist/drivers/pg-testkit/src/utils/fixtureValidation.js +53 -0
  35. package/templates/dist/drivers/pg-testkit/src/utils/fixtureValidation.js.map +1 -0
  36. package/templates/dist/mapper-core/src/index.d.ts +160 -0
  37. package/templates/dist/mapper-core/src/index.js +637 -0
  38. package/templates/dist/mapper-core/src/index.js.map +1 -0
  39. package/templates/dist/testkit-core/src/errors/index.d.ts +49 -0
  40. package/templates/dist/testkit-core/src/errors/index.js +111 -0
  41. package/templates/dist/testkit-core/src/errors/index.js.map +1 -0
  42. package/templates/dist/testkit-core/src/fixtures/ColumnAffinity.d.ts +5 -0
  43. package/templates/dist/testkit-core/src/fixtures/ColumnAffinity.js +29 -0
  44. package/templates/dist/testkit-core/src/fixtures/ColumnAffinity.js.map +1 -0
  45. package/templates/dist/testkit-core/src/fixtures/DdlFixtureLoader.d.ts +37 -0
  46. package/templates/dist/testkit-core/src/fixtures/DdlFixtureLoader.js +182 -0
  47. package/templates/dist/testkit-core/src/fixtures/DdlFixtureLoader.js.map +1 -0
  48. package/templates/dist/testkit-core/src/fixtures/FixtureProvider.d.ts +20 -0
  49. package/templates/dist/testkit-core/src/fixtures/FixtureProvider.js +121 -0
  50. package/templates/dist/testkit-core/src/fixtures/FixtureProvider.js.map +1 -0
  51. package/templates/dist/testkit-core/src/fixtures/FixtureStore.d.ts +51 -0
  52. package/templates/dist/testkit-core/src/fixtures/FixtureStore.js +199 -0
  53. package/templates/dist/testkit-core/src/fixtures/FixtureStore.js.map +1 -0
  54. package/templates/dist/testkit-core/src/fixtures/TableDefinitionSchemaRegistry.d.ts +10 -0
  55. package/templates/dist/testkit-core/src/fixtures/TableDefinitionSchemaRegistry.js +28 -0
  56. package/templates/dist/testkit-core/src/fixtures/TableDefinitionSchemaRegistry.js.map +1 -0
  57. package/templates/dist/testkit-core/src/fixtures/TableNameResolver.d.ts +18 -0
  58. package/templates/dist/testkit-core/src/fixtures/TableNameResolver.js +80 -0
  59. package/templates/dist/testkit-core/src/fixtures/TableNameResolver.js.map +1 -0
  60. package/templates/dist/testkit-core/src/fixtures/ddlLint.d.ts +59 -0
  61. package/templates/dist/testkit-core/src/fixtures/ddlLint.js +489 -0
  62. package/templates/dist/testkit-core/src/fixtures/ddlLint.js.map +1 -0
  63. package/templates/dist/testkit-core/src/fixtures/naming.d.ts +1 -0
  64. package/templates/dist/testkit-core/src/fixtures/naming.js +6 -0
  65. package/templates/dist/testkit-core/src/fixtures/naming.js.map +1 -0
  66. package/templates/dist/testkit-core/src/index.d.ts +17 -0
  67. package/templates/dist/testkit-core/src/index.js +47 -0
  68. package/templates/dist/testkit-core/src/index.js.map +1 -0
  69. package/templates/dist/testkit-core/src/logger/NoopLogger.d.ts +8 -0
  70. package/templates/dist/testkit-core/src/logger/NoopLogger.js +16 -0
  71. package/templates/dist/testkit-core/src/logger/NoopLogger.js.map +1 -0
  72. package/templates/dist/testkit-core/src/provider/TestkitProvider.d.ts +57 -0
  73. package/templates/dist/testkit-core/src/provider/TestkitProvider.js +149 -0
  74. package/templates/dist/testkit-core/src/provider/TestkitProvider.js.map +1 -0
  75. package/templates/dist/testkit-core/src/rewriter/ResultSelectRewriter.d.ts +43 -0
  76. package/templates/dist/testkit-core/src/rewriter/ResultSelectRewriter.js +473 -0
  77. package/templates/dist/testkit-core/src/rewriter/ResultSelectRewriter.js.map +1 -0
  78. package/templates/dist/testkit-core/src/rewriter/SelectAnalyzer.d.ts +9 -0
  79. package/templates/dist/testkit-core/src/rewriter/SelectAnalyzer.js +38 -0
  80. package/templates/dist/testkit-core/src/rewriter/SelectAnalyzer.js.map +1 -0
  81. package/templates/dist/testkit-core/src/rewriter/SelectFixtureRewriter.d.ts +42 -0
  82. package/templates/dist/testkit-core/src/rewriter/SelectFixtureRewriter.js +298 -0
  83. package/templates/dist/testkit-core/src/rewriter/SelectFixtureRewriter.js.map +1 -0
  84. package/templates/dist/testkit-core/src/sql/SqliteValuesBuilder.d.ts +12 -0
  85. package/templates/dist/testkit-core/src/sql/SqliteValuesBuilder.js +63 -0
  86. package/templates/dist/testkit-core/src/sql/SqliteValuesBuilder.js.map +1 -0
  87. package/templates/dist/testkit-core/src/types/index.d.ts +69 -0
  88. package/templates/dist/testkit-core/src/types/index.js +3 -0
  89. package/templates/dist/testkit-core/src/types/index.js.map +1 -0
  90. package/templates/dist/testkit-core/src/utils/queryHelpers.d.ts +28 -0
  91. package/templates/dist/testkit-core/src/utils/queryHelpers.js +81 -0
  92. package/templates/dist/testkit-core/src/utils/queryHelpers.js.map +1 -0
  93. package/templates/dist/writer-core/src/index.d.ts +34 -0
  94. package/templates/dist/writer-core/src/index.js +115 -0
  95. package/templates/dist/writer-core/src/index.js.map +1 -0
  96. package/templates/dist/ztd-cli/templates/src/db/sql-client.d.ts +20 -0
  97. package/templates/dist/ztd-cli/templates/src/db/sql-client.js +3 -0
  98. package/templates/dist/ztd-cli/templates/src/db/sql-client.js.map +1 -0
  99. package/templates/dist/ztd-cli/templates/src/repositories/user-accounts.d.ts +36 -0
  100. package/templates/dist/ztd-cli/templates/src/repositories/user-accounts.js +85 -0
  101. package/templates/dist/ztd-cli/templates/src/repositories/user-accounts.js.map +1 -0
  102. package/templates/dist/ztd-cli/templates/tests/generated/ztd-row-map.generated.d.ts +20 -0
  103. package/templates/dist/ztd-cli/templates/tests/generated/ztd-row-map.generated.js +33 -0
  104. package/templates/dist/ztd-cli/templates/tests/generated/ztd-row-map.generated.js.map +1 -0
  105. package/templates/dist/ztd-cli/templates/tests/support/global-setup.d.ts +10 -0
  106. package/templates/dist/ztd-cli/templates/tests/support/global-setup.js +29 -0
  107. package/templates/dist/ztd-cli/templates/tests/support/global-setup.js.map +1 -0
  108. package/templates/dist/ztd-cli/templates/tests/support/testkit-client.d.ts +66 -0
  109. package/templates/dist/ztd-cli/templates/tests/support/testkit-client.js +552 -0
  110. package/templates/dist/ztd-cli/templates/tests/support/testkit-client.js.map +1 -0
  111. package/templates/dist/ztd-cli/templates/tests/user-profiles.test.d.ts +1 -0
  112. package/templates/dist/ztd-cli/templates/tests/user-profiles.test.js +82 -0
  113. package/templates/dist/ztd-cli/templates/tests/user-profiles.test.js.map +1 -0
  114. package/templates/dist/ztd-cli/templates/tests/writer-constraints.test.d.ts +1 -0
  115. package/templates/dist/ztd-cli/templates/tests/writer-constraints.test.js +29 -0
  116. package/templates/dist/ztd-cli/templates/tests/writer-constraints.test.js.map +1 -0
  117. package/templates/dist/ztd-cli/templates/tests/ztd-layout.generated.d.ts +7 -0
  118. package/templates/dist/ztd-cli/templates/tests/ztd-layout.generated.js +10 -0
  119. package/templates/dist/ztd-cli/templates/tests/ztd-layout.generated.js.map +1 -0
  120. package/templates/src/db/sql-client.ts +1 -1
  121. package/templates/src/repositories/user-accounts.ts +179 -0
  122. package/templates/tests/AGENTS.md +59 -6
  123. package/templates/tests/support/global-setup.ts +1 -1
  124. package/templates/tests/support/testkit-client.ts +4 -4
  125. package/templates/tests/user-profiles.test.ts +161 -0
  126. package/templates/tests/writer-constraints.test.ts +32 -0
  127. package/templates/tests/ztd-layout.generated.ts +0 -2
  128. package/templates/tsconfig.json +1 -2
  129. package/templates/ztd/AGENTS.md +10 -85
  130. package/templates/ztd/README.md +10 -79
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-profiles.test.js","sourceRoot":"","sources":["../../../../tests/user-profiles.test.ts"],"names":[],"mappings":";;AAAA,mCAAgD;AAEhD,6EAI2C;AAC3C,6DAA+D;AAC/D,qEAAqE;AAErE,SAAS,iBAAiB;IACxB,OAAO;QACL;YACE,eAAe,EAAE,CAAC;YAClB,QAAQ,EAAE,OAAO;YACjB,KAAK,EAAE,mBAAmB;YAC1B,YAAY,EAAE,cAAc;YAC5B,UAAU,EAAE,sBAAsB;YAClC,UAAU,EAAE,sBAAsB;SACnC;QACD;YACE,eAAe,EAAE,CAAC;YAClB,QAAQ,EAAE,OAAO;YACjB,KAAK,EAAE,mBAAmB;YAC1B,YAAY,EAAE,eAAe;YAC7B,UAAU,EAAE,sBAAsB;YAClC,UAAU,EAAE,sBAAsB;SACnC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB;IACxB,OAAO;QACL;YACE,UAAU,EAAE,GAAG;YACf,eAAe,EAAE,CAAC;YAClB,GAAG,EAAE,oCAAoC;YACzC,OAAO,EAAE,qBAAqB;YAC9B,QAAQ,EAAE,IAAI;SACf;KACF,CAAC;AACJ,CAAC;AAED,SAAS,aAAa;IACpB,OAAO;QACL,IAAA,oCAAY,EACV,qBAAqB,EACrB,iBAAiB,EAAE,EACnB,oCAAY,CAAC,qBAAqB,CAAC,CACpC;QACD,IAAA,oCAAY,EACV,qBAAqB,EACrB,iBAAiB,EAAE,EACnB,oCAAY,CAAC,qBAAqB,CAAC,CACpC;KACF,CAAC;AACJ,CAAC;AAED,IAAA,iBAAQ,EAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,IAAA,aAAI,EAAC,4CAA4C,EAAE,KAAK,IAAI,EAAE;QAC5D,MAAM,QAAQ,GAAG,aAAa,EAAE,CAAC;QACjC,MAAM,MAAM,GAAG,MAAM,IAAA,oCAAmB,EAAC,QAAQ,CAAC,CAAC;QACnD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAA,gCAAgB,EAAC,MAAM,CAAC,CAAC;YAC9C,IAAA,eAAM,EAAC,MAAM,CAAC,CAAC,OAAO,CAAC;gBACrB;oBACE,aAAa,EAAE,CAAC;oBAChB,QAAQ,EAAE,OAAO;oBACjB,KAAK,EAAE,mBAAmB;oBAC1B,WAAW,EAAE,cAAc;oBAC3B,SAAS,EAAE,IAAI,IAAI,CAAC,sBAAsB,CAAC;oBAC3C,SAAS,EAAE,IAAI,IAAI,CAAC,sBAAsB,CAAC;oBAC3C,OAAO,EAAE;wBACP,SAAS,EAAE,GAAG;wBACd,aAAa,EAAE,CAAC;wBAChB,GAAG,EAAE,oCAAoC;wBACzC,OAAO,EAAE,qBAAqB;wBAC9B,QAAQ,EAAE,IAAI;qBACf;iBACF;gBACD;oBACE,aAAa,EAAE,CAAC;oBAChB,QAAQ,EAAE,OAAO;oBACjB,KAAK,EAAE,mBAAmB;oBAC1B,WAAW,EAAE,eAAe;oBAC5B,SAAS,EAAE,IAAI,IAAI,CAAC,sBAAsB,CAAC;oBAC3C,SAAS,EAAE,IAAI,IAAI,CAAC,sBAAsB,CAAC;oBAC3C,OAAO,EAAE,SAAS;iBACnB;aACF,CAAC,CAAC;QACL,CAAC;gBAAS,CAAC;YACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACvB,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const ztd_row_map_generated_1 = require("./generated/ztd-row-map.generated");
5
+ const user_accounts_1 = require("../src/repositories/user-accounts");
6
+ const userColumns = new Set(Object.keys(ztd_row_map_generated_1.tableSchemas['public.user_account'].columns));
7
+ (0, vitest_1.describe)('user_account writer columns', () => {
8
+ (0, vitest_1.test)('insert columns must exist on the canonical table', () => {
9
+ const { insertColumns } = user_accounts_1.userAccountWriterColumnSets;
10
+ const missing = insertColumns.filter((column) => !userColumns.has(column));
11
+ (0, vitest_1.expect)(missing).toEqual([]);
12
+ (0, vitest_1.expect)(insertColumns).toEqual(vitest_1.expect.arrayContaining(['username', 'email', 'display_name']));
13
+ });
14
+ (0, vitest_1.test)('update columns stay within allowed set and avoid immutable columns', () => {
15
+ const { updateColumns, immutableColumns } = user_accounts_1.userAccountWriterColumnSets;
16
+ const missing = updateColumns.filter((column) => !userColumns.has(column));
17
+ (0, vitest_1.expect)(missing).toEqual([]);
18
+ (0, vitest_1.expect)(updateColumns).not.toEqual(vitest_1.expect.arrayContaining([...immutableColumns]));
19
+ });
20
+ (0, vitest_1.test)('immutable columns reflect the DDL and are not targetted by updates', () => {
21
+ const { immutableColumns, updateColumns } = user_accounts_1.userAccountWriterColumnSets;
22
+ const missing = immutableColumns.filter((column) => !userColumns.has(column));
23
+ (0, vitest_1.expect)(missing).toEqual([]);
24
+ immutableColumns.forEach((column) => {
25
+ (0, vitest_1.expect)(updateColumns).not.toContain(column);
26
+ });
27
+ });
28
+ });
29
+ //# sourceMappingURL=writer-constraints.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writer-constraints.test.js","sourceRoot":"","sources":["../../../../tests/writer-constraints.test.ts"],"names":[],"mappings":";;AAAA,mCAAgD;AAChD,6EAAiE;AACjE,qEAAgF;AAEhF,MAAM,WAAW,GAAG,IAAI,GAAG,CACzB,MAAM,CAAC,IAAI,CAAC,oCAAY,CAAC,qBAAqB,CAAC,CAAC,OAAO,CAAC,CACzD,CAAC;AAEF,IAAA,iBAAQ,EAAC,6BAA6B,EAAE,GAAG,EAAE;IAC3C,IAAA,aAAI,EAAC,kDAAkD,EAAE,GAAG,EAAE;QAC5D,MAAM,EAAE,aAAa,EAAE,GAAG,2CAA2B,CAAC;QACtD,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;QAC3E,IAAA,eAAM,EAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC5B,IAAA,eAAM,EAAC,aAAa,CAAC,CAAC,OAAO,CAC3B,eAAM,CAAC,eAAe,CAAC,CAAC,UAAU,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC,CAC9D,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,IAAA,aAAI,EAAC,oEAAoE,EAAE,GAAG,EAAE;QAC9E,MAAM,EAAE,aAAa,EAAE,gBAAgB,EAAE,GAAG,2CAA2B,CAAC;QACxE,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;QAC3E,IAAA,eAAM,EAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC5B,IAAA,eAAM,EAAC,aAAa,CAAC,CAAC,GAAG,CAAC,OAAO,CAC/B,eAAM,CAAC,eAAe,CAAC,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAC9C,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,IAAA,aAAI,EAAC,oEAAoE,EAAE,GAAG,EAAE;QAC9E,MAAM,EAAE,gBAAgB,EAAE,aAAa,EAAE,GAAG,2CAA2B,CAAC;QACxE,MAAM,OAAO,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;QAC9E,IAAA,eAAM,EAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC5B,gBAAgB,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE;YAClC,IAAA,eAAM,EAAC,aAAa,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAC9C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,7 @@
1
+ declare const _default: {
2
+ ztdRootDir: string;
3
+ ddlDir: string;
4
+ enumsDir: string;
5
+ domainSpecsDir: string;
6
+ };
7
+ export default _default;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ // GENERATED FILE. DO NOT EDIT.
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.default = {
5
+ ztdRootDir: 'ztd',
6
+ ddlDir: 'ztd/ddl',
7
+ enumsDir: 'ztd/enums',
8
+ domainSpecsDir: 'ztd/domain-specs',
9
+ };
10
+ //# sourceMappingURL=ztd-layout.generated.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ztd-layout.generated.js","sourceRoot":"","sources":["../../../../tests/ztd-layout.generated.ts"],"names":[],"mappings":";AAAA,+BAA+B;;AAE/B,kBAAe;IACb,UAAU,EAAE,KAAK;IACjB,MAAM,EAAE,SAAS;IACjB,QAAQ,EAAE,WAAW;IACrB,cAAc,EAAE,kBAAkB;CACnC,CAAC"}
@@ -10,7 +10,7 @@ export type SqlQueryRows<T> = Promise<T[]>;
10
10
  * Minimal SQL client interface required by the repository layer.
11
11
  *
12
12
  * - Production: adapt `pg` (or other drivers) to normalize results into `T[]`
13
- * - Tests: compatible with `pg-testkit` clients returned by `createTestkitClient()`
13
+ * - Tests: compatible with the `testkit-postgres` pipeline exposed by `@rawsql-ts/adapter-node-pg` clients returned by `createTestkitClient()`
14
14
  *
15
15
  * Connection strategy note:
16
16
  * - Prefer a shared client per worker process for performance.
@@ -0,0 +1,179 @@
1
+ import type { SqlClient } from '../db/sql-client';
2
+ import { createMapper, entity, toRowsExecutor } from '@rawsql-ts/mapper-core';
3
+ import { insert, Key, remove, update } from '@rawsql-ts/writer-core';
4
+
5
+ const userAccountTable = 'public.user_account';
6
+
7
+ type UserProfileRow = {
8
+ profileId: number;
9
+ userAccountId: number;
10
+ bio: string | null;
11
+ website: string | null;
12
+ verified: boolean;
13
+ };
14
+
15
+ /**
16
+ * DTO that represents a user account with its optional profile information.
17
+ * @property {number} userAccountId The primary key for the user account.
18
+ * @property {string} username The canonical username.
19
+ * @property {string} email The account email address.
20
+ * @property {string} displayName The account display name.
21
+ * @property {Date} createdAt When the account was created.
22
+ * @property {Date} updatedAt When the account was last updated.
23
+ * @property {UserProfileRow} [profile] Optional profile payload joined from user_profile.
24
+ */
25
+ export type UserAccountWithProfile = {
26
+ userAccountId: number;
27
+ username: string;
28
+ email: string;
29
+ displayName: string;
30
+ createdAt: Date;
31
+ updatedAt: Date;
32
+ profile?: UserProfileRow;
33
+ };
34
+
35
+ // Map the joined profile columns so we can hydrate nested objects later.
36
+ const profileMapping = entity<UserProfileRow>({
37
+ name: 'userProfile',
38
+ key: 'profileId',
39
+ columnMap: {
40
+ profileId: 'profile_id',
41
+ userAccountId: 'profile_user_account_id',
42
+ bio: 'bio',
43
+ website: 'website',
44
+ verified: 'verified',
45
+ },
46
+ });
47
+
48
+ const userAccountMapping = entity<UserAccountWithProfile>({
49
+ name: 'userAccount',
50
+ key: 'userAccountId',
51
+ columnMap: {
52
+ userAccountId: 'user_account_id',
53
+ username: 'username',
54
+ email: 'email',
55
+ displayName: 'display_name',
56
+ createdAt: 'created_at',
57
+ updatedAt: 'updated_at',
58
+ },
59
+ }).belongsTo('profile', profileMapping, 'userAccountId', { optional: true });
60
+
61
+ const userProfilesSql = `
62
+ SELECT
63
+ u.user_account_id,
64
+ u.username,
65
+ u.email,
66
+ u.display_name,
67
+ u.created_at,
68
+ u.updated_at,
69
+ p.profile_id,
70
+ p.user_account_id AS profile_user_account_id,
71
+ p.bio,
72
+ p.website,
73
+ p.verified
74
+ FROM public.user_account u
75
+ LEFT JOIN public.user_profile p ON p.user_account_id = u.user_account_id
76
+ ORDER BY u.user_account_id, p.profile_id;
77
+ `;
78
+
79
+ // Build a mapper that can translate snake_case columns into camelCase DTOs.
80
+ const createMapperForClient = (client: SqlClient) =>
81
+ createMapper(
82
+ toRowsExecutor((sql, params: unknown[] = []) =>
83
+ client.query<Record<string, unknown>>(sql, params),
84
+ ),
85
+ {
86
+ // The explicit column maps enumerate the nested entity columns while keyTransform handles generic snake_to_camel conversions.
87
+ keyTransform: 'snake_to_camel',
88
+ coerceDates: true,
89
+ },
90
+ );
91
+
92
+ /**
93
+ * Queries all user accounts together with their associated profiles.
94
+ * @param {SqlClient} client Client proxy that executes the mapper SQL.
95
+ * @returns {Promise<UserAccountWithProfile[]>} The joined account-with-profile rows.
96
+ */
97
+ export async function listUserProfiles(
98
+ client: SqlClient,
99
+ ): Promise<UserAccountWithProfile[]> {
100
+ const mapper = createMapperForClient(client);
101
+ return mapper.query(userProfilesSql, [], userAccountMapping);
102
+ }
103
+
104
+ /**
105
+ * Parameters required to insert a new user account.
106
+ * @property {string} username The requested username.
107
+ * @property {string} email The requested email address.
108
+ * @property {string} displayName The requested display name.
109
+ */
110
+ export type NewUserAccount = {
111
+ username: string;
112
+ email: string;
113
+ displayName: string;
114
+ };
115
+
116
+ /**
117
+ * Payload describing the display name change for an existing account.
118
+ * @property {string} displayName The new display name to persist.
119
+ */
120
+ export type DisplayNameUpdatePayload = {
121
+ displayName: string;
122
+ };
123
+
124
+ /**
125
+ * Builds an insert statement for the user_account writer.
126
+ * @param {NewUserAccount} input The normalized fields for the new account.
127
+ * @returns {ReturnType<typeof insert>} A well-formed insert statement for the user_account writer.
128
+ */
129
+ export function buildInsertUserAccount(
130
+ input: NewUserAccount,
131
+ ): ReturnType<typeof insert> {
132
+ return insert(userAccountTable, {
133
+ username: input.username,
134
+ email: input.email,
135
+ display_name: input.displayName,
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Builds an update statement that refreshes the display name and timestamp.
141
+ * @param {Key} key The unique key identifying the row to update.
142
+ * @param {DisplayNameUpdatePayload} payload The new display name payload.
143
+ * @returns {ReturnType<typeof update>} A writer update statement that refreshes the display name and updated_at timestamp.
144
+ */
145
+ export function buildUpdateDisplayName(
146
+ key: Key,
147
+ payload: DisplayNameUpdatePayload,
148
+ ): ReturnType<typeof update> {
149
+ return update(
150
+ userAccountTable,
151
+ {
152
+ // Persist the new display name and bump the timestamp along with it.
153
+ display_name: payload.displayName,
154
+ updated_at: new Date(),
155
+ },
156
+ key,
157
+ );
158
+ }
159
+
160
+ /**
161
+ * Builds a delete statement for the specified user account key.
162
+ * @param {Key} key Identifies the row to remove.
163
+ * @returns {ReturnType<typeof remove>} A writer delete statement for the matching user account.
164
+ */
165
+ export function buildRemoveUserAccount(key: Key): ReturnType<typeof remove> {
166
+ return remove(userAccountTable, key);
167
+ }
168
+
169
+ /**
170
+ * Column sets that writer tests use to ensure only approved columns are touched.
171
+ * @property {readonly string[]} insertColumns Columns allowed for new account inserts.
172
+ * @property {readonly string[]} updateColumns Columns permitted during updates.
173
+ * @property {readonly string[]} immutableColumns Columns that must remain unchanged.
174
+ */
175
+ export const userAccountWriterColumnSets = {
176
+ insertColumns: ['username', 'email', 'display_name'] as const,
177
+ updateColumns: ['display_name', 'updated_at'] as const,
178
+ immutableColumns: ['user_account_id', 'created_at'] as const,
179
+ };
@@ -8,9 +8,21 @@ Tests are a shared workspace for humans and AI, but must respect ZTD-specific co
8
8
  ## Default execution mode (important)
9
9
 
10
10
  - The default execution mode for tests is **ZTD**.
11
+ - ZTD tests run through the new `createTestkitProvider` helper, which keeps a single
12
+ shared backend connection per worker and wraps each scenario in a rollback
13
+ transaction by default.
11
14
  - Do not switch to Traditional mode unless explicitly instructed by a human.
12
15
  - Do not set `ZTD_EXECUTION_MODE=traditional` or pass `{ mode: "traditional" }` to `createTestkitClient()` without explicit instruction.
16
+ - When you need session-level changes (temporary tables, `SET` commands, etc.), opt
17
+ into per-test isolation via `provider.perTest()` or by passing `{ strategy:
18
+ 'perTest' }` to `provider.withRepositoryFixture`. Shared mode otherwise keeps the
19
+ default behavior fast and deterministic.
13
20
  - If instructions are ambiguous, assume ZTD mode and proceed.
21
+ - Tests MUST NOT mix ZTD mode and Traditional mode within the same test suite.
22
+ - Traditional mode is reserved for integration-style validation; it is not a convenience or performance workaround.
23
+ - Every repository/test change must be followed by `pnpm --filter <package> test` (substitute the real template package name).
24
+ - `tests/support/global-setup.ts` already uses `@testcontainers/postgresql`, so a disposable Postgres container spins up automatically whenever `DATABASE_URL` is absent.
25
+ - If the suite fails, resolve the failure and rerun until it succeeds before considering the change complete.
14
26
 
15
27
  ---
16
28
 
@@ -37,6 +49,8 @@ If a required type is missing:
37
49
  - Export the correct type from `src/`.
38
50
 
39
51
  Do not invent substitute models.
52
+ - Tests MUST treat `tests/generated/ztd-row-map.generated.ts` as the single source of truth for table-shaped rows.
53
+ - If a column exists in the database but not in the row map, the schema is considered outdated until regenerated.
40
54
 
41
55
  ---
42
56
 
@@ -58,6 +72,8 @@ Preferred patterns:
58
72
  If behavior depends on transactions, isolation, or shared mutable state:
59
73
  - That test does not belong in ZTD unit tests.
60
74
  - Move it to an integration test and explicitly request Traditional mode.
75
+ - A single repository method call is the maximum scope of observation in ZTD tests.
76
+ - Tests that validate cross-call effects, workflows, or lifecycle transitions do not belong in ZTD tests.
61
77
 
62
78
  ---
63
79
 
@@ -67,23 +83,51 @@ If behavior depends on transactions, isolation, or shared mutable state:
67
83
  - Keep fixtures minimal and intention-revealing.
68
84
  - Do not add rows or columns unrelated to the test intent.
69
85
  - Do not simulate application-side logic in fixtures.
86
+ - Fixtures must satisfy non-nullable columns and required constraints derived from DDL.
87
+ - Fixtures MUST NOT encode business rules, defaults, or derived values.
88
+ - Fixtures exist only to satisfy schema constraints and make SQL executable.
70
89
 
71
90
  ---
72
91
 
73
92
  ## Assertions (important)
74
93
 
75
94
  - Assert only on relevant fields.
76
- - Do not assert implicit ordering.
95
+ - Do not assert implicit ordering unless the repository contract explicitly guarantees it
96
+ (e.g. the query includes a defined `ORDER BY`).
77
97
  - Do not assert specific values of auto-generated IDs.
78
- - Assert existence, type, or relative differences instead.
98
+ - Assert existence, type, cardinality, or relative differences instead.
79
99
 
80
100
  ---
81
101
 
82
- ## Repository boundaries
102
+ ## Repository boundaries (important)
83
103
 
84
104
  - Tests should verify observable behavior of repository methods.
85
105
  - Do not duplicate SQL logic or business rules inside tests.
86
106
  - Do not test internal helper functions or private implementation details.
107
+ - Tests must match the repository method contract exactly
108
+ (return type, nullability, and error behavior).
109
+ - Tests MUST NOT compensate for mapper or writer limitations by reimplementing logic in the test itself.
110
+
111
+ ---
112
+
113
+ ## Test helper and resource lifecycle (important)
114
+
115
+ - Any test helper that creates a client, connection, or testkit instance
116
+ **must guarantee cleanup**.
117
+ - Always close resources using `try/finally` or a dedicated helper
118
+ (e.g. `withRepository`).
119
+ - Do not rely on test success paths to release resources.
120
+
121
+ ---
122
+
123
+ ## Test file conventions (important)
124
+
125
+ - Do not assume Vitest globals are available.
126
+ - Explicitly import `describe`, `it`, and `expect` from `vitest`
127
+ unless the project explicitly documents global usage.
128
+ - Avoid implicit `any` in tests and helpers.
129
+ - Explicitly type fixtures and helper parameters
130
+ (e.g. `Parameters<typeof createTestkitClient>[0]`).
87
131
 
88
132
  ---
89
133
 
@@ -93,7 +137,9 @@ If behavior depends on transactions, isolation, or shared mutable state:
93
137
  - However:
94
138
  - Do not redefine models.
95
139
  - Do not change schema assumptions.
96
- - Do not edit `ztd/ddl`, `ztd/domain-specs`, or `ztd/enums` from tests.
140
+ - Do not edit `ztd/ddl`. `ztd/ddl` is the only authoritative `ztd` directory; do not create or assume additional `ztd` subdirectories without explicit instruction.
141
+ - The only authoritative source for tests is `ztd/ddl`.
142
+ - Tests may fail when `ztd/` definitions change; tests MUST be updated to reflect those changes, not the other way around.
97
143
 
98
144
  ---
99
145
 
@@ -105,10 +151,17 @@ If behavior depends on transactions, isolation, or shared mutable state:
105
151
 
106
152
  ---
107
153
 
154
+ ## Writer metadata guardrail (template-specific)
155
+
156
+ - `tests/writer-constraints.test.ts` reads `userAccountWriterColumnSets` together with `tests/generated/ztd-row-map.generated.ts` to ensure every writer column actually exists on `public.user_account`.
157
+ - Run `npx ztd ztd-config` before executing the template tests so the generated row map reflects your current schema changes.
158
+ - When new columns appear in the writer helpers, adjust `userAccountWriterColumnSets`, rerun the row-map generator, and update the constraint test expectations.
159
+ - These tests exist to enforce column correctness at test time so writer helpers remain runtime-safe and schema-agnostic.
160
+
108
161
  ## Guiding principle
109
162
 
110
- ZTD tests exist to validate **SQL semantics in isolation**.
111
- They are not integration tests, migration tests, or transaction tests.
163
+ ZTD tests exist to validate **repository behavior derived from SQL semantics in isolation**.
164
+ They are not integration tests, migration tests, or transaction tests.
112
165
 
113
166
  Prefer:
114
167
  - Clear intent
@@ -3,7 +3,7 @@ import { PostgreSqlContainer } from '@testcontainers/postgresql';
3
3
  /**
4
4
  * Vitest global setup.
5
5
  *
6
- * ZTD tests are safe to run in parallel against a single Postgres instance because pg-testkit
6
+ * ZTD tests are safe to run in parallel against a single Postgres instance because testkit-postgres
7
7
  * rewrites CRUD into fixture-backed SELECT queries (no physical tables are created/mutated).
8
8
  *
9
9
  * This setup starts exactly one disposable Postgres container when DATABASE_URL is not provided,
@@ -1,13 +1,13 @@
1
1
  // ZTD testkit helper - AUTO GENERATED
2
- // ztd-cli emits this file during project bootstrapping to wire pg-testkit.
2
+ // ztd-cli emits this file during project bootstrapping to wire @rawsql-ts/adapter-node-pg adapters.
3
3
  // Regenerate via npx ztd init (choose overwrite when prompted); avoid manual edits.
4
4
 
5
5
  import { existsSync, promises as fsPromises } from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { Client, types } from 'pg';
8
8
  import type { ClientConfig, QueryResultRow } from 'pg';
9
- import { createPgTestkitClient } from '@rawsql-ts/pg-testkit';
10
- import type { PgQueryInput, PgQueryable } from '@rawsql-ts/pg-testkit';
9
+ import { createPgTestkitClient } from '@rawsql-ts/adapter-node-pg';
10
+ import type { PgQueryInput, PgQueryable } from '@rawsql-ts/adapter-node-pg';
11
11
  import type { TableFixture } from '@rawsql-ts/testkit-core';
12
12
 
13
13
  const ddlDirectories = [path.resolve(__dirname, '../../ztd/ddl')];
@@ -172,7 +172,7 @@ async function getPgQueryable(): Promise<PgQueryable> {
172
172
 
173
173
  const client = await getPgClient();
174
174
 
175
- // Wrap the pg.Client to expose only the subset needed by pg-testkit.
175
+ // Wrap the pg.Client to expose only the subset needed by @rawsql-ts/adapter-node-pg.
176
176
  const wrappedQueryable: PgQueryable = {
177
177
  query: <T extends QueryResultRow>(textOrConfig: PgQueryInput, values?: unknown[]) =>
178
178
  client.query<T>(textOrConfig as never, values),
@@ -0,0 +1,161 @@
1
+ import { describe, expect, test, afterAll } from 'vitest';
2
+ import type { TableFixture, TestkitProvider } from '@rawsql-ts/testkit-core';
3
+ import { createTestkitProvider } from '@rawsql-ts/testkit-core';
4
+ import { createPgTestkitClient } from '@rawsql-ts/pg-testkit';
5
+ import { Pool } from 'pg';
6
+ import path from 'node:path';
7
+ import {
8
+ tableFixture,
9
+ tableSchemas,
10
+ TestRowMap,
11
+ } from './generated/ztd-row-map.generated';
12
+ import { listUserProfiles } from '../src/repositories/user-accounts';
13
+
14
+ const ddlDirectories = [path.resolve(__dirname, '../ztd/ddl')];
15
+ const skipReason = 'DATABASE_URL is not configured';
16
+ const configuredDatabaseUrl = process.env.DATABASE_URL?.trim();
17
+ const suiteTitle = configuredDatabaseUrl
18
+ ? 'user profile mapper'
19
+ : `user profile mapper (skipped: ${skipReason})`;
20
+ const describeUserProfile = configuredDatabaseUrl
21
+ ? describe
22
+ : (describe.skip as typeof describe);
23
+ let pool: Pool | undefined;
24
+ let providerPromise: Promise<TestkitProvider> | undefined;
25
+
26
+ // Lazily initialize the test provider so missing DATABASE_URL values do not trigger side effects.
27
+ function getProvider(): Promise<TestkitProvider> {
28
+ if (providerPromise) {
29
+ return providerPromise;
30
+ }
31
+
32
+ const databaseUrl = process.env.DATABASE_URL?.trim();
33
+ if (!databaseUrl) {
34
+ throw new Error(
35
+ 'Cannot initialize the repository testkit provider without DATABASE_URL.',
36
+ );
37
+ }
38
+
39
+ pool = new Pool({ connectionString: databaseUrl });
40
+ const activePool = pool;
41
+ providerPromise = createTestkitProvider({
42
+ connectionFactory: () => activePool.connect(),
43
+ resourceFactory: async (connection, fixtures) =>
44
+ createPgTestkitClient({
45
+ connectionFactory: () => connection,
46
+ tableRows: fixtures,
47
+ ddl: { directories: ddlDirectories },
48
+ }),
49
+ releaseResource: async (client) => {
50
+ await client.close();
51
+ },
52
+ disposeConnection: async (connection) => {
53
+ if (typeof connection.release === 'function') {
54
+ connection.release();
55
+ return;
56
+ }
57
+ if (typeof connection.end === 'function') {
58
+ await connection.end();
59
+ }
60
+ },
61
+ });
62
+
63
+ return providerPromise;
64
+ }
65
+
66
+ afterAll(async () => {
67
+ // Close resources only when initialization actually happened.
68
+ if (!providerPromise) {
69
+ return;
70
+ }
71
+
72
+ const provider = await providerPromise;
73
+ await provider.close();
74
+ if (pool) {
75
+ await pool.end();
76
+ }
77
+ });
78
+
79
+ function buildUserAccounts(): TestRowMap['public.user_account'][] {
80
+ return [
81
+ {
82
+ user_account_id: 1,
83
+ username: 'alpha',
84
+ email: 'alpha@example.com',
85
+ display_name: 'Alpha Tester',
86
+ created_at: '2025-12-01T08:00:00Z',
87
+ updated_at: '2025-12-01T09:00:00Z',
88
+ },
89
+ {
90
+ user_account_id: 2,
91
+ username: 'bravo',
92
+ email: 'bravo@example.com',
93
+ display_name: 'Bravo Builder',
94
+ created_at: '2025-12-02T10:00:00Z',
95
+ updated_at: '2025-12-02T11:00:00Z',
96
+ },
97
+ ];
98
+ }
99
+
100
+ function buildUserProfiles(): TestRowMap['public.user_profile'][] {
101
+ return [
102
+ {
103
+ profile_id: 101,
104
+ user_account_id: 1,
105
+ bio: 'Lead engineer and mapper advocate.',
106
+ website: 'https://example.com',
107
+ verified: true,
108
+ },
109
+ ];
110
+ }
111
+
112
+ function buildFixtures(): TableFixture[] {
113
+ return [
114
+ tableFixture(
115
+ 'public.user_account',
116
+ buildUserAccounts(),
117
+ tableSchemas['public.user_account'],
118
+ ),
119
+ tableFixture(
120
+ 'public.user_profile',
121
+ buildUserProfiles(),
122
+ tableSchemas['public.user_profile'],
123
+ ),
124
+ ];
125
+ }
126
+
127
+ describeUserProfile(suiteTitle, () => {
128
+ test('listUserProfiles hydrates optional profiles', async () => {
129
+ const fixtures = buildFixtures();
130
+ const provider = await getProvider();
131
+ await provider.withRepositoryFixture(fixtures, async (client) => {
132
+ const result = await listUserProfiles(client);
133
+ expect(result).toEqual([
134
+ {
135
+ userAccountId: 1,
136
+ username: 'alpha',
137
+ email: 'alpha@example.com',
138
+ displayName: 'Alpha Tester',
139
+ createdAt: new Date('2025-12-01T08:00:00Z'),
140
+ updatedAt: new Date('2025-12-01T09:00:00Z'),
141
+ profile: {
142
+ profileId: 101,
143
+ userAccountId: 1,
144
+ bio: 'Lead engineer and mapper advocate.',
145
+ website: 'https://example.com',
146
+ verified: true,
147
+ },
148
+ },
149
+ {
150
+ userAccountId: 2,
151
+ username: 'bravo',
152
+ email: 'bravo@example.com',
153
+ displayName: 'Bravo Builder',
154
+ createdAt: new Date('2025-12-02T10:00:00Z'),
155
+ updatedAt: new Date('2025-12-02T11:00:00Z'),
156
+ profile: undefined,
157
+ },
158
+ ]);
159
+ });
160
+ });
161
+ });
@@ -0,0 +1,32 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { tableSchemas } from './generated/ztd-row-map.generated';
3
+ import { userAccountWriterColumnSets } from '../src/repositories/user-accounts';
4
+
5
+ const userColumns = new Set(
6
+ Object.keys(tableSchemas['public.user_account'].columns),
7
+ );
8
+
9
+ describe('user_account writer columns', () => {
10
+ test('insert columns must exist on the canonical table', () => {
11
+ const { insertColumns } = userAccountWriterColumnSets;
12
+ const missing = insertColumns.filter((column) => !userColumns.has(column));
13
+ expect(missing, `Missing columns: ${missing.join(', ')}`).toEqual([]);
14
+ expect(insertColumns).toEqual(
15
+ expect.arrayContaining(['username', 'email', 'display_name']),
16
+ );
17
+ });
18
+
19
+ test('writer column sets align with the canonical table', () => {
20
+ const { updateColumns, immutableColumns } = userAccountWriterColumnSets;
21
+ const missingUpdates = updateColumns.filter((column) => !userColumns.has(column));
22
+ expect(missingUpdates, `Missing update columns: ${missingUpdates.join(', ')}`).toEqual([]);
23
+ const missingImmutables = immutableColumns.filter((column) => !userColumns.has(column));
24
+ expect(missingImmutables, `Missing immutable columns: ${missingImmutables.join(', ')}`).toEqual([]);
25
+ immutableColumns.forEach((column) => {
26
+ expect(
27
+ updateColumns,
28
+ `Immutable column "${column}" should never appear in updateColumns`,
29
+ ).not.toContain(column);
30
+ });
31
+ });
32
+ });
@@ -3,6 +3,4 @@
3
3
  export default {
4
4
  ztdRootDir: 'ztd',
5
5
  ddlDir: 'ztd/ddl',
6
- enumsDir: 'ztd/enums',
7
- domainSpecsDir: 'ztd/domain-specs',
8
6
  };
@@ -1,9 +1,8 @@
1
1
  {
2
2
  "extends": "../../../tsconfig.json",
3
3
  "compilerOptions": {
4
- "rootDir": ".",
5
4
  "outDir": "dist",
6
5
  "tsBuildInfoFile": "dist/.tsbuildinfo"
7
6
  },
8
- "include": ["tests/**/*.ts"]
7
+ "include": ["src/**/*.ts", "tests/**/*.ts"]
9
8
  }