@c9up/atlas 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/README.md +8 -0
  2. package/db.darwin-arm64.node +0 -0
  3. package/db.darwin-x64.node +0 -0
  4. package/db.linux-arm64-gnu.node +0 -0
  5. package/db.linux-x64-gnu.node +0 -0
  6. package/db.win32-x64-msvc.node +0 -0
  7. package/dist/AtlasProvider.d.ts +60 -7
  8. package/dist/AtlasProvider.d.ts.map +1 -1
  9. package/dist/AtlasProvider.js +204 -38
  10. package/dist/AtlasProvider.js.map +1 -1
  11. package/dist/BaseEntity.d.ts +17 -0
  12. package/dist/BaseEntity.d.ts.map +1 -1
  13. package/dist/BaseEntity.js.map +1 -1
  14. package/dist/BaseModel.d.ts +63 -27
  15. package/dist/BaseModel.d.ts.map +1 -1
  16. package/dist/BaseModel.js +108 -51
  17. package/dist/BaseModel.js.map +1 -1
  18. package/dist/BaseRepository.d.ts +11 -1
  19. package/dist/BaseRepository.d.ts.map +1 -1
  20. package/dist/BaseRepository.js +95 -20
  21. package/dist/BaseRepository.js.map +1 -1
  22. package/dist/ConnectionManager.d.ts +106 -0
  23. package/dist/ConnectionManager.d.ts.map +1 -0
  24. package/dist/ConnectionManager.js +228 -0
  25. package/dist/ConnectionManager.js.map +1 -0
  26. package/dist/ModelQuery.d.ts +256 -43
  27. package/dist/ModelQuery.d.ts.map +1 -1
  28. package/dist/ModelQuery.js +899 -231
  29. package/dist/ModelQuery.js.map +1 -1
  30. package/dist/Transaction.d.ts +54 -1
  31. package/dist/Transaction.d.ts.map +1 -1
  32. package/dist/Transaction.js +121 -59
  33. package/dist/Transaction.js.map +1 -1
  34. package/dist/adapters/NapiDbAdapter.d.ts +11 -0
  35. package/dist/adapters/NapiDbAdapter.d.ts.map +1 -1
  36. package/dist/adapters/NapiDbAdapter.js +35 -7
  37. package/dist/adapters/NapiDbAdapter.js.map +1 -1
  38. package/dist/console/contract.d.ts +62 -0
  39. package/dist/console/contract.d.ts.map +1 -0
  40. package/dist/console/contract.js +38 -0
  41. package/dist/console/contract.js.map +1 -0
  42. package/dist/console/factoryCommands.d.ts +23 -0
  43. package/dist/console/factoryCommands.d.ts.map +1 -0
  44. package/dist/console/factoryCommands.js +62 -0
  45. package/dist/console/factoryCommands.js.map +1 -0
  46. package/dist/console/migrationCommands.d.ts +38 -8
  47. package/dist/console/migrationCommands.d.ts.map +1 -1
  48. package/dist/console/migrationCommands.js +157 -50
  49. package/dist/console/migrationCommands.js.map +1 -1
  50. package/dist/console/schemaCheckCommand.d.ts +11 -17
  51. package/dist/console/schemaCheckCommand.d.ts.map +1 -1
  52. package/dist/console/schemaCheckCommand.js +21 -15
  53. package/dist/console/schemaCheckCommand.js.map +1 -1
  54. package/dist/console/schemaDumpCommand.d.ts +30 -0
  55. package/dist/console/schemaDumpCommand.d.ts.map +1 -0
  56. package/dist/console/schemaDumpCommand.js +69 -0
  57. package/dist/console/schemaDumpCommand.js.map +1 -0
  58. package/dist/console/schemaGenerateCommand.d.ts +100 -0
  59. package/dist/console/schemaGenerateCommand.d.ts.map +1 -0
  60. package/dist/console/schemaGenerateCommand.js +246 -0
  61. package/dist/console/schemaGenerateCommand.js.map +1 -0
  62. package/dist/console/seederCommands.d.ts +46 -0
  63. package/dist/console/seederCommands.d.ts.map +1 -0
  64. package/dist/console/seederCommands.js +136 -0
  65. package/dist/console/seederCommands.js.map +1 -0
  66. package/dist/decorators/entity.d.ts +13 -2
  67. package/dist/decorators/entity.d.ts.map +1 -1
  68. package/dist/decorators/entity.js +10 -0
  69. package/dist/decorators/entity.js.map +1 -1
  70. package/dist/decorators/hooks.d.ts +3 -3
  71. package/dist/decorators/hooks.d.ts.map +1 -1
  72. package/dist/decorators/hooks.js.map +1 -1
  73. package/dist/events.d.ts +5 -0
  74. package/dist/events.d.ts.map +1 -1
  75. package/dist/events.js.map +1 -1
  76. package/dist/factories.d.ts +8 -0
  77. package/dist/factories.d.ts.map +1 -0
  78. package/dist/factories.js +8 -0
  79. package/dist/factories.js.map +1 -0
  80. package/dist/index.d.ts +11 -5
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +12 -4
  83. package/dist/index.js.map +1 -1
  84. package/dist/lucid-schema.d.ts +15 -0
  85. package/dist/lucid-schema.d.ts.map +1 -0
  86. package/dist/lucid-schema.js +16 -0
  87. package/dist/lucid-schema.js.map +1 -0
  88. package/dist/orm.d.ts +14 -0
  89. package/dist/orm.d.ts.map +1 -0
  90. package/dist/orm.js +14 -0
  91. package/dist/orm.js.map +1 -0
  92. package/dist/query/DatabaseQueryBuilder.d.ts +557 -0
  93. package/dist/query/DatabaseQueryBuilder.d.ts.map +1 -0
  94. package/dist/query/DatabaseQueryBuilder.js +1798 -0
  95. package/dist/query/DatabaseQueryBuilder.js.map +1 -0
  96. package/dist/query/DmlBuilder.d.ts +62 -0
  97. package/dist/query/DmlBuilder.d.ts.map +1 -0
  98. package/dist/query/DmlBuilder.js +84 -0
  99. package/dist/query/DmlBuilder.js.map +1 -0
  100. package/dist/query/QueryBuilder.d.ts +5 -0
  101. package/dist/query/QueryBuilder.d.ts.map +1 -1
  102. package/dist/query/QueryBuilder.js +7 -0
  103. package/dist/query/QueryBuilder.js.map +1 -1
  104. package/dist/query/RawQueryBuilder.d.ts +47 -0
  105. package/dist/query/RawQueryBuilder.d.ts.map +1 -0
  106. package/dist/query/RawQueryBuilder.js +138 -0
  107. package/dist/query/RawQueryBuilder.js.map +1 -0
  108. package/dist/query/interpolate.d.ts +45 -0
  109. package/dist/query/interpolate.d.ts.map +1 -0
  110. package/dist/query/interpolate.js +51 -0
  111. package/dist/query/interpolate.js.map +1 -0
  112. package/dist/query/native.d.ts +8 -0
  113. package/dist/query/native.d.ts.map +1 -1
  114. package/dist/query/native.js +14 -0
  115. package/dist/query/native.js.map +1 -1
  116. package/dist/query/operators.d.ts +4 -0
  117. package/dist/query/operators.d.ts.map +1 -0
  118. package/dist/query/operators.js +27 -0
  119. package/dist/query/operators.js.map +1 -0
  120. package/dist/schema/Migration.d.ts +31 -7
  121. package/dist/schema/Migration.d.ts.map +1 -1
  122. package/dist/schema/Migration.js +34 -7
  123. package/dist/schema/Migration.js.map +1 -1
  124. package/dist/schema/MigrationRunner.d.ts +56 -6
  125. package/dist/schema/MigrationRunner.d.ts.map +1 -1
  126. package/dist/schema/MigrationRunner.js +382 -112
  127. package/dist/schema/MigrationRunner.js.map +1 -1
  128. package/dist/schema/Schema.d.ts +59 -2
  129. package/dist/schema/Schema.d.ts.map +1 -1
  130. package/dist/schema/Schema.js +143 -9
  131. package/dist/schema/Schema.js.map +1 -1
  132. package/dist/schema/SchemaDumper.d.ts +121 -0
  133. package/dist/schema/SchemaDumper.d.ts.map +1 -0
  134. package/dist/schema/SchemaDumper.js +365 -0
  135. package/dist/schema/SchemaDumper.js.map +1 -0
  136. package/dist/schema/Seeder.d.ts +13 -0
  137. package/dist/schema/Seeder.d.ts.map +1 -1
  138. package/dist/schema/Seeder.js +28 -5
  139. package/dist/schema/Seeder.js.map +1 -1
  140. package/dist/schema/TableBuilder.d.ts +27 -8
  141. package/dist/schema/TableBuilder.d.ts.map +1 -1
  142. package/dist/schema/TableBuilder.js +50 -12
  143. package/dist/schema/TableBuilder.js.map +1 -1
  144. package/dist/schema/catalog.d.ts +25 -1
  145. package/dist/schema/catalog.d.ts.map +1 -1
  146. package/dist/schema/catalog.js +69 -22
  147. package/dist/schema/catalog.js.map +1 -1
  148. package/dist/schema/introspect.d.ts +1 -1
  149. package/dist/schema/introspect.d.ts.map +1 -1
  150. package/dist/schema/introspect.js +26 -11
  151. package/dist/schema/introspect.js.map +1 -1
  152. package/dist/seeders.d.ts +7 -0
  153. package/dist/seeders.d.ts.map +1 -0
  154. package/dist/seeders.js +7 -0
  155. package/dist/seeders.js.map +1 -0
  156. package/dist/services/db.d.ts +120 -9
  157. package/dist/services/db.d.ts.map +1 -1
  158. package/dist/services/db.js +182 -26
  159. package/dist/services/db.js.map +1 -1
  160. package/dist/testing/DatabaseCleanup.d.ts +29 -6
  161. package/dist/testing/DatabaseCleanup.d.ts.map +1 -1
  162. package/dist/testing/DatabaseCleanup.js +44 -17
  163. package/dist/testing/DatabaseCleanup.js.map +1 -1
  164. package/dist/testing/DbAssertions.d.ts +48 -0
  165. package/dist/testing/DbAssertions.d.ts.map +1 -0
  166. package/dist/testing/DbAssertions.js +80 -0
  167. package/dist/testing/DbAssertions.js.map +1 -0
  168. package/dist/testing/Factory.d.ts +111 -25
  169. package/dist/testing/Factory.d.ts.map +1 -1
  170. package/dist/testing/Factory.js +331 -56
  171. package/dist/testing/Factory.js.map +1 -1
  172. package/dist/testing/TestUtils.d.ts +49 -0
  173. package/dist/testing/TestUtils.d.ts.map +1 -0
  174. package/dist/testing/TestUtils.js +79 -0
  175. package/dist/testing/TestUtils.js.map +1 -0
  176. package/dist/testing/index.d.ts +3 -1
  177. package/dist/testing/index.d.ts.map +1 -1
  178. package/dist/testing/index.js +3 -1
  179. package/dist/testing/index.js.map +1 -1
  180. package/dist/types/relations.d.ts +31 -0
  181. package/dist/types/relations.d.ts.map +1 -0
  182. package/dist/types/relations.js +20 -0
  183. package/dist/types/relations.js.map +1 -0
  184. package/index.darwin-arm64.node +0 -0
  185. package/index.darwin-x64.node +0 -0
  186. package/index.linux-arm64-gnu.node +0 -0
  187. package/index.linux-x64-gnu.node +0 -0
  188. package/index.win32-x64-msvc.node +0 -0
  189. package/package.json +30 -2
  190. package/src/AtlasProvider.ts +297 -51
  191. package/src/BaseEntity.ts +29 -0
  192. package/src/BaseModel.ts +174 -37
  193. package/src/BaseRepository.ts +136 -17
  194. package/src/ConnectionManager.ts +298 -0
  195. package/src/ModelQuery.ts +1309 -260
  196. package/src/Transaction.ts +194 -60
  197. package/src/adapters/NapiDbAdapter.ts +72 -14
  198. package/src/console/contract.ts +96 -0
  199. package/src/console/factoryCommands.ts +73 -0
  200. package/src/console/migrationCommands.ts +253 -63
  201. package/src/console/schemaCheckCommand.ts +26 -22
  202. package/src/console/schemaDumpCommand.ts +96 -0
  203. package/src/console/schemaGenerateCommand.ts +380 -0
  204. package/src/console/seederCommands.ts +174 -0
  205. package/src/decorators/entity.ts +18 -2
  206. package/src/decorators/hooks.ts +5 -3
  207. package/src/events.ts +5 -0
  208. package/src/factories.ts +10 -0
  209. package/src/index.ts +50 -3
  210. package/src/lucid-schema.ts +22 -0
  211. package/src/orm.ts +13 -0
  212. package/src/query/DatabaseQueryBuilder.ts +2724 -0
  213. package/src/query/DmlBuilder.ts +131 -0
  214. package/src/query/QueryBuilder.ts +8 -0
  215. package/src/query/RawQueryBuilder.ts +173 -0
  216. package/src/query/interpolate.ts +73 -0
  217. package/src/query/native.ts +29 -0
  218. package/src/query/operators.ts +27 -0
  219. package/src/schema/Migration.ts +36 -7
  220. package/src/schema/MigrationRunner.ts +483 -119
  221. package/src/schema/Schema.ts +168 -19
  222. package/src/schema/SchemaDumper.ts +518 -0
  223. package/src/schema/Seeder.ts +47 -10
  224. package/src/schema/TableBuilder.ts +48 -11
  225. package/src/schema/catalog.ts +94 -24
  226. package/src/schema/introspect.ts +26 -9
  227. package/src/seeders.ts +16 -0
  228. package/src/services/db.ts +343 -31
  229. package/src/testing/DatabaseCleanup.ts +73 -21
  230. package/src/testing/DbAssertions.ts +127 -0
  231. package/src/testing/Factory.ts +559 -79
  232. package/src/testing/TestUtils.ts +123 -0
  233. package/src/testing/index.ts +12 -1
  234. package/src/types/relations.ts +43 -0
@@ -5,20 +5,51 @@
5
5
  */
6
6
 
7
7
  import type { AsyncDatabaseConnection } from "../adapters/NapiDbAdapter.js";
8
+ import { AtlasError } from "../errors.js";
8
9
  import { compileStatementNative } from "../query/native.js";
9
- import { listUserTables, withoutForeignKeys } from "../schema/catalog.js";
10
+ import { listUserTables, runWithoutForeignKeys } from "../schema/catalog.js";
11
+ import type { TransactionClient } from "../Transaction.js";
12
+
13
+ /** What {@link useTransaction} hands back: the pinned trx plus its teardown. */
14
+ export interface TestTransaction {
15
+ /**
16
+ * The interactive transaction, pinned to ONE pooled connection. Run every
17
+ * query the test makes through THIS handle (`trx.query` / `trx.execute`, or
18
+ * `repo.useTransaction(trx)`), never the pooled `db` — that is what makes the
19
+ * rollback isolate the test.
20
+ */
21
+ trx: TransactionClient;
22
+ /** Roll everything the test did back and return the connection to the pool. */
23
+ rollback: () => Promise<void>;
24
+ }
10
25
 
11
26
  /**
12
- * Wrap a test in a savepoint that is rolled back after.
27
+ * Isolate a test by running it inside a transaction that is rolled back after
28
+ * (Adonis Lucid parity — Lucid's tests use `db.transaction()` the same way).
29
+ *
30
+ * The earlier implementation issued `SAVEPOINT` / `ROLLBACK` through the pooled
31
+ * `db`, so with `poolMax > 1` the savepoint and its rollback — and the test's
32
+ * own queries — could each land on a DIFFERENT pooled connection, and the
33
+ * "isolation" silently did nothing. This pins a single interactive transaction
34
+ * instead: every query on the returned `trx` runs on that one connection, and
35
+ * `rollback()` reverts them together, correctly on any pool size.
13
36
  */
14
37
  export async function useTransaction(
15
38
  db: AsyncDatabaseConnection,
16
- ): Promise<() => Promise<void>> {
17
- await db.execute("SAVEPOINT test_savepoint");
18
- return async () => {
19
- await db.execute("ROLLBACK TO SAVEPOINT test_savepoint");
20
- await db.execute("RELEASE SAVEPOINT test_savepoint");
21
- };
39
+ ): Promise<TestTransaction> {
40
+ if (typeof db.transaction !== "function") {
41
+ throw new AtlasError(
42
+ "E_NO_INTERACTIVE_TRANSACTION",
43
+ "useTransaction() needs a connection with an interactive transaction() (a real napi connection); this adapter has none.",
44
+ {
45
+ hint: "Use createNapiConnection(), or drive rollback yourself if your adapter can't pin a connection.",
46
+ },
47
+ );
48
+ }
49
+ // Manual (callback-less) mode: a pinned TransactionClient the test drives and
50
+ // tears down via rollback() — no cross-pool savepoint scatter.
51
+ const trx = await db.transaction();
52
+ return { trx, rollback: () => trx.rollback() };
22
53
  }
23
54
 
24
55
  /**
@@ -27,23 +58,44 @@ export async function useTransaction(
27
58
  * shared dialect-aware catalog helper, not a SQLite-only `sqlite_master` query.
28
59
  *
29
60
  * Foreign keys are suspended for the duration so the delete order doesn't
30
- * matter, and restored even if a statement throws. The row removal itself goes
31
- * through the Rust compiler (`DELETE` cross-dialect and transaction-friendly,
32
- * unlike `TRUNCATE` which auto-commits on MySQL).
61
+ * matter. The FK toggle and every DELETE run on ONE pinned connection (via
62
+ * `runWithoutForeignKeys` `runInTransaction`), so a connection pool can't
63
+ * scatter the connection-local `PRAGMA`/`SET` away from the deletes. The row
64
+ * removal goes through the Rust compiler (`DELETE` — cross-dialect, unlike
65
+ * `TRUNCATE` which auto-commits on MySQL).
33
66
  */
34
- export async function truncateAll(db: AsyncDatabaseConnection): Promise<void> {
67
+ export async function truncateAll(
68
+ db: AsyncDatabaseConnection,
69
+ ignoreTables: readonly string[] = [],
70
+ ): Promise<void> {
35
71
  // The connection's own dialect, not the module default — correct even when
36
72
  // an app runs several connections on different engines.
37
73
  const dialect = db.dialect;
38
- const tables = await listUserTables(db, dialect);
74
+ const ignore = new Set(ignoreTables);
75
+ const tables = (await listUserTables(db, dialect)).filter(
76
+ (t) => !ignore.has(t),
77
+ );
39
78
  if (tables.length === 0) return;
40
- await withoutForeignKeys(db, dialect, async () => {
41
- for (const name of tables) {
42
- const compiled = compileStatementNative(
43
- { kind: "delete", table: name, wheres: [] },
44
- dialect,
45
- );
46
- await db.execute(compiled.statements[0], compiled.params);
47
- }
79
+
80
+ // Postgres: a plain DELETE respects FKs immediately (atlas FKs aren't
81
+ // DEFERRABLE), so deleting a parent before its children raises 23503 — and
82
+ // nothing orders the tables. `TRUNCATE CASCADE` is transactional on pg,
83
+ // order-independent, and needs no session-level FK toggle to leak. (Names
84
+ // come from the DB catalog; quote them for identifiers with embedded quotes.)
85
+ if (dialect === "postgres") {
86
+ const list = tables.map((t) => `"${t.replace(/"/g, '""')}"`).join(", ");
87
+ await db.execute(`TRUNCATE ${list} RESTART IDENTITY CASCADE`);
88
+ return;
89
+ }
90
+
91
+ // MySQL/SQLite: DELETE (TRUNCATE auto-commits on MySQL, breaking test
92
+ // transaction isolation), with FK suspension on ONE pinned connection.
93
+ const statements = tables.map((name) => {
94
+ const compiled = compileStatementNative(
95
+ { kind: "delete", table: name, wheres: [] },
96
+ dialect,
97
+ );
98
+ return { sql: compiled.statements[0], params: compiled.params };
48
99
  });
100
+ await runWithoutForeignKeys(db, dialect, statements);
49
101
  }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * `db()` — a helix plugin (AdonisJS Lucid database-assertions parity) that
3
+ * injects database assertions on the test context as `ctx.db`:
4
+ *
5
+ * // tests/bootstrap.ts
6
+ * import { configure } from "@c9up/helix";
7
+ * import { db } from "@c9up/atlas/testing";
8
+ * await configure({ plugins: [db(connection)] });
9
+ *
10
+ * test("registers the user", async ({ db }) => {
11
+ * await db.assertHas("users", { email: "a@b.c" });
12
+ * await db.assertCount("users", 1);
13
+ * });
14
+ *
15
+ * Values are always bound as parameters; table/column identifiers are validated
16
+ * against a strict pattern (letters, digits, `_`, `.`) and quoted, so neither a
17
+ * value nor an identifier can inject SQL.
18
+ *
19
+ * NOTE (namespace): the AdonisJS docs expose these as `db.assert*`; the exact
20
+ * surface is re-verified against the Adonis docs when available. Model-based
21
+ * `assertModelExists`/`assertModelMissing` (entity PK metadata) are pending.
22
+ */
23
+
24
+ import type { Plugin } from "@c9up/helix";
25
+
26
+ /** The minimal connection the assertions need — a parameterized query runner. */
27
+ export interface DbConnectionLike {
28
+ query(sql: string, params?: unknown[]): Promise<Record<string, unknown>[]>;
29
+ }
30
+
31
+ /** Reject identifiers that could break out of the quoted context. */
32
+ function quoteIdent(name: string): string {
33
+ if (!/^[A-Za-z_][A-Za-z0-9_.]*$/.test(name)) {
34
+ throw new Error(`db assertion: unsafe identifier ${JSON.stringify(name)}`);
35
+ }
36
+ return `"${name.replace(/\./g, '"."')}"`;
37
+ }
38
+
39
+ async function countRows(
40
+ conn: DbConnectionLike,
41
+ table: string,
42
+ payload?: Record<string, unknown>,
43
+ ): Promise<number> {
44
+ let where = "";
45
+ let params: unknown[] = [];
46
+ if (payload) {
47
+ const keys = Object.keys(payload);
48
+ if (keys.length > 0) {
49
+ where = ` WHERE ${keys.map((k) => `${quoteIdent(k)} = ?`).join(" AND ")}`;
50
+ params = keys.map((k) => payload[k]);
51
+ }
52
+ }
53
+ const rows = await conn.query(
54
+ `SELECT COUNT(*) AS c FROM ${quoteIdent(table)}${where}`,
55
+ params,
56
+ );
57
+ return Number(rows[0]?.c ?? 0);
58
+ }
59
+
60
+ /** The `ctx.db` assertion surface (AdonisJS Lucid database-assertions parity). */
61
+ export interface DbAssertions {
62
+ /** At least one row in `table` matches every column in `payload`. */
63
+ assertHas(table: string, payload: Record<string, unknown>): Promise<void>;
64
+ /** No row in `table` matches `payload`. */
65
+ assertMissing(table: string, payload: Record<string, unknown>): Promise<void>;
66
+ /** `table` has exactly `expected` rows (optionally matching `payload`). */
67
+ assertCount(
68
+ table: string,
69
+ expected: number,
70
+ payload?: Record<string, unknown>,
71
+ ): Promise<void>;
72
+ /** `table` has no rows at all. */
73
+ assertEmpty(table: string): Promise<void>;
74
+ }
75
+
76
+ /** Build the assertion surface bound to a connection. */
77
+ export function createDbAssertions(conn: DbConnectionLike): DbAssertions {
78
+ return {
79
+ async assertHas(table, payload) {
80
+ const n = await countRows(conn, table, payload);
81
+ if (n === 0) {
82
+ throw new Error(
83
+ `Expected "${table}" to have a row matching ${JSON.stringify(payload)}, found none.`,
84
+ );
85
+ }
86
+ },
87
+ async assertMissing(table, payload) {
88
+ const n = await countRows(conn, table, payload);
89
+ if (n > 0) {
90
+ throw new Error(
91
+ `Expected "${table}" to have NO row matching ${JSON.stringify(payload)}, found ${n}.`,
92
+ );
93
+ }
94
+ },
95
+ async assertCount(table, expected, payload) {
96
+ const n = await countRows(conn, table, payload);
97
+ if (n !== expected) {
98
+ const scope = payload ? ` matching ${JSON.stringify(payload)}` : "";
99
+ throw new Error(
100
+ `Expected "${table}" to have ${expected} row(s)${scope}, found ${n}.`,
101
+ );
102
+ }
103
+ },
104
+ async assertEmpty(table) {
105
+ const n = await countRows(conn, table);
106
+ if (n > 0) {
107
+ throw new Error(`Expected "${table}" to be empty, found ${n} row(s).`);
108
+ }
109
+ },
110
+ };
111
+ }
112
+
113
+ /** The `db()` helix plugin — registers {@link DbAssertions} as `ctx.db`. */
114
+ export function db(conn: DbConnectionLike): Plugin {
115
+ const assertions = createDbAssertions(conn);
116
+ return (api) => {
117
+ api.context.macro("db", assertions);
118
+ };
119
+ }
120
+
121
+ // Typing side of the plugin — importing `@c9up/atlas/testing` augments the
122
+ // helix test context with `db` (the Japa pattern).
123
+ declare module "@c9up/helix" {
124
+ interface TestContext {
125
+ db: DbAssertions;
126
+ }
127
+ }