@rebasepro/server-postgres 0.18.0 → 0.18.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 (70) hide show
  1. package/dist/BranchService-ByLfM_Tt.js +360 -0
  2. package/dist/BranchService-ByLfM_Tt.js.map +1 -0
  3. package/dist/__vite-browser-external-BnuHet1e.js +46 -0
  4. package/dist/__vite-browser-external-BnuHet1e.js.map +1 -0
  5. package/dist/arg-Dni7MzLB.js +87 -0
  6. package/dist/arg-Dni7MzLB.js.map +1 -0
  7. package/dist/auth-users-columns-D2LBFrMH.js +116 -0
  8. package/dist/auth-users-columns-D2LBFrMH.js.map +1 -0
  9. package/dist/backup-cli-oM9WQVlV.js +408 -0
  10. package/dist/backup-cli-oM9WQVlV.js.map +1 -0
  11. package/dist/backup-service-Br3tqwsy.js +715 -0
  12. package/dist/backup-service-Br3tqwsy.js.map +1 -0
  13. package/dist/cli-errors-DxN4yzdH.js +657 -0
  14. package/dist/cli-errors-DxN4yzdH.js.map +1 -0
  15. package/dist/cli-output-CNdMql-L.js +47 -0
  16. package/dist/cli-output-CNdMql-L.js.map +1 -0
  17. package/dist/cli.js +2020 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/{collection-index-C7LopMyX.js → collection-index-BHgT_TKd.js} +32 -3
  20. package/dist/{collection-index-C7LopMyX.js.map → collection-index-BHgT_TKd.js.map} +1 -1
  21. package/dist/{collections-schema-version-BMeu3cgv.js → collections-schema-version-T6cGvrDi.js} +1 -2
  22. package/dist/{collections-schema-version-BMeu3cgv.js.map → collections-schema-version-T6cGvrDi.js.map} +1 -1
  23. package/dist/databasePoolManager-Bj5FbeAs.js +349 -0
  24. package/dist/databasePoolManager-Bj5FbeAs.js.map +1 -0
  25. package/dist/doctor-D7HWmGzs.js +974 -0
  26. package/dist/doctor-D7HWmGzs.js.map +1 -0
  27. package/dist/{ensure-collection-policies-CagOzEPS.js → ensure-collection-policies-2c2NV8zX.js} +5 -25
  28. package/dist/ensure-collection-policies-2c2NV8zX.js.map +1 -0
  29. package/dist/ensure-collection-tables-D4wJzM4y.js +820 -0
  30. package/dist/ensure-collection-tables-D4wJzM4y.js.map +1 -0
  31. package/dist/ensure-tables-CIojK-Rx.js +650 -0
  32. package/dist/ensure-tables-CIojK-Rx.js.map +1 -0
  33. package/dist/generate-drizzle-schema-CwEgVyjO.js +123 -0
  34. package/dist/generate-drizzle-schema-CwEgVyjO.js.map +1 -0
  35. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +528 -0
  36. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +1 -0
  37. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js +1312 -0
  38. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +1 -0
  39. package/dist/generated-schema-staleness-DQdfJl2A.js +157 -0
  40. package/dist/generated-schema-staleness-DQdfJl2A.js.map +1 -0
  41. package/dist/index.es.js +849 -10807
  42. package/dist/index.es.js.map +1 -1
  43. package/dist/introspect-db-logic-C6LQdTxj.js +1378 -0
  44. package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -0
  45. package/dist/policy-drift-kNJ6X2Uj.js +313 -0
  46. package/dist/policy-drift-kNJ6X2Uj.js.map +1 -0
  47. package/dist/rls-bootstrap-sql-DLDnUjtx.js +7092 -0
  48. package/dist/rls-bootstrap-sql-DLDnUjtx.js.map +1 -0
  49. package/dist/{rls-enforcement-ClJoTboT.js → rls-enforcement-DeYEjYLt.js} +14 -3
  50. package/dist/{rls-enforcement-ClJoTboT.js.map → rls-enforcement-DeYEjYLt.js.map} +1 -1
  51. package/dist/schema/bin/generate-drizzle-schema.d.ts +1 -0
  52. package/dist/schema/bin/generate-postgres-ddl.d.ts +1 -0
  53. package/dist/schema/doctor-cli.js +129 -0
  54. package/dist/schema/doctor-cli.js.map +1 -0
  55. package/dist/schema/generate-drizzle-schema.d.ts +1 -1
  56. package/dist/schema/generate-drizzle-schema.js +24 -0
  57. package/dist/schema/generate-drizzle-schema.js.map +1 -0
  58. package/dist/schema/generate-postgres-ddl.d.ts +1 -1
  59. package/dist/schema/generate-postgres-ddl.js +138 -0
  60. package/dist/schema/generate-postgres-ddl.js.map +1 -0
  61. package/dist/schema/introspect-db.js +866 -0
  62. package/dist/schema/introspect-db.js.map +1 -0
  63. package/dist/source-Br7L7GOI.js +410 -0
  64. package/dist/source-Br7L7GOI.js.map +1 -0
  65. package/dist/vector-index-DkWxm2de.js +309 -0
  66. package/dist/vector-index-DkWxm2de.js.map +1 -0
  67. package/package.json +7 -7
  68. package/dist/ensure-collection-policies-CagOzEPS.js.map +0 -1
  69. package/dist/ensure-collection-tables-DbAhrjnG.js +0 -2732
  70. package/dist/ensure-collection-tables-DbAhrjnG.js.map +0 -1
@@ -0,0 +1,974 @@
1
+ import { createRequire as __createRequire } from "module";
2
+ __createRequire(import.meta.url);
3
+ import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
4
+ import { i as diagnoseDbError, n as deepestErrorMessage, o as parseHostInfo } from "./cli-errors-DxN4yzdH.js";
5
+ import { t as generateSchema } from "./generate-drizzle-schema-logic-so2Z_lyC.js";
6
+ import { n as outError, t as out } from "./cli-output-CNdMql-L.js";
7
+ import { t as chalk } from "./source-Br7L7GOI.js";
8
+ import { loadCollectionsFromDirectory } from "@rebasepro/server";
9
+ import { computeSchemaVersion, isManyToMany, isPostgresCollectionConfig } from "@rebasepro/types";
10
+ import { fieldKeyForColumn, findRelation, getTableName, isRelationRequired, relationalCollections, resolveCollectionRelations, sortCollectionsBySlug } from "@rebasepro/common";
11
+ import { toSnakeCase } from "@rebasepro/utils";
12
+ import * as fs$1 from "fs";
13
+ import { promises } from "fs";
14
+ //#region ../codegen/dist/index.es.js
15
+ /**
16
+ * Utility functions for the SDK generator
17
+ */
18
+ /**
19
+ * Convert a slug/snake_case string to PascalCase
20
+ * e.g. "private_notes" → "PrivateNotes"
21
+ *
22
+ * Capitals already inside a word are meaningful and are kept: lowercasing the
23
+ * tail of every chunk turned "TestEntities" into "Testentities", which is what
24
+ * ended up in the generated type names. SHOUTING_CASE is the one shape where
25
+ * the tail is not meaningful, so it is folded down.
26
+ */
27
+ function toPascalCase(str) {
28
+ return str.split(/[_\-\s]+/).filter(Boolean).map((word) => {
29
+ const rest = /^[A-Z0-9]+$/.test(word) ? word.slice(1).toLowerCase() : word.slice(1);
30
+ return word.charAt(0).toUpperCase() + rest;
31
+ }).join("");
32
+ }
33
+ /**
34
+ * Convert a slug/snake_case string to camelCase
35
+ * e.g. "private_notes" → "privateNotes"
36
+ */
37
+ function toCamelCase(str) {
38
+ if (!/[_\-\s]/.test(str)) return str.charAt(0).toLowerCase() + str.slice(1);
39
+ const pascal = toPascalCase(str);
40
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
41
+ }
42
+ /**
43
+ * Convert a slug to a safe JS identifier
44
+ * e.g. "private-notes" → "privateNotes"
45
+ */
46
+ function toSafeIdentifier(str) {
47
+ return toCamelCase(str.replace(/[^a-zA-Z0-9_]/g, "_"));
48
+ }
49
+ /**
50
+ * A schema that cannot be expressed as a valid TypeScript file.
51
+ *
52
+ * Thrown rather than emitted. The generator used to concatenate whatever it was
53
+ * given, so a slug that collided with another one, or that was not an
54
+ * identifier, produced a file that either failed to compile or — worse —
55
+ * compiled while quietly routing one collection to another's slug.
56
+ */
57
+ var CodegenError = class extends Error {
58
+ constructor(message) {
59
+ super(message);
60
+ this.name = "CodegenError";
61
+ }
62
+ };
63
+ var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
64
+ /**
65
+ * A property name for the emitted TypeScript: verbatim when it is a valid
66
+ * identifier, quoted otherwise.
67
+ *
68
+ * Every key that reaches the output goes through here. Column names are not
69
+ * required to be identifiers — `"order"`, `"user id"`, a quoted Postgres
70
+ * identifier — and the previous behaviour of camel-casing them into shape
71
+ * renamed the column in the type while the wire kept the original, so the
72
+ * generated `Row` described fields that did not exist.
73
+ */
74
+ function emitKey(key) {
75
+ return IDENTIFIER.test(key) ? key : JSON.stringify(key);
76
+ }
77
+ /**
78
+ * A string literal, escaped.
79
+ *
80
+ * `"${value}"` was the previous form. A value containing a quote closed the
81
+ * literal early, which at best broke the file and at worst let a slug from a
82
+ * remote contract inject top-level statements into a file the developer
83
+ * compiles and bundles.
84
+ */
85
+ function emitString(value) {
86
+ return JSON.stringify(value);
87
+ }
88
+ /** The `id`s of an enum declared as an array, an array of `{ id }`, or an object map. */
89
+ function enumIds(raw) {
90
+ if (Array.isArray(raw)) return raw.map((entry) => entry && typeof entry === "object" ? entry.id : entry);
91
+ if (raw && typeof raw === "object") return Object.keys(raw);
92
+ return [];
93
+ }
94
+ function propertyToTypeScriptType(prop) {
95
+ switch (prop.type) {
96
+ case "string": {
97
+ const sp = prop;
98
+ if (sp.enum) {
99
+ const ids = enumIds(sp.enum);
100
+ if (ids.length === 0) return "string";
101
+ return ids.map((v) => emitString(String(v))).join(" | ");
102
+ }
103
+ return "string";
104
+ }
105
+ case "number": {
106
+ const np = prop;
107
+ if (np.enum) {
108
+ const ids = enumIds(np.enum);
109
+ const numbers = ids.map(Number);
110
+ if (ids.length === 0 || numbers.some((n) => !Number.isFinite(n))) return "number";
111
+ return numbers.map((n) => String(n)).join(" | ");
112
+ }
113
+ return "number";
114
+ }
115
+ case "boolean": return "boolean";
116
+ case "date": return "string";
117
+ case "geopoint": return "{ latitude: number; longitude: number; }";
118
+ case "reference": return "string | number";
119
+ case "relation": return "string | number";
120
+ case "map": {
121
+ const mapProp = prop;
122
+ if (mapProp.properties) return `{ ${Object.entries(mapProp.properties).map(([k, v]) => {
123
+ const child = v;
124
+ const optional = !child.validation?.required;
125
+ const type = propertyToTypeScriptType(child);
126
+ return `${emitKey(k)}${optional ? "?" : ""}: ${optional ? `${type} | null` : type};`;
127
+ }).join(" ")} }`;
128
+ return "Record<string, unknown>";
129
+ }
130
+ case "array": {
131
+ const arrProp = prop;
132
+ if (arrProp.of) return `Array<${propertyToTypeScriptType(arrProp.of)}>`;
133
+ return "Array<unknown>";
134
+ }
135
+ case "vector": return "number[]";
136
+ case "binary": return "string";
137
+ default: return "unknown";
138
+ }
139
+ }
140
+ /**
141
+ * Unwrap a relation target that arrived as a module namespace rather than the
142
+ * collection itself — `target: () => import("./authors")` is a common slip.
143
+ */
144
+ function resolveTargetCollection(relation) {
145
+ try {
146
+ let target = relation.target();
147
+ if (target && (target.default || target.__esModule)) target = target.default ?? target;
148
+ return target;
149
+ } catch {
150
+ return;
151
+ }
152
+ }
153
+ /** The TypeScript type of a foreign key: whatever the target's primary key is. */
154
+ function foreignKeyType(relation) {
155
+ const target = resolveTargetCollection(relation);
156
+ if (!target?.properties) return "string | number";
157
+ const idProp = Object.entries(target.properties).find(([_, p]) => p.isId);
158
+ if (!idProp) return "string | number";
159
+ return idProp[1].type === "number" ? "number" : "string";
160
+ }
161
+ /** Whether a property is the collection's primary key. */
162
+ function isPrimaryKey(prop) {
163
+ return Boolean(prop.isId);
164
+ }
165
+ /**
166
+ * Whether the server assigns this primary key, so a write does not have to.
167
+ * `true` and `"manual"` both mean the caller supplies it.
168
+ */
169
+ function isAutoAssignedId(prop) {
170
+ const isId = prop.isId;
171
+ return Boolean(isId) && isId !== "manual" && isId !== true;
172
+ }
173
+ /**
174
+ * The type an *included* relation arrives as: the target's own row, inlined.
175
+ *
176
+ * This is what the read pipeline actually serves — `toRestRow` puts the
177
+ * target's flat columns where the relation was, and the SDK and the HTTP API
178
+ * both go through it. It is deliberately *not* a `{ __type: "relation" }`
179
+ * envelope: that shape is the admin's view-model and never reaches a
180
+ * developer's `find()`.
181
+ *
182
+ * Falls back to an open record when the target is not part of this generation
183
+ * run, since there is no `Row` to point at.
184
+ */
185
+ function includedRelationType(relation, accessors) {
186
+ const slug = resolveTargetCollection(relation)?.slug ?? relation.targetSlug;
187
+ const accessor = slug ? accessors.get(slug) : void 0;
188
+ const rowType = accessor ? `Database[${emitString(accessor)}]["Row"]` : "Record<string, unknown>";
189
+ return relation.cardinality === "many" ? `Array<${rowType}>` : rowType;
190
+ }
191
+ /**
192
+ * Map every slug to the property name it is reachable under on `client.data`.
193
+ *
194
+ * The accessor is a safe identifier because `client.data.myNotes` is the point
195
+ * of generating this at all, and `collectionsDictionary` maps it back to the
196
+ * slug the wire uses. Two slugs that safe down to the same identifier cannot
197
+ * both have it: the interface would not compile, and the dictionary — an object
198
+ * literal — would silently keep only the last, routing one collection's reads
199
+ * to the other's table. There is no defensible way to pick, so this refuses.
200
+ */
201
+ function buildAccessors(collections) {
202
+ const accessors = /* @__PURE__ */ new Map();
203
+ const bySafeName = /* @__PURE__ */ new Map();
204
+ for (const collection of collections) {
205
+ const slug = collection.slug;
206
+ if (typeof slug !== "string" || slug.length === 0) throw new CodegenError("A collection has no slug, so it has no name to generate a type for. Every collection needs a unique `slug`.");
207
+ const safe = toSafeIdentifier(slug);
208
+ if (safe.length === 0) throw new CodegenError(`The slug ${emitString(slug)} has no characters that can form a property name, so it cannot be reached as \`client.data.<name>\`. Use a slug containing letters, digits, underscores or dashes.`);
209
+ const existing = bySafeName.get(safe);
210
+ if (existing !== void 0) throw new CodegenError(`The collections ${emitString(existing)} and ${emitString(slug)} both generate the accessor "${safe}", so only one of them could be reached from the generated client and the other's reads would silently go to the wrong table. Rename one of the slugs.`);
211
+ bySafeName.set(safe, slug);
212
+ accessors.set(slug, safe);
213
+ }
214
+ return accessors;
215
+ }
216
+ /** One emitted `key: type;` line, already indented. */
217
+ function line(key, type, optional) {
218
+ return ` ${emitKey(key)}${optional ? "?" : ""}: ${type};`;
219
+ }
220
+ /**
221
+ * The keys `excludeFromApi` takes off the API surface — in *both* directions.
222
+ *
223
+ * `excludeFromApi` means one thing: the API surface does not mention this
224
+ * property. `Row` already honoured that; `Insert` and `Update` deliberately did
225
+ * not, on the reading that the column is stripped from responses rather than
226
+ * from writes. That left the generated types as the one place a password hash
227
+ * was still named, and it invited a client to send one. The server still
228
+ * *accepts* such a field on a write — this describes the surface, it does not
229
+ * add an enforcement point — but nothing generated advertises it.
230
+ *
231
+ * Keyed by the property name *and* by its column name, the same pair the
232
+ * server's `stripExcluded` deletes, so a foreign key or a relation addressed
233
+ * under the column name cannot put the property back.
234
+ */
235
+ function excludedApiKeys(properties) {
236
+ const excluded = /* @__PURE__ */ new Set();
237
+ for (const [key, rawProp] of Object.entries(properties)) {
238
+ const prop = rawProp;
239
+ if (!prop?.excludeFromApi) continue;
240
+ excluded.add(key);
241
+ if (prop.columnName) excluded.add(prop.columnName);
242
+ }
243
+ return excluded;
244
+ }
245
+ function generateTypedefs(input) {
246
+ const collections = sortCollectionsBySlug(input);
247
+ const accessors = buildAccessors(collections);
248
+ const lines = [
249
+ "/**",
250
+ " * This file was auto-generated by Rebase.",
251
+ " * Do not make direct changes to the file.",
252
+ " */",
253
+ "",
254
+ "export interface Database {"
255
+ ];
256
+ for (const collection of collections) {
257
+ const properties = collection.properties ?? {};
258
+ let resolvedRelations = {};
259
+ try {
260
+ resolvedRelations = resolveCollectionRelations(collection);
261
+ } catch (e) {
262
+ console.warn(`[rebase] Could not resolve the relations of "${collection.slug}", so its generated type has no relation fields and none of their foreign-key columns. This is usually a circular import in the collection files — make sure the target is \`() => otherCollection\` and not evaluated at module load.\n ${e instanceof Error ? e.message : String(e)}`);
263
+ }
264
+ const subcollections = collection.subcollections;
265
+ if (Array.isArray(subcollections) && subcollections.length > 0) console.warn(`[rebase] "${collection.slug}" declares ${subcollections.length} subcollection(s), which are not part of the generated Database: they are reached over a nested path (\`data/${collection.slug}/<id>/<relation>\`), not as a top-level accessor. Register a subcollection as a collection of its own if you want a typed accessor for it.`);
266
+ lines.push(` ${emitKey(accessors.get(collection.slug))}: {`);
267
+ lines.push(" Row: {");
268
+ const emittedKeys = /* @__PURE__ */ new Set();
269
+ const excluded = excludedApiKeys(properties);
270
+ for (const key of excluded) emittedKeys.add(key);
271
+ for (const [key, rawProp] of Object.entries(properties)) {
272
+ const prop = rawProp;
273
+ if (prop.type === "relation") continue;
274
+ if (excluded.has(key)) continue;
275
+ const tsType = propertyToTypeScriptType(prop);
276
+ const isRequired = Boolean(prop.validation?.required) || isPrimaryKey(prop);
277
+ lines.push(line(key, isRequired ? tsType : `${tsType} | null`, !isRequired));
278
+ emittedKeys.add(key);
279
+ }
280
+ for (const [relKey, relation] of Object.entries(resolvedRelations)) if (relation.kind === "belongsTo" && relation.localKey) {
281
+ const fkKey = fieldKeyForColumn(collection, relation.localKey);
282
+ if (emittedKeys.has(fkKey)) continue;
283
+ const fkType = foreignKeyType(relation);
284
+ const shadowedByInclude = relKey === fkKey;
285
+ const tsType = shadowedByInclude ? `${fkType} | ${includedRelationType(relation, accessors)}` : fkType;
286
+ const isRequired = isRelationRequired(collection, relation) && !shadowedByInclude;
287
+ lines.push(line(fkKey, isRequired ? tsType : `${tsType} | null`, !isRequired));
288
+ emittedKeys.add(fkKey);
289
+ }
290
+ for (const [key, relation] of Object.entries(resolvedRelations)) {
291
+ if (emittedKeys.has(key)) continue;
292
+ lines.push(line(key, includedRelationType(relation, accessors), true));
293
+ emittedKeys.add(key);
294
+ }
295
+ for (const [key, rawProp] of Object.entries(properties)) {
296
+ if (rawProp.type !== "relation") continue;
297
+ if (emittedKeys.has(key)) continue;
298
+ lines.push(line(key, "Record<string, unknown>", true));
299
+ emittedKeys.add(key);
300
+ }
301
+ lines.push(" };");
302
+ lines.push(" Insert: {");
303
+ emittedKeys.clear();
304
+ for (const key of excluded) emittedKeys.add(key);
305
+ for (const [key, rawProp] of Object.entries(properties)) {
306
+ const prop = rawProp;
307
+ if (prop.type === "relation") continue;
308
+ if (excluded.has(key)) continue;
309
+ const tsType = propertyToTypeScriptType(prop);
310
+ const isOptional = !prop.validation?.required || isAutoAssignedId(prop);
311
+ lines.push(line(key, tsType, isOptional));
312
+ emittedKeys.add(key);
313
+ }
314
+ emitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, false);
315
+ lines.push(" };");
316
+ lines.push(" Update: {");
317
+ emittedKeys.clear();
318
+ for (const key of excluded) emittedKeys.add(key);
319
+ for (const [key, rawProp] of Object.entries(properties)) {
320
+ const prop = rawProp;
321
+ if (prop.type === "relation") continue;
322
+ if (isPrimaryKey(prop)) continue;
323
+ if (excluded.has(key)) continue;
324
+ lines.push(line(key, propertyToTypeScriptType(prop), true));
325
+ emittedKeys.add(key);
326
+ }
327
+ emitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, true);
328
+ lines.push(" };");
329
+ lines.push(" };");
330
+ }
331
+ lines.push("}");
332
+ lines.push("");
333
+ lines.push("export type CollectionName = keyof Database;");
334
+ lines.push("");
335
+ lines.push("export const collectionsDictionary = {");
336
+ for (const collection of collections) lines.push(` ${emitKey(accessors.get(collection.slug))}: ${emitString(collection.slug)},`);
337
+ lines.push("} as const;");
338
+ lines.push("");
339
+ lines.push("export type CollectionsDictionary = typeof collectionsDictionary;");
340
+ lines.push("");
341
+ return lines.join("\n");
342
+ }
343
+ /**
344
+ * The two ways a write can name a `belongsTo` target, both of which the server
345
+ * accepts: the foreign key under its own wire name (`{ authorId: 5 }`, which
346
+ * passes through to the `author_id` column untouched) and the relation
347
+ * *property* (`{ author: 5 }`, which the write transformer maps onto that
348
+ * column).
349
+ *
350
+ * Only the first was generated, so the documented and idiomatic write shape was
351
+ * a type error.
352
+ *
353
+ * The second form is emitted under the **property key**, not the resolved
354
+ * relation name, because that is what the transformer keys off: it looks the
355
+ * payload key up in `properties` and only treats it as a relation if what it
356
+ * finds there is one. A relation whose `relationName` differs from its property
357
+ * key is reachable as the property and not as the name, so emitting the name
358
+ * would have offered a key that writes to a column that does not exist.
359
+ */
360
+ function emitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, allOptional) {
361
+ const emit = (key, relation) => {
362
+ if (emittedKeys.has(key)) return;
363
+ const optional = allOptional || !isRelationRequired(collection, relation);
364
+ lines.push(line(key, foreignKeyType(relation), optional));
365
+ emittedKeys.add(key);
366
+ };
367
+ for (const relation of Object.values(resolvedRelations)) if (relation.kind === "belongsTo" && relation.localKey) emit(fieldKeyForColumn(collection, relation.localKey), relation);
368
+ for (const [key, rawProp] of Object.entries(properties)) {
369
+ if (rawProp.type !== "relation") continue;
370
+ const relation = findRelation(resolvedRelations, key);
371
+ if (relation?.kind === "belongsTo" && relation.localKey) emit(key, relation);
372
+ }
373
+ }
374
+ //#endregion
375
+ //#region src/schema/doctor.ts
376
+ /**
377
+ * Rebase Schema Doctor — Three-way schema drift detection.
378
+ *
379
+ * Compares:
380
+ * 1. Collection definitions → Generated Drizzle schema (staleness check)
381
+ * 2. Collection definitions → Live PostgreSQL database (structural drift)
382
+ *
383
+ * Run via: rebase doctor
384
+ */
385
+ var doctor_exports = /* @__PURE__ */ __exportAll({
386
+ checkCollectionsVsDatabase: () => checkCollectionsVsDatabase,
387
+ checkCollectionsVsSchema: () => checkCollectionsVsSchema,
388
+ checkCollectionsVsSdk: () => checkCollectionsVsSdk,
389
+ getExpectedColumnType: () => getExpectedColumnType,
390
+ loadCollections: () => loadCollections,
391
+ missingExtensionIssues: () => missingExtensionIssues,
392
+ renderReport: () => renderReport,
393
+ runDoctor: () => runDoctor,
394
+ schemaStampIssues: () => schemaStampIssues
395
+ });
396
+ /**
397
+ * Resolve the SQL column name for a property.
398
+ * Uses the explicit `columnName` when set (e.g. from introspection),
399
+ * falling back to `toSnakeCase(propName)` for manually-authored collections.
400
+ */
401
+ var resolveColumnName = (propName, prop) => {
402
+ if (prop && "columnName" in prop && typeof prop.columnName === "string") return prop.columnName;
403
+ return toSnakeCase(propName);
404
+ };
405
+ function getExpectedColumnType(prop) {
406
+ switch (prop.type) {
407
+ case "string": {
408
+ const sp = prop;
409
+ if (sp.enum) return "USER-DEFINED";
410
+ if ("isId" in sp && sp.isId === "uuid") return "uuid";
411
+ if (sp.columnType === "uuid") return "uuid";
412
+ if (sp.columnType === "char") return "character";
413
+ if (sp.columnType === "varchar") return "character varying";
414
+ return "text";
415
+ }
416
+ case "number": {
417
+ const np = prop;
418
+ if (np.columnType) return {
419
+ serial: "integer",
420
+ bigserial: "bigint",
421
+ smallserial: "smallint"
422
+ }[np.columnType] ?? np.columnType;
423
+ if (np.validation?.integer || "isId" in np && np.isId) return "integer";
424
+ return "numeric";
425
+ }
426
+ case "boolean": return "boolean";
427
+ case "date": {
428
+ const dp = prop;
429
+ if (dp.columnType === "date") return "date";
430
+ if (dp.columnType === "time") return "time without time zone";
431
+ return "timestamp with time zone";
432
+ }
433
+ case "array": {
434
+ const ap = prop;
435
+ let colType = ap.columnType;
436
+ if (!colType && ap.of && !Array.isArray(ap.of)) {
437
+ const ofProp = ap.of;
438
+ if (ofProp.type === "string") colType = "text[]";
439
+ else if (ofProp.type === "number") colType = ofProp.validation?.integer ? "integer[]" : "numeric[]";
440
+ else if (ofProp.type === "boolean") colType = "boolean[]";
441
+ }
442
+ if (colType === "json") return "json";
443
+ if (colType === "jsonb") return "jsonb";
444
+ if (colType && colType.endsWith("[]")) return "ARRAY";
445
+ return "jsonb";
446
+ }
447
+ case "map":
448
+ if (prop.columnType === "json") return "json";
449
+ return "jsonb";
450
+ case "relation": return null;
451
+ case "reference": return "text";
452
+ case "vector": return "USER-DEFINED";
453
+ case "binary": return "bytea";
454
+ default: return null;
455
+ }
456
+ }
457
+ /**
458
+ * Re-exported so callers keep importing it from here, but the implementation is
459
+ * now shared with the runtime and the generators — a doctor that disagreed with
460
+ * the policy generator about which files are collections would compare the
461
+ * wrong thing.
462
+ */
463
+ async function loadCollections(collectionsPath) {
464
+ return loadCollectionsFromDirectory(collectionsPath);
465
+ }
466
+ async function checkCollectionsVsSchema(collections, schemaFilePath) {
467
+ const issues = [];
468
+ if (!fs$1.existsSync(schemaFilePath)) {
469
+ issues.push({
470
+ severity: "error",
471
+ category: "schema_stale",
472
+ message: "Generated schema file does not exist.",
473
+ fix: "Run `rebase schema generate`"
474
+ });
475
+ return {
476
+ passed: false,
477
+ issues
478
+ };
479
+ }
480
+ const postgresCollections = relationalCollections(collections);
481
+ if (postgresCollections.length === 0) return {
482
+ passed: true,
483
+ issues
484
+ };
485
+ try {
486
+ const expectedSchema = await generateSchema(postgresCollections);
487
+ const actualSchema = await promises.readFile(schemaFilePath, "utf-8");
488
+ const normalize = (s) => s.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").trim();
489
+ if (normalize(expectedSchema) !== normalize(actualSchema)) issues.push({
490
+ severity: "warning",
491
+ category: "schema_stale",
492
+ message: "Generated schema is out of date — collection definitions have changed since last generation.",
493
+ fix: "Run `rebase schema generate`"
494
+ });
495
+ } catch (err) {
496
+ const message = err instanceof Error ? err.message : String(err);
497
+ issues.push({
498
+ severity: "warning",
499
+ category: "schema_stale",
500
+ message: `Could not regenerate schema for comparison: ${message}`,
501
+ fix: "Run `rebase schema generate` to verify"
502
+ });
503
+ }
504
+ return {
505
+ passed: issues.length === 0,
506
+ issues
507
+ };
508
+ }
509
+ async function checkCollectionsVsSdk(collections, sdkFilePath) {
510
+ const issues = [];
511
+ if (!fs$1.existsSync(sdkFilePath)) {
512
+ issues.push({
513
+ severity: "info",
514
+ category: "sdk_not_generated",
515
+ message: "Typed SDK not generated (optional).",
516
+ fix: "Run `rebase generate-sdk` if you want typed collection access"
517
+ });
518
+ return {
519
+ passed: true,
520
+ issues,
521
+ notApplicable: "not generated (optional)"
522
+ };
523
+ }
524
+ try {
525
+ const expectedSdk = generateTypedefs(collections);
526
+ const actualSdk = await promises.readFile(sdkFilePath, "utf-8");
527
+ const normalize = (s) => s.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").trim();
528
+ if (normalize(expectedSdk) !== normalize(actualSdk)) issues.push({
529
+ severity: "warning",
530
+ category: "sdk_stale",
531
+ message: "Generated SDK types are out of date — collection definitions have changed since last SDK generation.",
532
+ fix: "Run `rebase generate-sdk`"
533
+ });
534
+ } catch (err) {
535
+ const message = err instanceof Error ? err.message : String(err);
536
+ if (err instanceof Error && err.name === "CodegenError") issues.push({
537
+ severity: "error",
538
+ category: "sdk_ungeneratable",
539
+ message: `The typed SDK cannot be generated from these collections: ${message}`,
540
+ fix: "Fix the collection definitions named above, then run `rebase generate-sdk`"
541
+ });
542
+ else issues.push({
543
+ severity: "warning",
544
+ category: "sdk_stale",
545
+ message: `Could not regenerate SDK types for comparison: ${message}`,
546
+ fix: "Run `rebase generate-sdk` to verify"
547
+ });
548
+ }
549
+ return {
550
+ passed: issues.length === 0,
551
+ issues
552
+ };
553
+ }
554
+ /**
555
+ * Extensions the collections need, that the database does not have installed.
556
+ *
557
+ * Only `vector` today, and only because a `{ type: "vector" }` property plans a
558
+ * column of a type that will not exist. Rebase installs it only where a project
559
+ * has said it may (`database({ extensions: ["vector"] })`), so the absence is
560
+ * often correct configuration and a wrong deployment — which is exactly why the
561
+ * report has to name it rather than the first failing INSERT.
562
+ *
563
+ * Exported for its test.
564
+ */
565
+ function missingExtensionIssues(collections, installed) {
566
+ if (!collections.some((collection) => Object.values(collection.properties ?? {}).some((property) => property?.type === "vector")) || installed.has("vector")) return [];
567
+ return [{
568
+ severity: "error",
569
+ category: "missing_table",
570
+ message: "A collection declares a `{ type: \"vector\" }` property and the pgvector extension is not installed on this database.",
571
+ fix: "Declare it — `database({ extensions: [\"vector\"] })` in config/resources.ts — and use an image that ships the library (the scaffold's pgvector/pgvector:pg18; a stock postgres:18 does not). Or install it once by hand: CREATE EXTENSION vector;"
572
+ }];
573
+ }
574
+ /**
575
+ * The stamp the runtime wrote, against the collections on disk.
576
+ *
577
+ * A stamp that disagrees means this database was provisioned from a different
578
+ * set of collections than the ones in this checkout — a colleague's branch, an
579
+ * older deploy, a tenant somebody else migrated. Everything else in this report
580
+ * is then describing a schema somebody else created, which is worth knowing
581
+ * before acting on any of it.
582
+ *
583
+ * A warning, not an error, and never a direction: the stamp is a hash, so it
584
+ * can say the two disagree and never which is ahead. `null` — never stamped —
585
+ * is not a finding: every database provisioned before the stamp existed reads
586
+ * that way, and so does every fresh one.
587
+ */
588
+ async function schemaStampIssues(pool, collections) {
589
+ let stamped = null;
590
+ try {
591
+ if (!(await pool.query(`SELECT to_regclass('"rebase"."schema_meta"') IS NOT NULL AS present`)).rows[0]?.present) return [];
592
+ stamped = (await pool.query(`SELECT value FROM "rebase"."schema_meta" WHERE key = 'collections_schema_version' LIMIT 1`)).rows[0]?.value ?? null;
593
+ } catch {
594
+ return [];
595
+ }
596
+ if (!stamped) return [];
597
+ const expected = computeSchemaVersion(collections);
598
+ if (stamped === expected) return [];
599
+ return [{
600
+ severity: "warning",
601
+ category: "schema_stale",
602
+ message: `This database was provisioned from a different set of collections (stamp ${stamped.slice(0, 12)}…, this checkout computes ${expected.slice(0, 12)}…).`,
603
+ fix: "A hash says the two disagree, never which is ahead. Check you are pointed at the database you think you are; then `rebase db push` to bring it to these collections."
604
+ }];
605
+ }
606
+ async function checkCollectionsVsDatabase(collections, databaseUrl) {
607
+ const issues = [];
608
+ const pgModule = await import("pg");
609
+ const { Pool } = pgModule.default ?? pgModule;
610
+ const pool = new Pool({ connectionString: databaseUrl });
611
+ const schemas = Array.from(/* @__PURE__ */ new Set([
612
+ "public",
613
+ "rebase",
614
+ ...relationalCollections(collections).filter(isPostgresCollectionConfig).map((c) => c.schema).filter((s) => !!s)
615
+ ]));
616
+ try {
617
+ const tablesResult = await pool.query(`SELECT table_schema, table_name
618
+ FROM information_schema.tables
619
+ WHERE table_schema = ANY($1) AND table_type = 'BASE TABLE'`, [schemas]);
620
+ const existingTables = new Set(tablesResult.rows.map((r) => r.table_schema === "public" ? r.table_name : `${r.table_schema}.${r.table_name}`));
621
+ const extensionsResult = await pool.query("SELECT extname FROM pg_extension");
622
+ const installedExtensions = new Set(extensionsResult.rows.map((r) => r.extname));
623
+ issues.push(...missingExtensionIssues(collections, installedExtensions));
624
+ issues.push(...await schemaStampIssues(pool, collections));
625
+ const columnsResult = await pool.query(`SELECT table_schema, table_name, column_name, data_type, is_nullable, udt_name
626
+ FROM information_schema.columns
627
+ WHERE table_schema = ANY($1)
628
+ ORDER BY table_schema, table_name, ordinal_position`, [schemas]);
629
+ const columnsByTable = /* @__PURE__ */ new Map();
630
+ for (const row of columnsResult.rows) {
631
+ const tableSchema = row.table_schema;
632
+ const tableName = row.table_name;
633
+ const key = tableSchema === "public" ? tableName : `${tableSchema}.${tableName}`;
634
+ if (!columnsByTable.has(key)) columnsByTable.set(key, []);
635
+ columnsByTable.get(key).push(row);
636
+ }
637
+ const enumsResult = await pool.query(`SELECT t.typname as enum_name, e.enumlabel as enum_value
638
+ FROM pg_type t
639
+ JOIN pg_enum e ON t.oid = e.enumtypid
640
+ ORDER BY t.typname, e.enumsortorder`);
641
+ const enumsByName = /* @__PURE__ */ new Map();
642
+ for (const row of enumsResult.rows) {
643
+ if (!enumsByName.has(row.enum_name)) enumsByName.set(row.enum_name, []);
644
+ enumsByName.get(row.enum_name).push(row.enum_value);
645
+ }
646
+ const fksResult = await pool.query(`SELECT
647
+ tc.constraint_name,
648
+ tc.table_schema,
649
+ tc.table_name,
650
+ kcu.column_name,
651
+ ccu.table_schema AS foreign_table_schema,
652
+ ccu.table_name AS foreign_table_name,
653
+ ccu.column_name AS foreign_column_name
654
+ FROM information_schema.table_constraints AS tc
655
+ JOIN information_schema.key_column_usage AS kcu
656
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
657
+ JOIN information_schema.constraint_column_usage AS ccu
658
+ ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema
659
+ WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = ANY($1)`, [schemas]);
660
+ const fksByTable = /* @__PURE__ */ new Map();
661
+ for (const row of fksResult.rows) {
662
+ const tableSchema = row.table_schema;
663
+ const tableName = row.table_name;
664
+ const key = tableSchema === "public" ? tableName : `${tableSchema}.${tableName}`;
665
+ if (!fksByTable.has(key)) fksByTable.set(key, []);
666
+ fksByTable.get(key).push(row);
667
+ }
668
+ const postgresCollections = relationalCollections(collections);
669
+ for (const collection of postgresCollections) {
670
+ const tableName = getTableName(collection);
671
+ const schemaName = collection.schema || "public";
672
+ const fullTableName = schemaName === "public" ? tableName : `${schemaName}.${tableName}`;
673
+ if (!existingTables.has(fullTableName)) {
674
+ issues.push({
675
+ severity: "error",
676
+ category: "missing_table",
677
+ table: fullTableName,
678
+ message: `Table "${fullTableName}" does not exist in the database.`,
679
+ fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
680
+ });
681
+ continue;
682
+ }
683
+ const dbColumns = columnsByTable.get(fullTableName) ?? [];
684
+ const dbColumnMap = new Map(dbColumns.map((c) => [c.column_name, c]));
685
+ const systemColumns = /* @__PURE__ */ new Set([
686
+ "id",
687
+ "created_on",
688
+ "updated_on"
689
+ ]);
690
+ for (const [propName, prop] of Object.entries(collection.properties ?? {})) {
691
+ if (prop.type === "relation") {
692
+ const relation = findRelation(resolveCollectionRelations(collection), prop.relation?.relationName ?? propName);
693
+ if (relation?.kind === "belongsTo") {
694
+ const fkColName = relation.localKey;
695
+ if (!dbColumnMap.has(fkColName)) issues.push({
696
+ severity: "error",
697
+ category: "missing_column",
698
+ table: fullTableName,
699
+ column: fkColName,
700
+ message: `Foreign key column "${fkColName}" for relation "${propName}" is missing from table "${fullTableName}".`,
701
+ fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
702
+ });
703
+ const tableFks = fksByTable.get(fullTableName) ?? [];
704
+ let targetTableName = "unknown";
705
+ let targetSchemaName = "public";
706
+ try {
707
+ const targetColl = relation.target();
708
+ targetTableName = getTableName(targetColl);
709
+ targetSchemaName = targetColl.schema || "public";
710
+ } catch {}
711
+ const hasFk = tableFks.some((fk) => fk.column_name === fkColName && fk.foreign_table_name === targetTableName && fk.foreign_table_schema === targetSchemaName);
712
+ if (dbColumnMap.has(fkColName) && !hasFk) issues.push({
713
+ severity: "warning",
714
+ category: "missing_foreign_key",
715
+ table: fullTableName,
716
+ column: fkColName,
717
+ message: `Column "${fkColName}" exists but has no FOREIGN KEY constraint referencing "${targetSchemaName === "public" ? targetTableName : `${targetSchemaName}.${targetTableName}`}".`,
718
+ fix: "Run `rebase db push` or add the constraint manually"
719
+ });
720
+ }
721
+ continue;
722
+ }
723
+ const colName = resolveColumnName(propName, prop);
724
+ if (systemColumns.has(colName)) continue;
725
+ const dbCol = dbColumnMap.get(colName);
726
+ if (!dbCol) {
727
+ issues.push({
728
+ severity: "error",
729
+ category: "missing_column",
730
+ table: fullTableName,
731
+ column: colName,
732
+ message: `Column "${colName}" is defined in collection "${collection.slug}" but missing from table "${fullTableName}".`,
733
+ fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
734
+ });
735
+ continue;
736
+ }
737
+ const expectedType = getExpectedColumnType(prop);
738
+ if (expectedType) {
739
+ const actualType = dbCol.data_type;
740
+ let isMismatch = actualType !== expectedType;
741
+ if (prop.type === "vector" && dbCol.udt_name !== "vector") isMismatch = true;
742
+ if (prop.type === "array") {
743
+ const ap = prop;
744
+ let expectedColType = ap.columnType;
745
+ if (!expectedColType && ap.of && !Array.isArray(ap.of)) {
746
+ const ofProp = ap.of;
747
+ if (ofProp.type === "string") expectedColType = "text[]";
748
+ else if (ofProp.type === "number") expectedColType = ofProp.validation?.integer ? "integer[]" : "numeric[]";
749
+ else if (ofProp.type === "boolean") expectedColType = "boolean[]";
750
+ }
751
+ if (expectedColType && expectedColType.endsWith("[]")) if (actualType !== "ARRAY") isMismatch = true;
752
+ else {
753
+ const expectedUdt = expectedColType === "text[]" ? "_text" : expectedColType === "integer[]" ? "_int4" : expectedColType === "boolean[]" ? "_bool" : expectedColType === "numeric[]" ? "_numeric" : "";
754
+ if (expectedUdt && dbCol.udt_name !== expectedUdt) isMismatch = true;
755
+ }
756
+ }
757
+ if (isMismatch) issues.push({
758
+ severity: "warning",
759
+ category: "type_mismatch",
760
+ table: fullTableName,
761
+ column: colName,
762
+ expected: prop.type === "vector" ? "vector" : expectedType,
763
+ actual: dbCol.udt_name === "vector" ? "vector" : actualType,
764
+ message: `Column "${colName}" in table "${fullTableName}": expected type "${prop.type === "vector" ? "vector" : expectedType}" but found "${dbCol.udt_name === "vector" ? "vector" : actualType}".`,
765
+ fix: "Review collection property type or run a migration"
766
+ });
767
+ }
768
+ if (prop.type === "string" && prop.enum) {
769
+ const enumValues = prop.enum;
770
+ if (enumValues) {
771
+ const enumName = `${tableName}_${colName}`;
772
+ const dbEnumValues = enumsByName.get(enumName);
773
+ if (!dbEnumValues) issues.push({
774
+ severity: "warning",
775
+ category: "missing_enum",
776
+ table: fullTableName,
777
+ column: colName,
778
+ expected: enumName,
779
+ message: `Enum type "${enumName}" is defined in collection but not found in the database.`,
780
+ fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
781
+ });
782
+ else {
783
+ const expectedValues = Array.isArray(enumValues) ? enumValues.map((v) => typeof v === "string" ? v : String(v.id)) : Object.keys(enumValues);
784
+ const missing = expectedValues.filter((v) => !dbEnumValues.includes(v));
785
+ const extra = dbEnumValues.filter((v) => !expectedValues.includes(v));
786
+ if (missing.length > 0 || extra.length > 0) {
787
+ const parts = [];
788
+ if (missing.length > 0) parts.push(`missing: ${missing.join(", ")}`);
789
+ if (extra.length > 0) parts.push(`extra in DB: ${extra.join(", ")}`);
790
+ issues.push({
791
+ severity: "warning",
792
+ category: "enum_value_mismatch",
793
+ table: fullTableName,
794
+ column: colName,
795
+ expected: expectedValues.join(", "),
796
+ actual: dbEnumValues.join(", "),
797
+ message: `Enum values for "${colName}" in table "${fullTableName}" are out of sync (${parts.join("; ")}).`,
798
+ fix: "Run `rebase db push` to update the enum"
799
+ });
800
+ }
801
+ }
802
+ }
803
+ }
804
+ }
805
+ const resolvedRelations = resolveCollectionRelations(collection);
806
+ for (const relation of Object.values(resolvedRelations)) if (isManyToMany(relation)) {
807
+ const junctionTable = relation.through.table;
808
+ const junctionSchema = collection.schema || "public";
809
+ const fullJunctionTable = junctionSchema === "public" ? junctionTable : `${junctionSchema}.${junctionTable}`;
810
+ if (!existingTables.has(fullJunctionTable)) issues.push({
811
+ severity: "error",
812
+ category: "missing_table",
813
+ table: fullJunctionTable,
814
+ message: `Junction table "${fullJunctionTable}" for many-to-many relation "${relation.relationName}" is missing.`,
815
+ fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
816
+ });
817
+ }
818
+ }
819
+ } finally {
820
+ await pool.end();
821
+ }
822
+ return {
823
+ passed: issues.length === 0,
824
+ issues
825
+ };
826
+ }
827
+ function renderReport(report) {
828
+ out();
829
+ out(chalk.bold(" 🩺 Rebase Schema Doctor"));
830
+ out(chalk.gray(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
831
+ out();
832
+ renderPhase("Collections → Generated Schema", report.collectionsToSchema);
833
+ renderPhase("Collections → Database", report.schemaToDatabase);
834
+ renderPhase("Collections → SDK Types", report.collectionsToSdk);
835
+ out(chalk.gray(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"));
836
+ const { passed, skipped, notApplicable, warnings, errors } = report.summary;
837
+ const parts = [];
838
+ parts.push(chalk.green(`${passed} passed`));
839
+ if (skipped > 0) parts.push(chalk.yellow(`${skipped} skipped`));
840
+ if (notApplicable > 0) parts.push(chalk.gray(`${notApplicable} not applicable`));
841
+ if (warnings > 0) parts.push(chalk.yellow(`${warnings} warnings`));
842
+ if (errors > 0) parts.push(chalk.red(`${errors} errors`));
843
+ out(` Summary: ${parts.join(", ")}`);
844
+ out();
845
+ if (errors > 0) out(chalk.red.bold(" ✗ Schema drift detected. Run the suggested fixes above."));
846
+ else if (warnings > 0) out(chalk.yellow.bold(" ⚠ Minor issues detected. Consider running the suggested fixes."));
847
+ else if (skipped > 0) out(chalk.yellow.bold(` ⚠ Nothing wrong in the checks that ran, but ${skipped} did not run — this is not a clean bill of health.`));
848
+ else out(chalk.green.bold(" ✓ All schemas are in sync!"));
849
+ out();
850
+ }
851
+ function renderPhase(label, phase) {
852
+ if (phase.skipped) {
853
+ out(` ${chalk.yellow("⏭")} ${label}: ${chalk.yellow(`skipped (${phase.skipped})`)}`);
854
+ out();
855
+ return;
856
+ }
857
+ if (phase.notApplicable) {
858
+ out(` ${chalk.gray("➖")} ${label}: ${chalk.gray(phase.notApplicable)}`);
859
+ for (const issue of phase.issues.filter((i) => i.severity === "info")) out(` ${chalk.gray(issue.fix)}`);
860
+ out();
861
+ return;
862
+ }
863
+ const issues = phase.issues;
864
+ const errorCount = issues.filter((i) => i.severity === "error").length;
865
+ const warnCount = issues.filter((i) => i.severity === "warning").length;
866
+ const infoIssues = issues.filter((i) => i.severity === "info");
867
+ if (errorCount === 0 && warnCount === 0) out(` ${chalk.green("✅")} ${label}: ${chalk.green("In sync")}`);
868
+ else {
869
+ const parts = [];
870
+ if (errorCount > 0) parts.push(`${errorCount} error${errorCount > 1 ? "s" : ""}`);
871
+ if (warnCount > 0) parts.push(`${warnCount} warning${warnCount > 1 ? "s" : ""}`);
872
+ out(` ${chalk.yellow("⚠️")} ${label}: ${chalk.yellow(parts.join(", "))}`);
873
+ }
874
+ out();
875
+ for (const issue of infoIssues) {
876
+ const fixPart = issue.fix ? chalk.gray(` — ${issue.fix}`) : "";
877
+ out(` ${chalk.gray("ℹ")} ${chalk.gray(issue.message)}${fixPart}`);
878
+ out();
879
+ }
880
+ for (const issue of issues.filter((i) => i.severity !== "info")) {
881
+ const severityIcon = issue.severity === "error" ? chalk.red("✗") : chalk.yellow("⚠");
882
+ const categoryLabel = formatCategory(issue.category);
883
+ out(` ${chalk.gray("┌─")} ${severityIcon} ${chalk.bold(categoryLabel)} ${chalk.gray("─".repeat(Math.max(0, 42 - categoryLabel.length)))}`);
884
+ if (issue.table) {
885
+ const colPart = issue.column ? ` │ Column: ${chalk.cyan(issue.column)}` : "";
886
+ out(` ${chalk.gray("│")} Table: ${chalk.cyan(issue.table)}${colPart}`);
887
+ }
888
+ if (issue.expected && issue.actual) out(` ${chalk.gray("│")} Expected: ${chalk.green(issue.expected)} │ Actual: ${chalk.red(issue.actual)}`);
889
+ out(` ${chalk.gray("│")} ${issue.message}`);
890
+ out(` ${chalk.gray("│")} Fix: ${chalk.blue(issue.fix)}`);
891
+ out(` ${chalk.gray("└" + "─".repeat(48))}`);
892
+ out();
893
+ }
894
+ }
895
+ function formatCategory(cat) {
896
+ return {
897
+ missing_table: "Missing Table",
898
+ missing_column: "Missing Column",
899
+ type_mismatch: "Type Mismatch",
900
+ missing_constraint: "Missing Constraint",
901
+ schema_stale: "Stale Schema",
902
+ missing_enum: "Missing Enum",
903
+ enum_value_mismatch: "Enum Value Mismatch",
904
+ missing_foreign_key: "Missing Foreign Key",
905
+ sdk_stale: "Stale SDK Types",
906
+ sdk_not_generated: "SDK Types Not Generated",
907
+ sdk_ungeneratable: "SDK Cannot Be Generated"
908
+ }[cat];
909
+ }
910
+ async function runDoctor(options) {
911
+ out();
912
+ out(chalk.bold(" 🩺 Loading collections..."));
913
+ const collections = await loadCollections(options.collectionsPath);
914
+ if (collections.length === 0) {
915
+ outError(chalk.red(" ✗ No collections found."));
916
+ process.exit(1);
917
+ }
918
+ out(chalk.gray(` Found ${collections.length} collection(s)`));
919
+ out();
920
+ out(chalk.gray(" Checking Collections → Generated Schema..."));
921
+ const collectionsToSchema = await checkCollectionsVsSchema(collections, options.schemaPath);
922
+ let schemaToDatabase;
923
+ if (options.databaseUrl) {
924
+ out(chalk.gray(" Checking Collections → Database..."));
925
+ try {
926
+ schemaToDatabase = await checkCollectionsVsDatabase(collections, options.databaseUrl);
927
+ } catch (err) {
928
+ const banner = diagnoseDbError(err, options.databaseUrl);
929
+ if (banner) outError(banner);
930
+ const reason = deepestErrorMessage(err) ?? (err instanceof Error ? err.message : String(err));
931
+ schemaToDatabase = {
932
+ passed: false,
933
+ issues: [],
934
+ skipped: `could not reach ${parseHostInfo(options.databaseUrl)} — ${reason}`,
935
+ blocked: true
936
+ };
937
+ }
938
+ } else {
939
+ schemaToDatabase = {
940
+ passed: false,
941
+ issues: [],
942
+ skipped: "DATABASE_URL not set"
943
+ };
944
+ out(chalk.yellow(" ⚠ DATABASE_URL not set — skipping database comparison."));
945
+ out(chalk.gray(" Set DATABASE_URL in your .env to enable full drift detection."));
946
+ }
947
+ out(chalk.gray(" Checking Collections → SDK Types..."));
948
+ const collectionsToSdk = await checkCollectionsVsSdk(collections, options.sdkPath);
949
+ const phases = [
950
+ collectionsToSchema,
951
+ schemaToDatabase,
952
+ collectionsToSdk
953
+ ];
954
+ const allIssues = phases.flatMap((p) => p.issues);
955
+ const summary = {
956
+ passed: phases.filter((p) => p.passed && !p.skipped && !p.notApplicable).length,
957
+ skipped: phases.filter((p) => p.skipped).length,
958
+ notApplicable: phases.filter((p) => p.notApplicable && !p.skipped).length,
959
+ warnings: allIssues.filter((i) => i.severity === "warning").length,
960
+ errors: allIssues.filter((i) => i.severity === "error").length
961
+ };
962
+ const report = {
963
+ collectionsToSchema,
964
+ collectionsToSdk,
965
+ schemaToDatabase,
966
+ summary
967
+ };
968
+ renderReport(report);
969
+ return report;
970
+ }
971
+ //#endregion
972
+ export { loadCollections as n, runDoctor as r, doctor_exports as t };
973
+
974
+ //# sourceMappingURL=doctor-D7HWmGzs.js.map