@c9up/atlas 0.1.19 → 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 (250) hide show
  1. package/README.md +63 -14
  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 +66 -7
  8. package/dist/AtlasProvider.d.ts.map +1 -1
  9. package/dist/AtlasProvider.js +205 -39
  10. package/dist/AtlasProvider.js.map +1 -1
  11. package/dist/BaseEntity.d.ts +181 -5
  12. package/dist/BaseEntity.d.ts.map +1 -1
  13. package/dist/BaseEntity.js +339 -33
  14. package/dist/BaseEntity.js.map +1 -1
  15. package/dist/BaseModel.d.ts +127 -0
  16. package/dist/BaseModel.d.ts.map +1 -0
  17. package/dist/BaseModel.js +250 -0
  18. package/dist/BaseModel.js.map +1 -0
  19. package/dist/BaseRepository.d.ts +81 -15
  20. package/dist/BaseRepository.d.ts.map +1 -1
  21. package/dist/BaseRepository.js +1433 -387
  22. package/dist/BaseRepository.js.map +1 -1
  23. package/dist/ConnectionManager.d.ts +106 -0
  24. package/dist/ConnectionManager.d.ts.map +1 -0
  25. package/dist/ConnectionManager.js +228 -0
  26. package/dist/ConnectionManager.js.map +1 -0
  27. package/dist/ModelQuery.d.ts +577 -41
  28. package/dist/ModelQuery.d.ts.map +1 -1
  29. package/dist/ModelQuery.js +2188 -291
  30. package/dist/ModelQuery.js.map +1 -1
  31. package/dist/Transaction.d.ts +71 -1
  32. package/dist/Transaction.d.ts.map +1 -1
  33. package/dist/Transaction.js +138 -24
  34. package/dist/Transaction.js.map +1 -1
  35. package/dist/adapters/NapiDbAdapter.d.ts +44 -4
  36. package/dist/adapters/NapiDbAdapter.d.ts.map +1 -1
  37. package/dist/adapters/NapiDbAdapter.js +131 -13
  38. package/dist/adapters/NapiDbAdapter.js.map +1 -1
  39. package/dist/console/contract.d.ts +62 -0
  40. package/dist/console/contract.d.ts.map +1 -0
  41. package/dist/console/contract.js +38 -0
  42. package/dist/console/contract.js.map +1 -0
  43. package/dist/console/factoryCommands.d.ts +23 -0
  44. package/dist/console/factoryCommands.d.ts.map +1 -0
  45. package/dist/console/factoryCommands.js +62 -0
  46. package/dist/console/factoryCommands.js.map +1 -0
  47. package/dist/console/migrationCommands.d.ts +78 -0
  48. package/dist/console/migrationCommands.d.ts.map +1 -0
  49. package/dist/console/migrationCommands.js +327 -0
  50. package/dist/console/migrationCommands.js.map +1 -0
  51. package/dist/console/schemaCheckCommand.d.ts +11 -17
  52. package/dist/console/schemaCheckCommand.d.ts.map +1 -1
  53. package/dist/console/schemaCheckCommand.js +21 -15
  54. package/dist/console/schemaCheckCommand.js.map +1 -1
  55. package/dist/console/schemaDumpCommand.d.ts +30 -0
  56. package/dist/console/schemaDumpCommand.d.ts.map +1 -0
  57. package/dist/console/schemaDumpCommand.js +69 -0
  58. package/dist/console/schemaDumpCommand.js.map +1 -0
  59. package/dist/console/schemaGenerateCommand.d.ts +100 -0
  60. package/dist/console/schemaGenerateCommand.d.ts.map +1 -0
  61. package/dist/console/schemaGenerateCommand.js +246 -0
  62. package/dist/console/schemaGenerateCommand.js.map +1 -0
  63. package/dist/console/seederCommands.d.ts +46 -0
  64. package/dist/console/seederCommands.d.ts.map +1 -0
  65. package/dist/console/seederCommands.js +136 -0
  66. package/dist/console/seederCommands.js.map +1 -0
  67. package/dist/decorators/entity.d.ts +50 -8
  68. package/dist/decorators/entity.d.ts.map +1 -1
  69. package/dist/decorators/entity.js +42 -2
  70. package/dist/decorators/entity.js.map +1 -1
  71. package/dist/decorators/hooks.d.ts +3 -3
  72. package/dist/decorators/hooks.d.ts.map +1 -1
  73. package/dist/decorators/hooks.js.map +1 -1
  74. package/dist/events.d.ts +69 -0
  75. package/dist/events.d.ts.map +1 -0
  76. package/dist/events.js +82 -0
  77. package/dist/events.js.map +1 -0
  78. package/dist/factories.d.ts +8 -0
  79. package/dist/factories.d.ts.map +1 -0
  80. package/dist/factories.js +8 -0
  81. package/dist/factories.js.map +1 -0
  82. package/dist/index.d.ts +15 -5
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +14 -3
  85. package/dist/index.js.map +1 -1
  86. package/dist/lucid-schema.d.ts +15 -0
  87. package/dist/lucid-schema.d.ts.map +1 -0
  88. package/dist/lucid-schema.js +16 -0
  89. package/dist/lucid-schema.js.map +1 -0
  90. package/dist/metadata-keys.d.ts +3 -2
  91. package/dist/metadata-keys.d.ts.map +1 -1
  92. package/dist/naming/NamingStrategy.d.ts +7 -0
  93. package/dist/naming/NamingStrategy.d.ts.map +1 -1
  94. package/dist/naming/NamingStrategy.js +16 -0
  95. package/dist/naming/NamingStrategy.js.map +1 -1
  96. package/dist/orm.d.ts +14 -0
  97. package/dist/orm.d.ts.map +1 -0
  98. package/dist/orm.js +14 -0
  99. package/dist/orm.js.map +1 -0
  100. package/dist/query/DatabaseQueryBuilder.d.ts +557 -0
  101. package/dist/query/DatabaseQueryBuilder.d.ts.map +1 -0
  102. package/dist/query/DatabaseQueryBuilder.js +1798 -0
  103. package/dist/query/DatabaseQueryBuilder.js.map +1 -0
  104. package/dist/query/DmlBuilder.d.ts +62 -0
  105. package/dist/query/DmlBuilder.d.ts.map +1 -0
  106. package/dist/query/DmlBuilder.js +84 -0
  107. package/dist/query/DmlBuilder.js.map +1 -0
  108. package/dist/query/QueryBuilder.d.ts +5 -0
  109. package/dist/query/QueryBuilder.d.ts.map +1 -1
  110. package/dist/query/QueryBuilder.js +7 -0
  111. package/dist/query/QueryBuilder.js.map +1 -1
  112. package/dist/query/RawQueryBuilder.d.ts +47 -0
  113. package/dist/query/RawQueryBuilder.d.ts.map +1 -0
  114. package/dist/query/RawQueryBuilder.js +138 -0
  115. package/dist/query/RawQueryBuilder.js.map +1 -0
  116. package/dist/query/interpolate.d.ts +45 -0
  117. package/dist/query/interpolate.d.ts.map +1 -0
  118. package/dist/query/interpolate.js +51 -0
  119. package/dist/query/interpolate.js.map +1 -0
  120. package/dist/query/native.d.ts +8 -0
  121. package/dist/query/native.d.ts.map +1 -1
  122. package/dist/query/native.js +14 -0
  123. package/dist/query/native.js.map +1 -1
  124. package/dist/query/operators.d.ts +4 -0
  125. package/dist/query/operators.d.ts.map +1 -0
  126. package/dist/query/operators.js +27 -0
  127. package/dist/query/operators.js.map +1 -0
  128. package/dist/schema/Migration.d.ts +50 -3
  129. package/dist/schema/Migration.d.ts.map +1 -1
  130. package/dist/schema/Migration.js +60 -24
  131. package/dist/schema/Migration.js.map +1 -1
  132. package/dist/schema/MigrationRunner.d.ts +95 -34
  133. package/dist/schema/MigrationRunner.d.ts.map +1 -1
  134. package/dist/schema/MigrationRunner.js +523 -68
  135. package/dist/schema/MigrationRunner.js.map +1 -1
  136. package/dist/schema/Schema.d.ts +114 -0
  137. package/dist/schema/Schema.d.ts.map +1 -1
  138. package/dist/schema/Schema.js +272 -3
  139. package/dist/schema/Schema.js.map +1 -1
  140. package/dist/schema/SchemaDumper.d.ts +121 -0
  141. package/dist/schema/SchemaDumper.d.ts.map +1 -0
  142. package/dist/schema/SchemaDumper.js +365 -0
  143. package/dist/schema/SchemaDumper.js.map +1 -0
  144. package/dist/schema/Seeder.d.ts +13 -0
  145. package/dist/schema/Seeder.d.ts.map +1 -1
  146. package/dist/schema/Seeder.js +28 -5
  147. package/dist/schema/Seeder.js.map +1 -1
  148. package/dist/schema/TableBuilder.d.ts +274 -16
  149. package/dist/schema/TableBuilder.d.ts.map +1 -1
  150. package/dist/schema/TableBuilder.js +656 -52
  151. package/dist/schema/TableBuilder.js.map +1 -1
  152. package/dist/schema/catalog.d.ts +71 -0
  153. package/dist/schema/catalog.d.ts.map +1 -0
  154. package/dist/schema/catalog.js +158 -0
  155. package/dist/schema/catalog.js.map +1 -0
  156. package/dist/schema/introspect.d.ts +1 -1
  157. package/dist/schema/introspect.d.ts.map +1 -1
  158. package/dist/schema/introspect.js +26 -11
  159. package/dist/schema/introspect.js.map +1 -1
  160. package/dist/schema/types.d.ts +150 -1
  161. package/dist/schema/types.d.ts.map +1 -1
  162. package/dist/schema/types.js +11 -0
  163. package/dist/schema/types.js.map +1 -1
  164. package/dist/seeders.d.ts +7 -0
  165. package/dist/seeders.d.ts.map +1 -0
  166. package/dist/seeders.js +7 -0
  167. package/dist/seeders.js.map +1 -0
  168. package/dist/services/db.d.ts +122 -5
  169. package/dist/services/db.d.ts.map +1 -1
  170. package/dist/services/db.js +187 -14
  171. package/dist/services/db.js.map +1 -1
  172. package/dist/testing/DatabaseCleanup.d.ts +33 -7
  173. package/dist/testing/DatabaseCleanup.d.ts.map +1 -1
  174. package/dist/testing/DatabaseCleanup.js +54 -24
  175. package/dist/testing/DatabaseCleanup.js.map +1 -1
  176. package/dist/testing/DbAssertions.d.ts +48 -0
  177. package/dist/testing/DbAssertions.d.ts.map +1 -0
  178. package/dist/testing/DbAssertions.js +80 -0
  179. package/dist/testing/DbAssertions.js.map +1 -0
  180. package/dist/testing/Factory.d.ts +166 -15
  181. package/dist/testing/Factory.d.ts.map +1 -1
  182. package/dist/testing/Factory.js +497 -23
  183. package/dist/testing/Factory.js.map +1 -1
  184. package/dist/testing/TestUtils.d.ts +49 -0
  185. package/dist/testing/TestUtils.d.ts.map +1 -0
  186. package/dist/testing/TestUtils.js +79 -0
  187. package/dist/testing/TestUtils.js.map +1 -0
  188. package/dist/testing/index.d.ts +3 -1
  189. package/dist/testing/index.d.ts.map +1 -1
  190. package/dist/testing/index.js +3 -1
  191. package/dist/testing/index.js.map +1 -1
  192. package/dist/types/relations.d.ts +31 -0
  193. package/dist/types/relations.d.ts.map +1 -0
  194. package/dist/types/relations.js +20 -0
  195. package/dist/types/relations.js.map +1 -0
  196. package/index.darwin-arm64.node +0 -0
  197. package/index.darwin-x64.node +0 -0
  198. package/index.linux-arm64-gnu.node +0 -0
  199. package/index.linux-x64-gnu.node +0 -0
  200. package/index.win32-x64-msvc.node +0 -0
  201. package/package.json +32 -2
  202. package/scripts/guard-publish.mjs +15 -0
  203. package/src/AtlasProvider.ts +305 -52
  204. package/src/BaseEntity.ts +460 -41
  205. package/src/BaseModel.ts +461 -0
  206. package/src/BaseRepository.ts +1710 -421
  207. package/src/ConnectionManager.ts +298 -0
  208. package/src/ModelQuery.ts +2998 -335
  209. package/src/Transaction.ts +227 -30
  210. package/src/adapters/NapiDbAdapter.ts +224 -17
  211. package/src/console/contract.ts +96 -0
  212. package/src/console/factoryCommands.ts +73 -0
  213. package/src/console/migrationCommands.ts +448 -0
  214. package/src/console/schemaCheckCommand.ts +26 -22
  215. package/src/console/schemaDumpCommand.ts +96 -0
  216. package/src/console/schemaGenerateCommand.ts +380 -0
  217. package/src/console/seederCommands.ts +174 -0
  218. package/src/decorators/entity.ts +71 -8
  219. package/src/decorators/hooks.ts +5 -3
  220. package/src/events.ts +117 -0
  221. package/src/factories.ts +10 -0
  222. package/src/index.ts +69 -3
  223. package/src/lucid-schema.ts +22 -0
  224. package/src/metadata-keys.ts +3 -2
  225. package/src/naming/NamingStrategy.ts +23 -0
  226. package/src/orm.ts +13 -0
  227. package/src/query/DatabaseQueryBuilder.ts +2724 -0
  228. package/src/query/DmlBuilder.ts +131 -0
  229. package/src/query/QueryBuilder.ts +8 -0
  230. package/src/query/RawQueryBuilder.ts +173 -0
  231. package/src/query/interpolate.ts +73 -0
  232. package/src/query/native.ts +29 -0
  233. package/src/query/operators.ts +27 -0
  234. package/src/schema/Migration.ts +71 -3
  235. package/src/schema/MigrationRunner.ts +676 -69
  236. package/src/schema/Schema.ts +359 -3
  237. package/src/schema/SchemaDumper.ts +518 -0
  238. package/src/schema/Seeder.ts +47 -10
  239. package/src/schema/TableBuilder.ts +783 -52
  240. package/src/schema/catalog.ts +236 -0
  241. package/src/schema/introspect.ts +26 -9
  242. package/src/schema/types.ts +137 -2
  243. package/src/seeders.ts +16 -0
  244. package/src/services/db.ts +361 -21
  245. package/src/testing/DatabaseCleanup.ts +78 -25
  246. package/src/testing/DbAssertions.ts +127 -0
  247. package/src/testing/Factory.ts +837 -40
  248. package/src/testing/TestUtils.ts +123 -0
  249. package/src/testing/index.ts +12 -1
  250. package/src/types/relations.ts +43 -0
@@ -0,0 +1,518 @@
1
+ /**
2
+ * Schema dumps (Adonis Lucid `schema:dump` / `SchemaDumper`). Serialise the live
3
+ * database's structure — every table's DDL plus the migration bookkeeping rows —
4
+ * into a `.sql` file and a `.meta.json` manifest, so a fresh database can be
5
+ * rebuilt from the dump instead of replaying the whole migration history.
6
+ *
7
+ * The migration runner consumes this via `migration:run --schema-path` /
8
+ * `migration:fresh --schema-path`: when a database has no applied migrations and
9
+ * a dump exists, it loads the dump, then runs only the migrations that postdate
10
+ * it (see {@link MigrationRunner.loadDump}).
11
+ *
12
+ * const dumper = new SchemaDumper(db, { migrationsDir: 'database/migrations' })
13
+ * await dumper.run()
14
+ * if (dumper.error) throw dumper.error
15
+ * console.log(dumper.result?.dumpPath, dumper.result?.metaPath)
16
+ */
17
+
18
+ import * as fsp from "node:fs/promises";
19
+ import * as path from "node:path";
20
+ import type { AsyncDatabaseConnection } from "../adapters/NapiDbAdapter.js";
21
+ import type { AtlasDialect } from "../query/native.js";
22
+ import { listUserTables } from "./catalog.js";
23
+ import { introspectTable } from "./introspect.js";
24
+
25
+ /** Current time as an ISO string (extracted so tests can stub it). */
26
+ function nowIso(): string {
27
+ return new Date().toISOString();
28
+ }
29
+
30
+ /** The manifest path for a dump `.sql` path (extension swapped for `.meta.json`). */
31
+ export function schemaDumpManifestPath(dumpPath: string): string {
32
+ return `${dumpPath.replace(/\.sql$/, "")}.meta.json`;
33
+ }
34
+
35
+ /** Structural guard for a manifest — no cast, `in`-narrowed field checks. */
36
+ function isSchemaDumpManifest(v: unknown): v is SchemaDumpManifest {
37
+ return (
38
+ typeof v === "object" &&
39
+ v !== null &&
40
+ "version" in v &&
41
+ v.version === 1 &&
42
+ "schemaTableName" in v &&
43
+ typeof v.schemaTableName === "string"
44
+ );
45
+ }
46
+
47
+ /**
48
+ * Read + validate a dump's manifest, or return `undefined` when it is missing.
49
+ * Throws on a present-but-malformed manifest (wrong `version`, missing fields)
50
+ * so a corrupt dump is caught rather than silently mis-loaded.
51
+ */
52
+ export async function readSchemaDumpManifest(
53
+ dumpPath: string,
54
+ ): Promise<SchemaDumpManifest | undefined> {
55
+ const metaPath = schemaDumpManifestPath(dumpPath);
56
+ let raw: string;
57
+ try {
58
+ raw = await fsp.readFile(metaPath, "utf8");
59
+ } catch {
60
+ return undefined;
61
+ }
62
+ const parsed: unknown = JSON.parse(raw);
63
+ if (!isSchemaDumpManifest(parsed)) {
64
+ throw new Error(
65
+ `Invalid schema-dump manifest at ${metaPath}: expected version 1 with a schemaTableName.`,
66
+ );
67
+ }
68
+ return parsed;
69
+ }
70
+
71
+ /** The manifest sidecar written next to the SQL dump (Adonis Lucid `.meta.json`). */
72
+ export interface SchemaDumpManifest {
73
+ version: 1;
74
+ connection: string;
75
+ dialect: AtlasDialect;
76
+ /** Path to the SQL dump this manifest describes (Adonis Lucid `dumpPath`). */
77
+ dumpPath: string;
78
+ generatedAt: string;
79
+ schemaTableName: string;
80
+ /**
81
+ * Lucid tracks migration file versions in a second table; atlas's single
82
+ * `ream_migrations` (name+batch) has no equivalent, so this is always `null`.
83
+ * Kept for manifest-shape parity with Lucid.
84
+ */
85
+ schemaVersionsTableName: string | null;
86
+ /**
87
+ * Migration files collapsed into this dump (only when `--prune` deleted them).
88
+ * Lets the runner tell a deliberately-squashed migration from a missing file.
89
+ */
90
+ squashedMigrationNames: string[];
91
+ }
92
+
93
+ /** A foreign-key constraint introspected from Postgres, for the dump. */
94
+ export interface PgForeignKey {
95
+ name: string;
96
+ columns: string[];
97
+ foreignTable: string;
98
+ foreignColumns: string[];
99
+ onUpdate?: string;
100
+ onDelete?: string;
101
+ }
102
+
103
+ /** A column shape the pure Postgres renderers accept (subset of IntrospectedColumn). */
104
+ interface DumpColumn {
105
+ name: string;
106
+ type: string;
107
+ nullable: boolean;
108
+ primaryKey: boolean;
109
+ }
110
+
111
+ const pgQuote = (id: string): string => `"${id}"`;
112
+
113
+ /** Map a `pg_constraint.confupdtype`/`confdeltype` char to its SQL action. */
114
+ function pgActionCode(code: string | null): string | undefined {
115
+ switch (code) {
116
+ case "c":
117
+ return "CASCADE";
118
+ case "n":
119
+ return "SET NULL";
120
+ case "d":
121
+ return "SET DEFAULT";
122
+ case "r":
123
+ return "RESTRICT";
124
+ default:
125
+ return undefined; // 'a' NO ACTION (default) — omit
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Render `CREATE TABLE` from introspected columns + primary key (Postgres dump).
131
+ * Pure — no DB access — so the assembly is unit-testable with fixture columns.
132
+ */
133
+ export function renderPgCreateTable(
134
+ table: string,
135
+ columns: DumpColumn[],
136
+ ): string {
137
+ const cols = columns.map((c) => {
138
+ const notNull = c.nullable ? "" : " NOT NULL";
139
+ return ` ${pgQuote(c.name)} ${c.type}${notNull}`;
140
+ });
141
+ const pks = columns.filter((c) => c.primaryKey).map((c) => pgQuote(c.name));
142
+ if (pks.length > 0) cols.push(` PRIMARY KEY (${pks.join(", ")})`);
143
+ return `CREATE TABLE ${pgQuote(table)} (\n${cols.join(",\n")}\n);`;
144
+ }
145
+
146
+ /**
147
+ * Render an `ALTER TABLE … ADD CONSTRAINT … FOREIGN KEY` from an introspected
148
+ * {@link PgForeignKey} (Postgres dump). Pure — unit-testable. `ON UPDATE` /
149
+ * `ON DELETE` are emitted only when the rule is not the default `NO ACTION`.
150
+ */
151
+ export function renderPgForeignKeyDdl(table: string, fk: PgForeignKey): string {
152
+ const cols = fk.columns.map(pgQuote).join(", ");
153
+ const fcols = fk.foreignColumns.map(pgQuote).join(", ");
154
+ const nonDefault = (rule?: string) =>
155
+ rule && rule.toUpperCase() !== "NO ACTION" ? rule.toUpperCase() : undefined;
156
+ const onUpdate = nonDefault(fk.onUpdate);
157
+ const onDelete = nonDefault(fk.onDelete);
158
+ return (
159
+ `ALTER TABLE ${pgQuote(table)} ADD CONSTRAINT ${pgQuote(fk.name)} ` +
160
+ `FOREIGN KEY (${cols}) REFERENCES ${pgQuote(fk.foreignTable)} (${fcols})` +
161
+ (onUpdate ? ` ON UPDATE ${onUpdate}` : "") +
162
+ (onDelete ? ` ON DELETE ${onDelete}` : "") +
163
+ ";"
164
+ );
165
+ }
166
+
167
+ export interface SchemaDumperOptions {
168
+ /** Logical connection name — used in the default dump/manifest file names. Default `"default"`. */
169
+ connectionName?: string;
170
+ /**
171
+ * Explicit SQL dump FILE path (Adonis Lucid `--path`). Wins over
172
+ * `outputDir`/`connectionName`; the manifest is written beside it as
173
+ * `<name>.meta.json`.
174
+ */
175
+ dumpPath?: string;
176
+ /** Directory for the default `{connection}-schema.sql` when `dumpPath` is unset. Default `"database/schema"`. */
177
+ outputDir?: string;
178
+ /** Migration directory, required for `--prune` (the files it deletes). */
179
+ migrationsDir?: string;
180
+ /** Migration bookkeeping table name. Default `"ream_migrations"`. */
181
+ schemaTableName?: string;
182
+ /**
183
+ * Collapse the migration history into this dump: after a successful dump,
184
+ * delete every file in `migrationsDir` and record their names in the manifest.
185
+ */
186
+ prune?: boolean;
187
+ /** Manifest timestamp; defaults to the current time. Pass one for deterministic tests. */
188
+ generatedAt?: string;
189
+ }
190
+
191
+ export interface SchemaDumpResult {
192
+ dumpPath: string;
193
+ metaPath: string;
194
+ tableCount: number;
195
+ }
196
+
197
+ /**
198
+ * Dumps a database's schema to `{outputDir}/{connection}-schema.sql` and a
199
+ * `.meta.json` manifest. Errors are captured on `.error` (Lucid parity) rather
200
+ * than thrown, so a CLI can report cleanly; `.result` holds the output paths on
201
+ * success.
202
+ */
203
+ export class SchemaDumper {
204
+ readonly #db: AsyncDatabaseConnection;
205
+ readonly #options: Required<
206
+ Omit<SchemaDumperOptions, "generatedAt" | "dumpPath">
207
+ > & { generatedAt?: string; dumpPath?: string };
208
+ #result?: SchemaDumpResult;
209
+ #error?: Error;
210
+
211
+ constructor(db: AsyncDatabaseConnection, options: SchemaDumperOptions = {}) {
212
+ this.#db = db;
213
+ this.#options = {
214
+ connectionName: options.connectionName ?? "default",
215
+ dumpPath: options.dumpPath,
216
+ outputDir: options.outputDir ?? "database/schema",
217
+ migrationsDir: options.migrationsDir ?? "",
218
+ schemaTableName: options.schemaTableName ?? "ream_migrations",
219
+ prune: options.prune ?? false,
220
+ generatedAt: options.generatedAt,
221
+ };
222
+ }
223
+
224
+ get result(): SchemaDumpResult | undefined {
225
+ return this.#result;
226
+ }
227
+
228
+ get error(): Error | undefined {
229
+ return this.#error;
230
+ }
231
+
232
+ /** Resolved SQL dump file path — the explicit `dumpPath` or the default per-connection name. */
233
+ get dumpPath(): string {
234
+ return (
235
+ this.#options.dumpPath ??
236
+ path.join(
237
+ this.#options.outputDir,
238
+ `${this.#options.connectionName}-schema.sql`,
239
+ )
240
+ );
241
+ }
242
+
243
+ /** Manifest path — the dump path with its extension swapped for `.meta.json`. */
244
+ get metaPath(): string {
245
+ return schemaDumpManifestPath(this.dumpPath);
246
+ }
247
+
248
+ async run(): Promise<void> {
249
+ try {
250
+ const sql = await this.#buildDump();
251
+ const dumpPath = this.dumpPath;
252
+ const metaPath = this.metaPath;
253
+ await fsp.mkdir(path.dirname(dumpPath), { recursive: true });
254
+ await fsp.writeFile(dumpPath, sql);
255
+
256
+ const squashed = this.#options.prune ? await this.#prune() : [];
257
+ const manifest: SchemaDumpManifest = {
258
+ version: 1,
259
+ connection: this.#options.connectionName,
260
+ dialect: this.#db.dialect,
261
+ dumpPath,
262
+ generatedAt: this.#options.generatedAt ?? nowIso(),
263
+ schemaTableName: this.#options.schemaTableName,
264
+ schemaVersionsTableName: null,
265
+ squashedMigrationNames: squashed,
266
+ };
267
+ await fsp.writeFile(metaPath, `${JSON.stringify(manifest, null, 2)}\n`);
268
+ this.#result = {
269
+ dumpPath,
270
+ metaPath,
271
+ tableCount: (sql.match(/CREATE TABLE/gi) ?? []).length,
272
+ };
273
+ } catch (err) {
274
+ this.#error = err instanceof Error ? err : new Error(String(err));
275
+ }
276
+ }
277
+
278
+ /** Serialise DDL for every table (incl. the migrations table) + its bookkeeping rows. */
279
+ async #buildDump(): Promise<string> {
280
+ const dialect = this.#db.dialect;
281
+ const header =
282
+ "-- Generated by @c9up/atlas schema:dump — DO NOT EDIT.\n" +
283
+ `-- connection=${this.#options.connectionName} dialect=${dialect}\n\n`;
284
+ const ddl = await this.#dumpDdl(dialect);
285
+ const data = await this.#dumpMigrationRows();
286
+ return header + ddl + data;
287
+ }
288
+
289
+ /** Dialect-specific DDL for all tables (framework tables included, for the dump). */
290
+ async #dumpDdl(dialect: AtlasDialect): Promise<string> {
291
+ // The migration LOCK table is transient infrastructure, never schema — its
292
+ // presence in the dump would collide with the runner re-creating it.
293
+ const lockTable = `${this.#options.schemaTableName}_lock`;
294
+
295
+ if (dialect === "sqlite") {
296
+ // sqlite_master carries the exact CREATE statements (tables + indexes).
297
+ const rows = await this.#db.query<{
298
+ name: string;
299
+ sql: string | null;
300
+ }>(
301
+ "SELECT name, sql FROM sqlite_master " +
302
+ "WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%' " +
303
+ "ORDER BY (type = 'table') DESC, name",
304
+ );
305
+ return `${rows
306
+ .filter((r) => r.sql && r.name !== lockTable)
307
+ .map((r) => `${r.sql};`)
308
+ .join("\n\n")}\n\n`;
309
+ }
310
+
311
+ const tables = (
312
+ await listUserTables(this.#db, dialect, {
313
+ includeFrameworkTables: true,
314
+ })
315
+ ).filter((t) => t !== lockTable);
316
+ if (dialect === "mysql") {
317
+ // SHOW CREATE TABLE gives the exact DDL, indexes and constraints included.
318
+ const out: string[] = [];
319
+ for (const table of tables.sort()) {
320
+ let rows: Array<Record<string, string>>;
321
+ try {
322
+ rows = await this.#db.query<Record<string, string>>(
323
+ `SHOW CREATE TABLE \`${table}\``,
324
+ );
325
+ } catch {
326
+ // The table vanished between listing and dumping (concurrent DDL) —
327
+ // skip it, mirroring the postgres path (introspectTable → null).
328
+ continue;
329
+ }
330
+ const create = rows[0]?.["Create Table"];
331
+ if (create) out.push(`${create};`);
332
+ }
333
+ return `${out.join("\n\n")}\n\n`;
334
+ }
335
+
336
+ // postgres: reconstruct from catalog introspection (Adonis Lucid introspects
337
+ // the connection — it does NOT shell out to pg_dump). Per table: CREATE
338
+ // TABLE (columns + PK) + foreign keys + CHECK constraints + indexes; then
339
+ // the schema's views. Proven against real PostgreSQL (podman) in
340
+ // schema-dump-pg.test.ts.
341
+ const out: string[] = [];
342
+ for (const table of tables.sort()) {
343
+ const columns = await introspectTable(this.#db, dialect, table);
344
+ if (!columns) continue;
345
+ out.push(renderPgCreateTable(table, columns));
346
+ for (const fk of await this.#introspectPgForeignKeys(table)) {
347
+ out.push(renderPgForeignKeyDdl(table, fk));
348
+ }
349
+ // CHECK constraints — pg_get_constraintdef gives the exact `CHECK (...)`.
350
+ for (const chk of await this.#introspectPgChecks(table)) {
351
+ out.push(
352
+ `ALTER TABLE ${pgQuote(table)} ADD CONSTRAINT ${pgQuote(chk.name)} ${chk.def};`,
353
+ );
354
+ }
355
+ // `indexdef` is the exact CREATE INDEX statement; pass it through.
356
+ for (const indexDef of await this.#introspectPgIndexes(table)) {
357
+ out.push(`${indexDef};`);
358
+ }
359
+ }
360
+ // Views + materialized views (schema-level, not in the base-table list).
361
+ for (const view of await this.#introspectPgViews()) {
362
+ const kind = view.materialized ? "MATERIALIZED VIEW" : "VIEW";
363
+ out.push(
364
+ `CREATE ${kind} ${pgQuote(view.name)} AS\n${view.definition.replace(/;\s*$/, "")};`,
365
+ );
366
+ }
367
+ return `${out.join("\n\n")}\n\n`;
368
+ }
369
+
370
+ /**
371
+ * FK constraints for `table`, grouped per constraint (Postgres). Uses
372
+ * `pg_constraint` with `unnest(conkey, confkey) WITH ORDINALITY` so the local
373
+ * and foreign columns of a COMPOSITE FK are paired by position — the
374
+ * information_schema kcu×ccu join produces a cartesian product and mismaps
375
+ * multi-column keys.
376
+ */
377
+ async #introspectPgForeignKeys(table: string): Promise<PgForeignKey[]> {
378
+ const rows = await this.#db.query<{
379
+ constraint_name: string;
380
+ column_name: string;
381
+ foreign_table: string;
382
+ foreign_column: string;
383
+ update_type: string | null;
384
+ delete_type: string | null;
385
+ }>(
386
+ // ::text on every column — pg_catalog uses the `name` and `"char"` types
387
+ // which atlas's strict sqlx decode rejects as Option<String>.
388
+ `SELECT con.conname::text AS constraint_name,
389
+ att.attname::text AS column_name,
390
+ fatt.attname::text AS foreign_column,
391
+ fcl.relname::text AS foreign_table,
392
+ con.confupdtype::text AS update_type,
393
+ con.confdeltype::text AS delete_type
394
+ FROM pg_constraint con
395
+ JOIN pg_class cl ON cl.oid = con.conrelid
396
+ JOIN pg_class fcl ON fcl.oid = con.confrelid
397
+ JOIN LATERAL unnest(con.conkey, con.confkey) WITH ORDINALITY
398
+ AS keys(conkey, confkey, ord) ON true
399
+ JOIN pg_attribute att
400
+ ON att.attrelid = con.conrelid AND att.attnum = keys.conkey
401
+ JOIN pg_attribute fatt
402
+ ON fatt.attrelid = con.confrelid AND fatt.attnum = keys.confkey
403
+ WHERE con.contype = 'f' AND cl.relname = $1
404
+ AND cl.relnamespace = current_schema()::regnamespace
405
+ ORDER BY con.conname, keys.ord`,
406
+ [table],
407
+ );
408
+ const byName = new Map<string, PgForeignKey>();
409
+ for (const r of rows) {
410
+ const fk = byName.get(r.constraint_name) ?? {
411
+ name: r.constraint_name,
412
+ columns: [],
413
+ foreignTable: r.foreign_table,
414
+ foreignColumns: [],
415
+ onUpdate: pgActionCode(r.update_type),
416
+ onDelete: pgActionCode(r.delete_type),
417
+ };
418
+ fk.columns.push(r.column_name);
419
+ fk.foreignColumns.push(r.foreign_column);
420
+ byName.set(r.constraint_name, fk);
421
+ }
422
+ return [...byName.values()];
423
+ }
424
+
425
+ /** Non-primary index definitions for `table` (Postgres `pg_indexes.indexdef`). */
426
+ async #introspectPgIndexes(table: string): Promise<string[]> {
427
+ const rows = await this.#db.query<{ indexdef: string }>(
428
+ `SELECT i.indexdef FROM pg_indexes i
429
+ WHERE i.schemaname = current_schema() AND i.tablename = $1
430
+ AND NOT EXISTS (
431
+ SELECT 1 FROM pg_constraint c
432
+ WHERE c.conname = i.indexname AND c.contype = 'p'
433
+ )
434
+ ORDER BY i.indexname`,
435
+ [table],
436
+ );
437
+ return rows.map((r) => r.indexdef).filter((d): d is string => Boolean(d));
438
+ }
439
+
440
+ /** CHECK constraints for `table` (Postgres) — `pg_get_constraintdef` is exact. */
441
+ async #introspectPgChecks(
442
+ table: string,
443
+ ): Promise<Array<{ name: string; def: string }>> {
444
+ const rows = await this.#db.query<{ name: string; def: string }>(
445
+ `SELECT con.conname::text AS name,
446
+ pg_get_constraintdef(con.oid)::text AS def
447
+ FROM pg_constraint con
448
+ JOIN pg_class cl ON cl.oid = con.conrelid
449
+ WHERE con.contype = 'c' AND cl.relname = $1
450
+ AND cl.relnamespace = current_schema()::regnamespace
451
+ ORDER BY con.conname`,
452
+ [table],
453
+ );
454
+ return rows.filter((r) => r.name && r.def);
455
+ }
456
+
457
+ /** Views + materialized views in the current schema (Postgres). */
458
+ async #introspectPgViews(): Promise<
459
+ Array<{ name: string; definition: string; materialized: boolean }>
460
+ > {
461
+ const views = await this.#db.query<{ name: string; definition: string }>(
462
+ `SELECT viewname::text AS name, definition::text AS definition
463
+ FROM pg_views WHERE schemaname = current_schema() ORDER BY viewname`,
464
+ );
465
+ const matviews = await this.#db.query<{ name: string; definition: string }>(
466
+ `SELECT matviewname::text AS name, definition::text AS definition
467
+ FROM pg_matviews WHERE schemaname = current_schema() ORDER BY matviewname`,
468
+ );
469
+ return [
470
+ ...views.map((v) => ({ ...v, materialized: false })),
471
+ ...matviews.map((v) => ({ ...v, materialized: true })),
472
+ ].filter((v) => v.name && v.definition);
473
+ }
474
+
475
+ /** `INSERT` the applied-migration rows so the runner sees them as applied. */
476
+ async #dumpMigrationRows(): Promise<string> {
477
+ const table = this.#options.schemaTableName;
478
+ let rows: Array<{ name: string; batch: number }>;
479
+ try {
480
+ rows = await this.#db.query<{ name: string; batch: number }>(
481
+ `SELECT name, batch FROM ${this.#quote(table)} ORDER BY id`,
482
+ );
483
+ } catch {
484
+ // No bookkeeping table yet (nothing migrated) — nothing to record.
485
+ return "";
486
+ }
487
+ if (rows.length === 0) return "";
488
+ const values = rows
489
+ .map((r) => `(${this.#literal(r.name)}, ${Number(r.batch)})`)
490
+ .join(",\n ");
491
+ return `INSERT INTO ${this.#quote(table)} (name, batch) VALUES\n ${values};\n`;
492
+ }
493
+
494
+ /** Delete every migration file and return the pruned names (`--prune`). */
495
+ async #prune(): Promise<string[]> {
496
+ if (!this.#options.migrationsDir) {
497
+ throw new Error("schema:dump --prune requires a migrationsDir");
498
+ }
499
+ const entries = (await fsp.readdir(this.#options.migrationsDir))
500
+ .filter(
501
+ (f) => (f.endsWith(".ts") || f.endsWith(".js")) && !f.endsWith(".d.ts"),
502
+ )
503
+ .sort();
504
+ for (const file of entries) {
505
+ await fsp.rm(path.join(this.#options.migrationsDir, file));
506
+ }
507
+ return entries.map((f) => f.replace(/\.(ts|js)$/, ""));
508
+ }
509
+
510
+ #quote(id: string): string {
511
+ return this.#db.dialect === "mysql" ? `\`${id}\`` : `"${id}"`;
512
+ }
513
+
514
+ /** Single-quote a string literal for the dump (doubling embedded quotes). */
515
+ #literal(value: string): string {
516
+ return `'${value.replace(/'/g, "''")}'`;
517
+ }
518
+ }
@@ -22,6 +22,7 @@
22
22
  */
23
23
 
24
24
  import * as fsp from "node:fs/promises";
25
+ import * as path from "node:path";
25
26
  import { pathToFileURL } from "node:url";
26
27
  import type { DatabaseConnection } from "../BaseRepository.js";
27
28
  import { AtlasError } from "../errors.js";
@@ -40,10 +41,22 @@ import {
40
41
  export abstract class BaseSeeder {
41
42
  protected db: DatabaseConnection;
42
43
 
44
+ /**
45
+ * Environments this seeder is allowed to run in (Adonis Lucid
46
+ * `static environment`). When set, `runSeederDirectory({ environment })`
47
+ * skips it unless the current environment is listed. Unset = every env.
48
+ */
49
+ static environment?: string[];
50
+
43
51
  constructor(db: DatabaseConnection) {
44
52
  this.db = db;
45
53
  }
46
54
 
55
+ /** Alias of {@link db} — Adonis Lucid seeders expose the connection as `this.client`. */
56
+ protected get client(): DatabaseConnection {
57
+ return this.db;
58
+ }
59
+
47
60
  /** The seeder body. Should be idempotent — `repo.upsert(...)` is the recommended pattern. */
48
61
  abstract run(): Promise<void> | void;
49
62
  }
@@ -77,7 +90,14 @@ export async function runSeeders(seeders: BaseSeeder[]): Promise<void> {
77
90
  export async function runSeederDirectory(
78
91
  dir: string,
79
92
  db: DatabaseConnection,
80
- options?: { files?: readonly string[] },
93
+ options?: {
94
+ /** Run only these seeders, by base name OR full/relative file path (Lucid `--files`). */
95
+ files?: readonly string[];
96
+ /** Sort files numerically (`2_x` before `10_x`) instead of lexicographically (Lucid `naturalSort`). */
97
+ naturalSort?: boolean;
98
+ /** The current environment — skips a seeder whose `static environment` excludes it (Lucid). */
99
+ environment?: string;
100
+ },
81
101
  ): Promise<string[]> {
82
102
  if (!(await pathExists(dir))) {
83
103
  throw new AtlasError(
@@ -89,16 +109,23 @@ export async function runSeederDirectory(
89
109
  );
90
110
  }
91
111
 
92
- const allFiles = (await fsp.readdir(dir))
93
- .filter(
94
- (f) => (f.endsWith(".ts") || f.endsWith(".js")) && !f.endsWith(".d.ts"),
95
- )
96
- .sort();
97
-
98
- const selected = options?.files
99
- ? allFiles.filter((f) =>
100
- options.files?.includes(f.replace(/\.(ts|js)$/, "")),
112
+ const files = (await fsp.readdir(dir)).filter(
113
+ (f) => (f.endsWith(".ts") || f.endsWith(".js")) && !f.endsWith(".d.ts"),
114
+ );
115
+ // naturalSort compares embedded numbers by value; the default is lexicographic.
116
+ const allFiles = options?.naturalSort
117
+ ? files.sort((a, b) =>
118
+ a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" }),
101
119
  )
120
+ : files.sort();
121
+
122
+ // `--files` matches a base name (`UserSeeder`) OR a full/relative path
123
+ // (`database/seeders/UserSeeder.ts`) — take each entry's basename sans ext.
124
+ const wanted = options?.files?.map((f) =>
125
+ path.basename(f).replace(/\.(ts|js)$/, ""),
126
+ );
127
+ const selected = wanted
128
+ ? allFiles.filter((f) => wanted.includes(f.replace(/\.(ts|js)$/, "")))
102
129
  : allFiles;
103
130
 
104
131
  const executed: string[] = [];
@@ -125,6 +152,16 @@ export async function runSeederDirectory(
125
152
  );
126
153
  }
127
154
 
155
+ // Skip a seeder whose `static environment` excludes the current one (Lucid).
156
+ const allowed: unknown = SeederClass.environment;
157
+ if (
158
+ options?.environment &&
159
+ Array.isArray(allowed) &&
160
+ !allowed.includes(options.environment)
161
+ ) {
162
+ continue;
163
+ }
164
+
128
165
  const instance: BaseSeeder = new SeederClass(db);
129
166
  await instance.run();
130
167
  executed.push(file.replace(/\.(ts|js)$/, ""));