@rebasepro/server-postgres 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BranchService-DGPL6G_C.js → BranchService-BRt78gfa.js} +35 -18
- package/dist/BranchService-BRt78gfa.js.map +1 -0
- package/dist/PostgresBackendDriver.d.ts +84 -12
- package/dist/auth/services.d.ts +72 -14
- package/dist/{auth-users-columns-D2LBFrMH.js → auth-users-columns-C72EMoDJ.js} +17 -1
- package/dist/{auth-users-columns-D2LBFrMH.js.map → auth-users-columns-C72EMoDJ.js.map} +1 -1
- package/dist/{backup-cli-24v4OlSp.js → backup-cli-DW5p9_zv.js} +2 -2
- package/dist/{backup-cli-24v4OlSp.js.map → backup-cli-DW5p9_zv.js.map} +1 -1
- package/dist/{backup-service-HQ9GC4tN.js → backup-service-EwcDVG-8.js} +7 -9
- package/dist/{backup-service-HQ9GC4tN.js.map → backup-service-EwcDVG-8.js.map} +1 -1
- package/dist/{cli-errors-B8qHg02P.js → cli-errors-DsA-K9uP.js} +96 -1
- package/dist/{cli-errors-B8qHg02P.js.map → cli-errors-DsA-K9uP.js.map} +1 -1
- package/dist/cli-errors.d.ts +42 -0
- package/dist/cli-helpers.d.ts +12 -12
- package/dist/cli.js +91 -37
- package/dist/cli.js.map +1 -1
- package/dist/{column-plan-helpers-DF-8dTVa.js → column-plan-helpers-1-LQD0yI.js} +34 -24
- package/dist/column-plan-helpers-1-LQD0yI.js.map +1 -0
- package/dist/data-transformer.d.ts +0 -8
- package/dist/{doctor-Cb2thZ8s.js → doctor-C-sYWbmt.js} +224 -43
- package/dist/doctor-C-sYWbmt.js.map +1 -0
- package/dist/{ensure-collection-policies-RHUcEp4v.js → ensure-collection-policies-B1ureSIV.js} +73 -18
- package/dist/ensure-collection-policies-B1ureSIV.js.map +1 -0
- package/dist/{ensure-collection-tables-BEEjn5cn.js → ensure-collection-tables-kkkHk8oo.js} +76 -20
- package/dist/ensure-collection-tables-kkkHk8oo.js.map +1 -0
- package/dist/{ensure-tables-Dhn9KM3B.js → ensure-tables-BmI_tRxc.js} +10 -3
- package/dist/ensure-tables-BmI_tRxc.js.map +1 -0
- package/dist/{generate-drizzle-schema-yzY_BLhr.js → generate-drizzle-schema-93M0lUxK.js} +2 -2
- package/dist/{generate-drizzle-schema-yzY_BLhr.js.map → generate-drizzle-schema-93M0lUxK.js.map} +1 -1
- package/dist/{generate-drizzle-schema-logic-BfzK7UQd.js → generate-drizzle-schema-logic-sSFDp6LR.js} +26 -8
- package/dist/generate-drizzle-schema-logic-sSFDp6LR.js.map +1 -0
- package/dist/generate-postgres-ddl-logic-BJsLaVNX.js +152 -0
- package/dist/generate-postgres-ddl-logic-BJsLaVNX.js.map +1 -0
- package/dist/generated-sql.d.ts +28 -0
- package/dist/index.es.js +3415 -917
- package/dist/index.es.js.map +1 -1
- package/dist/{introspect-db-logic-WMuAfxvw.js → introspect-db-logic-kCETE8TY.js} +523 -349
- package/dist/introspect-db-logic-kCETE8TY.js.map +1 -0
- package/dist/introspect-db-queries-C_Q5VgQw.js +317 -0
- package/dist/introspect-db-queries-C_Q5VgQw.js.map +1 -0
- package/dist/{plan-schema-C0fxM8dY.js → plan-schema-DU9exq6C.js} +139 -504
- package/dist/plan-schema-DU9exq6C.js.map +1 -0
- package/dist/{policy-drift-DljYdrpW.js → policy-drift-B-J2hhm0.js} +3 -3
- package/dist/policy-drift-B-J2hhm0.js.map +1 -0
- package/dist/{generate-postgres-ddl-logic-Bt2d2mRH.js → render-ddl-Ds2t_d9V.js} +11 -149
- package/dist/render-ddl-Ds2t_d9V.js.map +1 -0
- package/dist/{rls-bootstrap-sql-B8EclDyM.js → rls-bootstrap-sql-_KNnjanK.js} +263 -39
- package/dist/rls-bootstrap-sql-_KNnjanK.js.map +1 -0
- package/dist/{rls-enforcement-C6Xk0lA6.js → rls-enforcement-CfXOJJaW.js} +10 -2
- package/dist/rls-enforcement-CfXOJJaW.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +170 -0
- package/dist/schema/classify-change.d.ts +29 -1
- package/dist/schema/column-plan-helpers.d.ts +33 -21
- package/dist/schema/destructive-sql.d.ts +20 -1
- package/dist/schema/doctor-cli.js +4 -4
- package/dist/schema/doctor.d.ts +34 -1
- package/dist/schema/ensure-collection-policies.d.ts +22 -0
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -5
- package/dist/schema/generate-postgres-ddl.js +1 -1
- package/dist/schema/generate-schema-commit.d.ts +12 -0
- package/dist/schema/introspect-db-logic.d.ts +31 -0
- package/dist/schema/introspect-db-queries.d.ts +1 -1
- package/dist/schema/introspect-db-search.d.ts +22 -0
- package/dist/schema/introspect-db-storage.d.ts +83 -0
- package/dist/schema/introspect-db.js +15 -3
- package/dist/schema/introspect-db.js.map +1 -1
- package/dist/schema/plan/diff-plan.d.ts +1 -1
- package/dist/schema/plan/plan-schema.d.ts +16 -8
- package/dist/schema/plan/render-ddl.d.ts +6 -0
- package/dist/schema/plan/types.d.ts +37 -11
- package/dist/search-column-BM-GV6vH.js +442 -0
- package/dist/search-column-BM-GV6vH.js.map +1 -0
- package/dist/security/policy-drift.d.ts +1 -1
- package/dist/security/rls-enforcement.d.ts +7 -0
- package/dist/services/BranchService.d.ts +22 -1
- package/dist/services/FetchService.d.ts +5 -4
- package/dist/services/RelationService.d.ts +18 -0
- package/dist/services/cdc/CdcListener.d.ts +9 -4
- package/dist/services/cdc/identity-columns.d.ts +19 -0
- package/dist/services/cdc/trigger-cdc.d.ts +45 -7
- package/dist/services/channel-bus/PostgresChannelBus.d.ts +12 -7
- package/dist/services/channel-history.d.ts +17 -1
- package/dist/services/collection-helpers.d.ts +11 -1
- package/dist/services/pg-notify-listener.d.ts +88 -6
- package/dist/services/realtimeService.d.ts +159 -45
- package/dist/services/socket-liveness.d.ts +45 -0
- package/dist/services/soft-delete.d.ts +12 -0
- package/dist/services/sql-script.d.ts +71 -0
- package/dist/services/write-depth.d.ts +16 -0
- package/dist/services/write-transaction-scope.d.ts +6 -3
- package/dist/utils/drizzle-conditions.d.ts +42 -2
- package/dist/utils/sql-redaction.d.ts +21 -0
- package/dist/websocket.d.ts +50 -18
- package/package.json +7 -7
- package/dist/BranchService-DGPL6G_C.js.map +0 -1
- package/dist/column-plan-helpers-DF-8dTVa.js.map +0 -1
- package/dist/doctor-Cb2thZ8s.js.map +0 -1
- package/dist/ensure-collection-policies-RHUcEp4v.js.map +0 -1
- package/dist/ensure-collection-tables-BEEjn5cn.js.map +0 -1
- package/dist/ensure-tables-Dhn9KM3B.js.map +0 -1
- package/dist/generate-drizzle-schema-logic-BfzK7UQd.js.map +0 -1
- package/dist/generate-postgres-ddl-logic-Bt2d2mRH.js.map +0 -1
- package/dist/introspect-db-logic-WMuAfxvw.js.map +0 -1
- package/dist/plan-schema-C0fxM8dY.js.map +0 -1
- package/dist/policy-drift-DljYdrpW.js.map +0 -1
- package/dist/rls-bootstrap-sql-B8EclDyM.js.map +0 -1
- package/dist/rls-enforcement-C6Xk0lA6.js.map +0 -1
|
@@ -4,7 +4,6 @@ import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
|
|
|
4
4
|
import { toSnakeCase } from "@rebasepro/utils";
|
|
5
5
|
//#region src/schema/column-plan-helpers.ts
|
|
6
6
|
var column_plan_helpers_exports = /* @__PURE__ */ __exportAll({
|
|
7
|
-
assertSinglePrimaryKey: () => assertSinglePrimaryKey,
|
|
8
7
|
declaresEnumType: () => declaresEnumType,
|
|
9
8
|
enumLabelsOf: () => enumLabelsOf,
|
|
10
9
|
getPrimaryKeyColumn: () => getPrimaryKeyColumn,
|
|
@@ -12,6 +11,7 @@ var column_plan_helpers_exports = /* @__PURE__ */ __exportAll({
|
|
|
12
11
|
getPrimaryKeyProp: () => getPrimaryKeyProp,
|
|
13
12
|
idColumnDefault: () => idColumnDefault,
|
|
14
13
|
isIdProperty: () => isIdProperty,
|
|
14
|
+
primaryKeyPropertyNames: () => primaryKeyPropertyNames,
|
|
15
15
|
resolveColumnName: () => resolveColumnName
|
|
16
16
|
});
|
|
17
17
|
/**
|
|
@@ -27,27 +27,31 @@ var resolveColumnName = (propName, prop) => {
|
|
|
27
27
|
/** Every property that carries a truthy `isId`, in declaration order. */
|
|
28
28
|
var idPropertyEntries = (collection) => Object.entries(collection.properties ?? {}).filter(([, prop]) => prop && typeof prop === "object" && "isId" in prop && Boolean(prop.isId));
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
30
|
+
* The properties marked `isId`, in declaration order.
|
|
31
31
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
*
|
|
32
|
+
* Several of them are one composite primary key: the table carries a single
|
|
33
|
+
* `PRIMARY KEY (a, b)` constraint over their columns, and a row's address lists
|
|
34
|
+
* the parts in this same order (`buildCompositeId`), which is the order the
|
|
35
|
+
* admin reads them in too.
|
|
36
|
+
*/
|
|
37
|
+
var primaryKeyPropertyNames = (collection) => idPropertyEntries(collection).map(([name]) => name);
|
|
38
|
+
/**
|
|
39
|
+
* The one column a link into this collection points at.
|
|
40
40
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
41
|
+
* A foreign key column references a single column, and Postgres only accepts
|
|
42
|
+
* that column when it is unique on its own — `REFERENCES t (a)` against a
|
|
43
|
+
* composite key `(a, b)` fails with "there is no unique constraint matching
|
|
44
|
+
* given keys for referenced table". So a link into a collection whose key
|
|
45
|
+
* spans several columns has no column to point at, and it is refused here,
|
|
46
|
+
* naming the link, rather than emitted as a constraint `db push` or boot would
|
|
47
|
+
* then fail to create.
|
|
48
|
+
*
|
|
49
|
+
* @param via the link that needs the column, for the refusal: `relation "posts.author"`.
|
|
43
50
|
*/
|
|
44
|
-
var
|
|
51
|
+
var getPrimaryKeyProp = (collection, via) => {
|
|
45
52
|
const ids = idPropertyEntries(collection);
|
|
46
|
-
if (ids.length
|
|
47
|
-
|
|
48
|
-
};
|
|
49
|
-
var getPrimaryKeyProp = (collection) => {
|
|
50
|
-
const idPropEntry = idPropertyEntries(collection)[0];
|
|
53
|
+
if (ids.length > 1) throw new Error(`${via} points at collection "${collection.slug ?? collection.name ?? "(unnamed)"}", whose primary key spans ${ids.length} columns (${ids.map(([name]) => `"${name}"`).join(", ")}). A foreign key is one column and cannot reference a composite key. Point the link at a collection with a single-column key, or reach these rows with a \`via\` relation, whose \`joinPath\` compares every key column.`);
|
|
54
|
+
const idPropEntry = ids[0];
|
|
51
55
|
if (idPropEntry) {
|
|
52
56
|
const [name, prop] = idPropEntry;
|
|
53
57
|
return {
|
|
@@ -63,10 +67,16 @@ var getPrimaryKeyProp = (collection) => {
|
|
|
63
67
|
prop: idProp
|
|
64
68
|
};
|
|
65
69
|
};
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
/**
|
|
71
|
+
* The primary key's property key. What a Drizzle relation's `references`
|
|
72
|
+
* names. Refused on the same terms as {@link getPrimaryKeyProp}.
|
|
73
|
+
*/
|
|
74
|
+
var getPrimaryKeyName = (collection, via) => getPrimaryKeyProp(collection, via).name;
|
|
75
|
+
/**
|
|
76
|
+
* The primary key's column. What a foreign key's `REFERENCES` names. Refused
|
|
77
|
+
* on the same terms as {@link getPrimaryKeyProp}.
|
|
78
|
+
*/
|
|
79
|
+
var getPrimaryKeyColumn = (collection, via) => getPrimaryKeyProp(collection, via).column;
|
|
70
80
|
var isIdProperty = (propName, prop, collection) => {
|
|
71
81
|
if ("isId" in prop && Boolean(prop.isId)) return true;
|
|
72
82
|
return idPropertyEntries(collection).length === 0 && propName === "id";
|
|
@@ -136,6 +146,6 @@ var idColumnDefault = (propName, prop, collection) => {
|
|
|
136
146
|
};
|
|
137
147
|
};
|
|
138
148
|
//#endregion
|
|
139
|
-
export {
|
|
149
|
+
export { getPrimaryKeyName as a, isIdProperty as c, getPrimaryKeyColumn as i, primaryKeyPropertyNames as l, declaresEnumType as n, getPrimaryKeyProp as o, enumLabelsOf as r, idColumnDefault as s, column_plan_helpers_exports as t, resolveColumnName as u };
|
|
140
150
|
|
|
141
|
-
//# sourceMappingURL=column-plan-helpers-
|
|
151
|
+
//# sourceMappingURL=column-plan-helpers-1-LQD0yI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"column-plan-helpers-1-LQD0yI.js","names":[],"sources":["../src/schema/column-plan-helpers.ts"],"sourcesContent":["/**\n * The questions about a property that the schema planner asks first.\n *\n * What is the primary key, what is this column called, what does an id default\n * to, what labels does this enum have. `schema/plan/plan-schema.ts` is the only\n * caller — it is the only reader of a `Property` — and these live beside it\n * rather than inside it because they are also where the *refusals* are, and a\n * refusal wants to be reachable and testable on its own.\n *\n * They were extracted while there were still three emitters, each carrying its\n * own verbatim copy of all four rules until one of them was edited: the audit\n * that produced this module counted twelve open disagreements and found that a\n * third of the commits touching those files were re-synchronisations. The\n * emitters are renderers now and there is nothing left to keep in step, but the\n * questions are still questions about a property rather than about a plan, so\n * this is where they belong.\n *\n * Everything here is pure and synchronous, and it throws rather than guesses:\n * an enum with no values, a `cuid` id on Postgres and a foreign key into a\n * composite key are all configurations that produce a broken database in\n * silence, so they are refused where every path passes.\n */\nimport type {\n CollectionConfig,\n NumberProperty,\n Property,\n StringProperty\n} from \"@rebasepro/types\";\nimport { toSnakeCase } from \"@rebasepro/utils\";\n\n/**\n * Resolve the SQL column name for a property.\n *\n * Uses the explicit `columnName` when set (e.g. from introspection), falling\n * back to `toSnakeCase(propName)` for manually-authored collections.\n */\nexport const resolveColumnName = (propName: string, prop?: Property | null): string => {\n if (prop && \"columnName\" in prop && typeof prop.columnName === \"string\") {\n return prop.columnName;\n }\n return toSnakeCase(propName);\n};\n\n/** Every property that carries a truthy `isId`, in declaration order. */\nconst idPropertyEntries = (collection: CollectionConfig): [string, Property][] =>\n Object.entries(collection.properties ?? {}).filter(\n ([, prop]) => prop && typeof prop === \"object\" && \"isId\" in prop && Boolean((prop as { isId?: unknown }).isId)\n ) as [string, Property][];\n\n/**\n * The properties marked `isId`, in declaration order.\n *\n * Several of them are one composite primary key: the table carries a single\n * `PRIMARY KEY (a, b)` constraint over their columns, and a row's address lists\n * the parts in this same order (`buildCompositeId`), which is the order the\n * admin reads them in too.\n */\nexport const primaryKeyPropertyNames = (collection: CollectionConfig): string[] =>\n idPropertyEntries(collection).map(([name]) => name);\n\n/**\n * A collection's primary key, by both of its names.\n *\n * `name` is the property key — what the generated Drizzle table is keyed by,\n * so it is what a relation's `references` names. `column` is the SQL\n * identifier, so it is what a foreign key's `REFERENCES` names. They differ\n * whenever the key is camelCase or carries a `columnName` (`authorId` is\n * `author_id`, and every table `rebase schema introspect` adopts is spelled\n * that way), and a constraint written with the key named a column that does\n * not exist: `db push` could not build its desired state, and boot failed the\n * constraint on every start.\n */\nexport interface PrimaryKey {\n /** Property key: the Drizzle object key and the JSON field. */\n name: string;\n /** SQL column. */\n column: string;\n /** The declaring property; absent for the implicit `id TEXT` of a collection that declares none. */\n prop?: Property;\n}\n\n/**\n * The one column a link into this collection points at.\n *\n * A foreign key column references a single column, and Postgres only accepts\n * that column when it is unique on its own — `REFERENCES t (a)` against a\n * composite key `(a, b)` fails with \"there is no unique constraint matching\n * given keys for referenced table\". So a link into a collection whose key\n * spans several columns has no column to point at, and it is refused here,\n * naming the link, rather than emitted as a constraint `db push` or boot would\n * then fail to create.\n *\n * @param via the link that needs the column, for the refusal: `relation \"posts.author\"`.\n */\nexport const getPrimaryKeyProp = (collection: CollectionConfig, via: string): PrimaryKey => {\n const ids = idPropertyEntries(collection);\n if (ids.length > 1) {\n throw new Error(\n `${via} points at collection \"${collection.slug ?? collection.name ?? \"(unnamed)\"}\", whose primary ` +\n `key spans ${ids.length} columns (${ids.map(([name]) => `\"${name}\"`).join(\", \")}). A foreign key is ` +\n \"one column and cannot reference a composite key. Point the link at a collection with a \" +\n \"single-column key, or reach these rows with a `via` relation, whose `joinPath` compares every \" +\n \"key column.\"\n );\n }\n const idPropEntry = ids[0];\n if (idPropEntry) {\n const [name, prop] = idPropEntry;\n return { name, column: resolveColumnName(name, prop), prop };\n }\n // Fallback: a collection that declares no `isId` gets an implicit `id`.\n const idProp = collection.properties?.[\"id\"] as Property | undefined;\n return { name: \"id\", column: resolveColumnName(\"id\", idProp), prop: idProp };\n};\n\n/**\n * The primary key's property key. What a Drizzle relation's `references`\n * names. Refused on the same terms as {@link getPrimaryKeyProp}.\n */\nexport const getPrimaryKeyName = (collection: CollectionConfig, via: string): string =>\n getPrimaryKeyProp(collection, via).name;\n\n/**\n * The primary key's column. What a foreign key's `REFERENCES` names. Refused\n * on the same terms as {@link getPrimaryKeyProp}.\n */\nexport const getPrimaryKeyColumn = (collection: CollectionConfig, via: string): string =>\n getPrimaryKeyProp(collection, via).column;\n\nexport const isIdProperty = (propName: string, prop: Property, collection: CollectionConfig): boolean => {\n if (\"isId\" in prop && Boolean(prop.isId)) return true;\n\n // Only fall back to \"id\" when NO property is explicitly marked.\n return idPropertyEntries(collection).length === 0 && propName === \"id\";\n};\n\n/**\n * The labels an `enum` declares, in order, whichever form it was written in.\n *\n * `EnumValues` is `EnumValueConfig[] | Record<id, label>` and the docs recommend\n * the record. Boot-ensure read only the array form — `(p.enum as unknown[]).map`\n * — so a record-form enum threw `p.enum.map is not a function` on a managed\n * tenant, at boot, with no developer in the loop. The other two emitters each\n * carried their own copy of the both-forms walk.\n *\n * An empty list throws rather than returning `[]`. `CREATE TYPE … AS ENUM ()`\n * is not valid SQL, so every emitter \"handled\" it by skipping the type and then\n * typing the column with it anyway: Drizzle referenced an enum variable it never\n * declared (the generated file does not compile), the DDL named a type nothing\n * creates, and boot-ensure's `ADD COLUMN` failed — and add-column is not a\n * survivable action, so the boot died. There is no useful column at the end of\n * any of those paths.\n */\nexport const enumLabelsOf = (propName: string, prop: Property, collection: CollectionConfig): string[] => {\n const values = (prop as { enum?: unknown }).enum;\n if (values === undefined || values === null) return [];\n\n const labels = Array.isArray(values)\n ? values.map((entry: unknown) =>\n String(entry !== null && typeof entry === \"object\" && \"id\" in (entry as Record<string, unknown>)\n ? (entry as Record<string, unknown>).id\n : entry))\n : Object.keys(values as Record<string, unknown>).map(String);\n\n const usable = labels.filter(label => label.length > 0);\n if (usable.length === 0) {\n throw new Error(\n `Property \"${propName}\" of collection \"${collection.slug ?? collection.name ?? \"(unnamed)\"}\" ` +\n \"declares an empty `enum`. A Postgres enum type needs at least one label — \" +\n \"`CREATE TYPE … AS ENUM ()` is not valid SQL — so the column would reference a type \" +\n \"nothing creates. List the values, or drop the `enum` for a plain column.\"\n );\n }\n return usable;\n};\n\n/**\n * Does this property compile to a Postgres enum *type*?\n *\n * Strings only. A `number` property with an `enum` used to create a type on all\n * three paths and then declare the column `NUMERIC`/`INTEGER`, so the type was\n * orphaned the moment it was created and the values it listed were enforced by\n * nothing. The dropdown in the panel is driven by the config, not the column, so\n * nothing is lost by not creating it — and a type no column uses is one more\n * object `db push` has to plan a DROP for.\n */\nexport const declaresEnumType = (prop: Property): boolean =>\n prop.type === \"string\" && Boolean((prop as StringProperty).enum);\n\n/**\n * The `DEFAULT` an id column carries, as the thing itself rather than a\n * rendered string — each emitter spells it differently.\n *\n * The three of them disagreed on all four strategies. The DDL generator wrote\n * `DEFAULT ${prop.isId}` verbatim, so the documented `` isId: \"sql`gen_id()`\" ``\n * reached Postgres with the template wrapper still on it — a syntax error in\n * `schema.sql`. Boot-ensure gave a default to `uuid` and to nothing else, so on\n * a managed tenant every other strategy produced a column with no default at\n * all, and the first insert that trusted the config failed on a NULL primary\n * key. Only the Drizzle generator stripped the wrapper.\n */\nexport type IdColumnDefault =\n /** `gen_random_uuid()` / `.defaultRandom()`. */\n | { kind: \"uuid\" }\n /** `GENERATED BY DEFAULT AS IDENTITY` / `.generatedByDefaultAsIdentity()` — part of the type, not a DEFAULT. */\n | { kind: \"identity\" }\n /** A raw SQL expression, wrapper already stripped. */\n | { kind: \"sql\"; expression: string };\n\n/**\n * Refuses `isId: \"cuid\"`.\n *\n * `DEFAULT cuid()` has been emitted since the option existed and no `cuid()`\n * function has ever been created — not by the generators, not by boot, not by\n * any migration. So the column has never had a working default on Postgres: the\n * first insert that relied on it failed with `function cuid() does not exist`.\n * There is nothing to keep compatible.\n *\n * It is refused rather than implemented. Shipping a real `rebase.cuid()` means a\n * new SQL function in the same carved-out class as the search helpers — excluded\n * from Atlas by pattern, applied by boot, re-applied by push, checked by the\n * doctor, and versioned across the fleet forever — to reproduce a monotonic id\n * that `uuid` already provides and that a project wanting cuid's exact shape can\n * write itself in one line of SQL.\n */\nexport const idColumnDefault = (propName: string, prop: Property, collection: CollectionConfig): IdColumnDefault | undefined => {\n if (!(\"isId\" in prop)) return undefined;\n const isId = (prop as { isId?: unknown }).isId;\n // `true` and `\"manual\"` both mean \"the application supplies it\".\n if (isId === undefined || isId === null || isId === true || isId === \"manual\" || isId === false) return undefined;\n if (typeof isId !== \"string\") return undefined;\n\n if (prop.type === \"string\" && (isId as StringProperty[\"isId\"]) === \"uuid\") return { kind: \"uuid\" };\n if (prop.type === \"number\" && (isId as NumberProperty[\"isId\"]) === \"increment\") return { kind: \"identity\" };\n\n if (isId === \"cuid\") {\n throw new Error(\n `Property \"${propName}\" of collection \"${collection.slug ?? collection.name ?? \"(unnamed)\"}\" ` +\n \"uses `isId: \\\"cuid\\\"`, which Postgres cannot honour: the generated default calls a `cuid()` \" +\n \"function that Rebase has never created, so every insert relying on it fails. \" +\n \"Use `isId: \\\"uuid\\\"`, or give the strategy as SQL — ``isId: \\\"sql`my_id()`\\\"`` — and create that \" +\n \"function in a migration.\"\n );\n }\n\n // Any other string is a raw SQL default. Written either bare\n // (`gen_random_uuid()::text`) or in the documented template form\n // (``sql`gen_random_uuid()::text` ``); the wrapper is markup, not SQL.\n const expression = isId.startsWith(\"sql`\") && isId.endsWith(\"`\")\n ? isId.substring(4, isId.length - 1)\n : isId;\n return { kind: \"sql\", expression };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAoCA,IAAa,qBAAqB,UAAkB,SAAmC;CACnF,IAAI,QAAQ,gBAAgB,QAAQ,OAAO,KAAK,eAAe,UAC3D,OAAO,KAAK;CAEhB,OAAO,YAAY,QAAQ;AAC/B;;AAGA,IAAM,qBAAqB,eACvB,OAAO,QAAQ,WAAW,cAAc,CAAC,CAAC,CAAC,CAAC,QACvC,GAAG,UAAU,QAAQ,OAAO,SAAS,YAAY,UAAU,QAAQ,QAAS,KAA4B,IAAI,CACjH;;;;;;;;;AAUJ,IAAa,2BAA2B,eACpC,kBAAkB,UAAU,CAAC,CAAC,KAAK,CAAC,UAAU,IAAI;;;;;;;;;;;;;;AAoCtD,IAAa,qBAAqB,YAA8B,QAA4B;CACxF,MAAM,MAAM,kBAAkB,UAAU;CACxC,IAAI,IAAI,SAAS,GACb,MAAM,IAAI,MACN,GAAG,IAAI,yBAAyB,WAAW,QAAQ,WAAW,QAAQ,YAAY,6BACrE,IAAI,OAAO,YAAY,IAAI,KAAK,CAAC,UAAU,IAAI,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,yNAIpF;CAEJ,MAAM,cAAc,IAAI;CACxB,IAAI,aAAa;EACb,MAAM,CAAC,MAAM,QAAQ;EACrB,OAAO;GAAE;GAAM,QAAQ,kBAAkB,MAAM,IAAI;GAAG;EAAK;CAC/D;CAEA,MAAM,SAAS,WAAW,aAAa;CACvC,OAAO;EAAE,MAAM;EAAM,QAAQ,kBAAkB,MAAM,MAAM;EAAG,MAAM;CAAO;AAC/E;;;;;AAMA,IAAa,qBAAqB,YAA8B,QAC5D,kBAAkB,YAAY,GAAG,CAAC,CAAC;;;;;AAMvC,IAAa,uBAAuB,YAA8B,QAC9D,kBAAkB,YAAY,GAAG,CAAC,CAAC;AAEvC,IAAa,gBAAgB,UAAkB,MAAgB,eAA0C;CACrG,IAAI,UAAU,QAAQ,QAAQ,KAAK,IAAI,GAAG,OAAO;CAGjD,OAAO,kBAAkB,UAAU,CAAC,CAAC,WAAW,KAAK,aAAa;AACtE;;;;;;;;;;;;;;;;;;AAmBA,IAAa,gBAAgB,UAAkB,MAAgB,eAA2C;CACtG,MAAM,SAAU,KAA4B;CAC5C,IAAI,WAAW,KAAA,KAAa,WAAW,MAAM,OAAO,CAAC;CASrD,MAAM,UAPS,MAAM,QAAQ,MAAM,IAC7B,OAAO,KAAK,UACV,OAAO,UAAU,QAAQ,OAAO,UAAU,YAAY,QAAS,QACxD,MAAkC,KACnC,KAAK,CAAC,IACd,OAAO,KAAK,MAAiC,CAAC,CAAC,IAAI,MAAM,EAAA,CAEzC,QAAO,UAAS,MAAM,SAAS,CAAC;CACtD,IAAI,OAAO,WAAW,GAClB,MAAM,IAAI,MACN,aAAa,SAAS,mBAAmB,WAAW,QAAQ,WAAW,QAAQ,YAAY,8OAI/F;CAEJ,OAAO;AACX;;;;;;;;;;;AAYA,IAAa,oBAAoB,SAC7B,KAAK,SAAS,YAAY,QAAS,KAAwB,IAAI;;;;;;;;;;;;;;;;;AAsCnE,IAAa,mBAAmB,UAAkB,MAAgB,eAA8D;CAC5H,IAAI,EAAE,UAAU,OAAO,OAAO,KAAA;CAC9B,MAAM,OAAQ,KAA4B;CAE1C,IAAI,SAAS,KAAA,KAAa,SAAS,QAAQ,SAAS,QAAQ,SAAS,YAAY,SAAS,OAAO,OAAO,KAAA;CACxG,IAAI,OAAO,SAAS,UAAU,OAAO,KAAA;CAErC,IAAI,KAAK,SAAS,YAAa,SAAoC,QAAQ,OAAO,EAAE,MAAM,OAAO;CACjG,IAAI,KAAK,SAAS,YAAa,SAAoC,aAAa,OAAO,EAAE,MAAM,WAAW;CAE1G,IAAI,SAAS,QACT,MAAM,IAAI,MACN,aAAa,SAAS,mBAAmB,WAAW,QAAQ,WAAW,QAAQ,YAAY,2SAK/F;CASJ,OAAO;EAAE,MAAM;EAAO,YAHH,KAAK,WAAW,MAAM,KAAK,KAAK,SAAS,GAAG,IACzD,KAAK,UAAU,GAAG,KAAK,SAAS,CAAC,IACjC;CAC2B;AACrC"}
|
|
@@ -41,14 +41,6 @@ export declare function sanitizeAndConvertDates(obj: unknown): unknown;
|
|
|
41
41
|
* Transform relations for database storage (relation objects to IDs)
|
|
42
42
|
*/
|
|
43
43
|
export declare function serializeDataToServer<M extends Record<string, unknown>>(row: M, properties: Properties, collection?: CollectionConfig, registry?: PostgresCollectionRegistry): SerializedEntityData;
|
|
44
|
-
/**
|
|
45
|
-
* Serialize a single property value for database storage.
|
|
46
|
-
*
|
|
47
|
-
* `propertyKey` is only ever used to phrase errors and warnings. Without it the
|
|
48
|
-
* one trace a bad value left was `Expected array value for array property, got
|
|
49
|
-
* string` — no collection, no property, no value, which in the log of a
|
|
50
|
-
* thousand-row import names nothing at all.
|
|
51
|
-
*/
|
|
52
44
|
export declare function serializePropertyToServer(value: unknown, property: Property, propertyKey?: string): unknown;
|
|
53
45
|
/**
|
|
54
46
|
* Transform IDs back to relation objects for frontend
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
3
|
import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { t as generateSchema } from "./generate-drizzle-schema-logic-
|
|
4
|
+
import { i as diagnoseDbError, n as deepestErrorMessage, s as parseHostInfo } from "./cli-errors-DsA-K9uP.js";
|
|
5
|
+
import { n as planSchema, t as columnPgType } from "./plan-schema-DU9exq6C.js";
|
|
6
|
+
import { t as generateSchema } from "./generate-drizzle-schema-logic-sSFDp6LR.js";
|
|
7
|
+
import { r as renderPgType } from "./render-ddl-Ds2t_d9V.js";
|
|
7
8
|
import { n as outError, t as out } from "./cli-output-CNdMql-L.js";
|
|
8
9
|
import { t as chalk } from "./source-Br7L7GOI.js";
|
|
9
10
|
import { n as describeDeclarationDifferences, t as compareGeneratedDeclarations } from "./generated-schema-staleness-Di9c4ZxN.js";
|
|
10
11
|
import { loadCollectionsFromDirectory } from "@rebasepro/server";
|
|
11
12
|
import { computeSchemaVersion, getDeclaredSubcollections, isManyToMany, isPostgresCollectionConfig } from "@rebasepro/types";
|
|
12
13
|
import { effectiveAccess, fieldKeyForColumn, findRelation, getTableName, isRelationRequired, relationalCollections, resolveCollectionRelations, resolveJunctionSpecs, sortCollectionsBySlug } from "@rebasepro/common";
|
|
13
|
-
import { toSnakeCase } from "@rebasepro/utils";
|
|
14
|
+
import { toPostgresIdentifier, toSnakeCase } from "@rebasepro/utils";
|
|
14
15
|
import * as fs$1 from "fs";
|
|
15
16
|
import { promises } from "fs";
|
|
16
17
|
//#region ../codegen/dist/index.es.js
|
|
@@ -201,7 +202,9 @@ function includedRelationType(relation, accessors) {
|
|
|
201
202
|
const slug = resolveTargetCollection(relation)?.slug ?? relation.targetSlug;
|
|
202
203
|
const accessor = slug ? accessors.get(slug) : void 0;
|
|
203
204
|
const rowType = accessor ? `Database[${emitString(accessor)}]["Row"]` : "Record<string, unknown>";
|
|
204
|
-
|
|
205
|
+
const pivot = relation.kind === "manyToMany" ? pivotType(relation.through.properties, "read") : void 0;
|
|
206
|
+
const elementType = pivot ? `${rowType} & { _pivot?: ${pivot} }` : rowType;
|
|
207
|
+
return relation.cardinality === "many" ? `Array<${elementType}>` : elementType;
|
|
205
208
|
}
|
|
206
209
|
/**
|
|
207
210
|
* Map every slug to the property name it is reachable under on `client.data`.
|
|
@@ -228,6 +231,19 @@ function buildAccessors(collections) {
|
|
|
228
231
|
}
|
|
229
232
|
return accessors;
|
|
230
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* A column's type on a write: its own type, or `null` too when it is not
|
|
236
|
+
* required.
|
|
237
|
+
*
|
|
238
|
+
* `Row` has typed a non-required column `T | null` for a while; its write twins
|
|
239
|
+
* did not, so the one way to clear a value — `update(id, { publishedAt: null })`,
|
|
240
|
+
* `{ authorId: null }` to unassign, the edit-form round trip of a row that came
|
|
241
|
+
* back with nulls in it — was a compile error the server would have accepted.
|
|
242
|
+
* A primary key is never null, required or not.
|
|
243
|
+
*/
|
|
244
|
+
function writableType(prop, tsType) {
|
|
245
|
+
return prop.validation?.required || isPrimaryKey(prop) ? tsType : `${tsType} | null`;
|
|
246
|
+
}
|
|
231
247
|
/** One emitted `key: type;` line, already indented. */
|
|
232
248
|
function line(key, type, optional) {
|
|
233
249
|
return ` ${emitKey(key)}${optional ? "?" : ""}: ${type};`;
|
|
@@ -239,9 +255,9 @@ function line(key, type, optional) {
|
|
|
239
255
|
* property. `Row` already honoured that; `Insert` and `Update` deliberately did
|
|
240
256
|
* not, on the reading that the column is stripped from responses rather than
|
|
241
257
|
* from writes. That left the generated types as the one place a password hash
|
|
242
|
-
* was still named, and it invited a client to send one. The server
|
|
243
|
-
*
|
|
244
|
-
*
|
|
258
|
+
* was still named, and it invited a client to send one. The server refuses
|
|
259
|
+
* such a field on a write (`VALIDATION_EXCLUDED_FIELDS`), so nothing generated
|
|
260
|
+
* advertises it.
|
|
245
261
|
*
|
|
246
262
|
* Read through `effectiveAccess`, so the flag and its longhand
|
|
247
263
|
* `access: { read: [], write: [] }` produce the same file. A *role* rule is
|
|
@@ -265,6 +281,28 @@ function excludedApiKeys(properties) {
|
|
|
265
281
|
}
|
|
266
282
|
return excluded;
|
|
267
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* The keys nobody may write: `access: { write: [] }`, readable or not.
|
|
286
|
+
*
|
|
287
|
+
* Off `Insert` and `Update` for the same reason `excludedApiKeys` takes a
|
|
288
|
+
* hidden property off all three — except that a field everyone can read but
|
|
289
|
+
* nobody can write stays on `Row`. The server refuses one on any write with
|
|
290
|
+
* `VALIDATION_EXCLUDED_FIELDS`, for every caller, `admin` included, so a
|
|
291
|
+
* generated `create({ computed })` compiled and could only ever be a 400.
|
|
292
|
+
*
|
|
293
|
+
* A write rule naming *roles* is not this: some caller can write it, and one
|
|
294
|
+
* generated shape cannot say which.
|
|
295
|
+
*/
|
|
296
|
+
function unwritableKeys(properties) {
|
|
297
|
+
const unwritable = /* @__PURE__ */ new Set();
|
|
298
|
+
for (const [key, rawProp] of Object.entries(properties)) {
|
|
299
|
+
const prop = rawProp;
|
|
300
|
+
if (effectiveAccess(prop)?.write?.length !== 0) continue;
|
|
301
|
+
unwritable.add(key);
|
|
302
|
+
if (prop.columnName) unwritable.add(prop.columnName);
|
|
303
|
+
}
|
|
304
|
+
return unwritable;
|
|
305
|
+
}
|
|
268
306
|
function generateTypedefs(input) {
|
|
269
307
|
const collections = sortCollectionsBySlug(input);
|
|
270
308
|
const accessors = buildAccessors(collections);
|
|
@@ -329,39 +367,42 @@ function generateTypedefs(input) {
|
|
|
329
367
|
}
|
|
330
368
|
lines.push(" };");
|
|
331
369
|
lines.push(" Insert: {");
|
|
370
|
+
const notWritable = /* @__PURE__ */ new Set([...excluded, ...unwritableKeys(properties)]);
|
|
332
371
|
emittedKeys.clear();
|
|
333
|
-
for (const key of
|
|
372
|
+
for (const key of notWritable) emittedKeys.add(key);
|
|
334
373
|
for (const [key, rawProp] of Object.entries(properties)) {
|
|
335
374
|
const prop = rawProp;
|
|
336
375
|
if (prop.type === "relation") continue;
|
|
337
|
-
if (
|
|
376
|
+
if (notWritable.has(key)) continue;
|
|
338
377
|
const tsType = propertyToTypeScriptType(prop);
|
|
339
378
|
const isOptional = !prop.validation?.required || isAutoAssignedId(prop);
|
|
340
|
-
lines.push(line(key, tsType, isOptional));
|
|
379
|
+
lines.push(line(key, writableType(prop, tsType), isOptional));
|
|
341
380
|
emittedKeys.add(key);
|
|
342
381
|
}
|
|
343
382
|
emitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, false);
|
|
344
383
|
lines.push(" };");
|
|
345
384
|
lines.push(" Update: {");
|
|
346
385
|
emittedKeys.clear();
|
|
347
|
-
for (const key of
|
|
386
|
+
for (const key of notWritable) emittedKeys.add(key);
|
|
348
387
|
for (const [key, rawProp] of Object.entries(properties)) {
|
|
349
388
|
const prop = rawProp;
|
|
350
389
|
if (prop.type === "relation") continue;
|
|
351
390
|
if (isPrimaryKey(prop)) continue;
|
|
352
|
-
if (
|
|
353
|
-
lines.push(line(key, propertyToTypeScriptType(prop), true));
|
|
391
|
+
if (notWritable.has(key)) continue;
|
|
392
|
+
lines.push(line(key, writableType(prop, propertyToTypeScriptType(prop)), true));
|
|
354
393
|
emittedKeys.add(key);
|
|
355
394
|
}
|
|
356
395
|
emitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, true);
|
|
357
396
|
lines.push(" };");
|
|
358
|
-
lines.push(" Relations:
|
|
397
|
+
if (Object.keys(resolvedRelations).length === 0) lines.push(" Relations: Record<never, never>;");
|
|
398
|
+
else lines.push(" Relations: {");
|
|
359
399
|
for (const [key, relation] of Object.entries(resolvedRelations)) {
|
|
360
400
|
const slug = resolveTargetCollection(relation)?.slug ?? relation.targetSlug;
|
|
361
401
|
const accessor = slug ? accessors.get(slug) : void 0;
|
|
362
402
|
lines.push(` ${emitKey(key)}: ${accessor ? emitString(accessor) : "never"};`);
|
|
363
403
|
}
|
|
364
|
-
lines.push(" };");
|
|
404
|
+
if (Object.keys(resolvedRelations).length > 0) lines.push(" };");
|
|
405
|
+
lines.push(` Slug: ${emitString(collection.slug)};`);
|
|
365
406
|
lines.push(" };");
|
|
366
407
|
}
|
|
367
408
|
lines.push("}");
|
|
@@ -484,8 +525,9 @@ function includeHelperLines() {
|
|
|
484
525
|
function emitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, allOptional) {
|
|
485
526
|
const emit = (key, relation) => {
|
|
486
527
|
if (emittedKeys.has(key)) return;
|
|
487
|
-
const
|
|
488
|
-
|
|
528
|
+
const required = isRelationRequired(collection, relation);
|
|
529
|
+
const type = required ? foreignKeyType(relation) : `${foreignKeyType(relation)} | null`;
|
|
530
|
+
lines.push(line(key, type, allOptional || !required));
|
|
489
531
|
emittedKeys.add(key);
|
|
490
532
|
};
|
|
491
533
|
for (const relation of Object.values(resolvedRelations)) if (relation.kind === "belongsTo" && relation.localKey) emit(fieldKeyForColumn(collection, relation.localKey), relation);
|
|
@@ -494,6 +536,31 @@ function emitWritableRelations(lines, collection, properties, resolvedRelations,
|
|
|
494
536
|
const relation = findRelation(resolvedRelations, key);
|
|
495
537
|
if (relation?.kind === "belongsTo" && relation.localKey) emit(key, relation);
|
|
496
538
|
}
|
|
539
|
+
for (const [key, relation] of Object.entries(resolvedRelations)) {
|
|
540
|
+
if (relation.cardinality !== "many" || !relation.writable) continue;
|
|
541
|
+
if (emittedKeys.has(key)) continue;
|
|
542
|
+
const fk = foreignKeyType(relation);
|
|
543
|
+
const pivot = relation.kind === "manyToMany" ? pivotType(relation.through.properties, "write") : void 0;
|
|
544
|
+
const element = `${fk} | { id: ${fk}${pivot ? `; _pivot?: ${pivot}` : ""} }`;
|
|
545
|
+
lines.push(line(key, `Array<${element}>`, true));
|
|
546
|
+
emittedKeys.add(key);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* The `_pivot` a many-to-many link carries: the junction's payload columns,
|
|
551
|
+
* typed as a row's columns are on a read and as a write's on a write. `undefined`
|
|
552
|
+
* when the junction declares none, so there is no `_pivot` to name.
|
|
553
|
+
*/
|
|
554
|
+
function pivotType(pivotProperties, direction) {
|
|
555
|
+
const entries = Object.entries(pivotProperties);
|
|
556
|
+
if (entries.length === 0) return void 0;
|
|
557
|
+
return `{ ${entries.map(([key, rawProp]) => {
|
|
558
|
+
const prop = rawProp;
|
|
559
|
+
const tsType = propertyToTypeScriptType(prop);
|
|
560
|
+
const required = Boolean(prop.validation?.required);
|
|
561
|
+
const type = direction === "write" ? writableType(prop, tsType) : required ? tsType : `${tsType} | null`;
|
|
562
|
+
return `${emitKey(key)}${required && direction === "read" ? "" : "?"}: ${type};`;
|
|
563
|
+
}).join(" ")} }`;
|
|
497
564
|
}
|
|
498
565
|
//#endregion
|
|
499
566
|
//#region src/schema/doctor.ts
|
|
@@ -511,11 +578,14 @@ var doctor_exports = /* @__PURE__ */ __exportAll({
|
|
|
511
578
|
checkCollectionsVsSchema: () => checkCollectionsVsSchema,
|
|
512
579
|
checkCollectionsVsSdk: () => checkCollectionsVsSdk,
|
|
513
580
|
getExpectedColumnType: () => getExpectedColumnType,
|
|
581
|
+
junctionKeyIssues: () => junctionKeyIssues,
|
|
514
582
|
loadCollections: () => loadCollections,
|
|
515
583
|
missingExtensionIssues: () => missingExtensionIssues,
|
|
584
|
+
primaryKeyIssues: () => primaryKeyIssues,
|
|
516
585
|
renderReport: () => renderReport,
|
|
517
586
|
runDoctor: () => runDoctor,
|
|
518
|
-
schemaStampIssues: () => schemaStampIssues
|
|
587
|
+
schemaStampIssues: () => schemaStampIssues,
|
|
588
|
+
typeMismatchFix: () => typeMismatchFix
|
|
519
589
|
});
|
|
520
590
|
/**
|
|
521
591
|
* Resolve the SQL column name for a property.
|
|
@@ -736,6 +806,91 @@ async function schemaStampIssues(pool, collections) {
|
|
|
736
806
|
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."
|
|
737
807
|
}];
|
|
738
808
|
}
|
|
809
|
+
/** `"public"."posts"` — quoted, for a statement the reader will paste. */
|
|
810
|
+
var quotedTable = (schema, table) => `"${schema}"."${table}"`;
|
|
811
|
+
/**
|
|
812
|
+
* What to do about a column whose type is not the one the collection plans.
|
|
813
|
+
*
|
|
814
|
+
* Both ways out, because the doctor cannot know which side is right: the
|
|
815
|
+
* property moved, or the database did. Putting the property back always clears
|
|
816
|
+
* the finding; converting the column clears it when every value converts.
|
|
817
|
+
*/
|
|
818
|
+
function typeMismatchFix(schema, table, column, actual, planned) {
|
|
819
|
+
const back = `Either put the property back to the column's type (${actual}), or`;
|
|
820
|
+
if (!planned) return `${back} run a migration that converts the column.`;
|
|
821
|
+
const type = renderPgType(planned);
|
|
822
|
+
return `${back} convert the column when you can watch it (every value must convert): ALTER TABLE ${quotedTable(schema, table)} ALTER COLUMN "${column}" TYPE ${type} USING "${column}"::${type};`;
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* The table's primary key against the one the collection reads it by.
|
|
826
|
+
*
|
|
827
|
+
* Rebase addresses a row by the key the plan names — the `isId` property, or
|
|
828
|
+
* the implicit `id` — so a table keyed on other columns, or on none, serves
|
|
829
|
+
* reads and writes against a key that does not hold. An error, because that is
|
|
830
|
+
* what it is at runtime.
|
|
831
|
+
*/
|
|
832
|
+
function primaryKeyIssues(tablePlan, dbPk, displayName) {
|
|
833
|
+
const expected = tablePlan.primaryKey;
|
|
834
|
+
if (dbPk.length === expected.length && [...dbPk].sort().join("\0") === [...expected].sort().join("\0")) return [];
|
|
835
|
+
const table = quotedTable(tablePlan.schema, tablePlan.table);
|
|
836
|
+
const keyList = expected.map((c) => `"${c}"`).join(", ");
|
|
837
|
+
if (dbPk.length === 0) return [{
|
|
838
|
+
severity: "error",
|
|
839
|
+
category: "primary_key_mismatch",
|
|
840
|
+
table: displayName,
|
|
841
|
+
expected: expected.join(", "),
|
|
842
|
+
actual: "(none)",
|
|
843
|
+
message: `Table "${displayName}" has no primary key; the collection reads its rows by ${keyList}.`,
|
|
844
|
+
fix: `ALTER TABLE ${table} ADD PRIMARY KEY (${keyList});`
|
|
845
|
+
}];
|
|
846
|
+
const only = expected.some((column) => !dbPk.includes(column)) ? "only " : "";
|
|
847
|
+
const fix = dbPk.length === 1 ? `Mark ${only}the property for column "${dbPk[0]}" with \`isId\` — the table is keyed on it — or re-key the table on ${keyList}.` : `Mark ${only}the properties for columns ${dbPk.map((c) => `"${c}"`).join(", ")} with \`isId\` — together they are the table's composite key — or re-key the table on ${keyList}.`;
|
|
848
|
+
return [{
|
|
849
|
+
severity: "error",
|
|
850
|
+
category: "primary_key_mismatch",
|
|
851
|
+
table: displayName,
|
|
852
|
+
expected: expected.join(", "),
|
|
853
|
+
actual: dbPk.join(", "),
|
|
854
|
+
message: `Table "${displayName}" is keyed on (${dbPk.join(", ")}), but the collection reads its rows by ${keyList}.`,
|
|
855
|
+
fix
|
|
856
|
+
}];
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* A junction's two key columns: present, typed like the keys they point at,
|
|
860
|
+
* and the table keyed on the pair. Order is not compared — boot and `db push`
|
|
861
|
+
* have built the key in both orders, and either one serves the same rows.
|
|
862
|
+
*/
|
|
863
|
+
function junctionKeyIssues(junctionPlan, dbColumns, dbPk, displayName) {
|
|
864
|
+
const issues = [];
|
|
865
|
+
const byName = new Map(dbColumns.map((c) => [c.column_name, c]));
|
|
866
|
+
for (const column of junctionPlan.columns) {
|
|
867
|
+
const dbCol = byName.get(column.column);
|
|
868
|
+
if (!dbCol) {
|
|
869
|
+
issues.push({
|
|
870
|
+
severity: "error",
|
|
871
|
+
category: "missing_column",
|
|
872
|
+
table: displayName,
|
|
873
|
+
column: column.column,
|
|
874
|
+
message: `Junction table "${displayName}" has no "${column.column}" column.`,
|
|
875
|
+
fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
|
|
876
|
+
});
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
const expected = INFORMATION_SCHEMA_NAMES[column.type.kind];
|
|
880
|
+
if (dbCol.data_type !== expected) issues.push({
|
|
881
|
+
severity: "warning",
|
|
882
|
+
category: "type_mismatch",
|
|
883
|
+
table: displayName,
|
|
884
|
+
column: column.column,
|
|
885
|
+
expected,
|
|
886
|
+
actual: dbCol.data_type,
|
|
887
|
+
message: `Junction column "${column.column}" in "${displayName}" is ${dbCol.data_type}, but the key it points at is ${expected}.`,
|
|
888
|
+
fix: typeMismatchFix(junctionPlan.schema, junctionPlan.table, column.column, dbCol.data_type, column.type)
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
issues.push(...primaryKeyIssues(junctionPlan, dbPk, displayName));
|
|
892
|
+
return issues;
|
|
893
|
+
}
|
|
739
894
|
async function checkCollectionsVsDatabase(collections, databaseUrl) {
|
|
740
895
|
const issues = [];
|
|
741
896
|
const pgModule = await import("pg");
|
|
@@ -767,15 +922,27 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
|
|
|
767
922
|
if (!columnsByTable.has(key)) columnsByTable.set(key, []);
|
|
768
923
|
columnsByTable.get(key).push(row);
|
|
769
924
|
}
|
|
770
|
-
const enumsResult = await pool.query(`SELECT t.typname
|
|
925
|
+
const enumsResult = await pool.query(`SELECT n.nspname AS enum_schema, t.typname AS enum_name, e.enumlabel AS enum_value
|
|
771
926
|
FROM pg_type t
|
|
927
|
+
JOIN pg_namespace n ON n.oid = t.typnamespace
|
|
772
928
|
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
773
|
-
ORDER BY t.typname, e.enumsortorder`);
|
|
929
|
+
ORDER BY n.nspname, t.typname, e.enumsortorder`);
|
|
774
930
|
const enumsByName = /* @__PURE__ */ new Map();
|
|
775
931
|
for (const row of enumsResult.rows) {
|
|
776
|
-
|
|
777
|
-
enumsByName.
|
|
932
|
+
const key = `${row.enum_schema}.${row.enum_name}`;
|
|
933
|
+
if (!enumsByName.has(key)) enumsByName.set(key, []);
|
|
934
|
+
enumsByName.get(key).push(row.enum_value);
|
|
778
935
|
}
|
|
936
|
+
const pksResult = await pool.query(`SELECT n.nspname AS table_schema, c.relname AS table_name,
|
|
937
|
+
array_agg(a.attname::text ORDER BY array_position(i.indkey::int2[], a.attnum)) AS columns
|
|
938
|
+
FROM pg_index i
|
|
939
|
+
JOIN pg_class c ON c.oid = i.indrelid
|
|
940
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
941
|
+
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(i.indkey::int2[])
|
|
942
|
+
WHERE i.indisprimary AND n.nspname = ANY($1)
|
|
943
|
+
GROUP BY n.nspname, c.relname`, [schemas]);
|
|
944
|
+
const pksByTable = /* @__PURE__ */ new Map();
|
|
945
|
+
for (const row of pksResult.rows) pksByTable.set(row.table_schema === "public" ? row.table_name : `${row.table_schema}.${row.table_name}`, row.columns);
|
|
779
946
|
const fksResult = await pool.query(`SELECT
|
|
780
947
|
tc.constraint_name,
|
|
781
948
|
tc.table_schema,
|
|
@@ -800,6 +967,14 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
|
|
|
800
967
|
}
|
|
801
968
|
const postgresCollections = relationalCollections(collections);
|
|
802
969
|
const junctionSpecs = resolveJunctionSpecs(postgresCollections);
|
|
970
|
+
let plan;
|
|
971
|
+
try {
|
|
972
|
+
plan = planSchema(postgresCollections);
|
|
973
|
+
} catch {
|
|
974
|
+
plan = void 0;
|
|
975
|
+
}
|
|
976
|
+
const tablePlans = new Map((plan?.tables ?? []).map((t) => [t.qualified, t]));
|
|
977
|
+
const checkedJunctions = /* @__PURE__ */ new Set();
|
|
803
978
|
for (const collection of postgresCollections) {
|
|
804
979
|
const tableName = getTableName(collection);
|
|
805
980
|
const schemaName = collection.schema || "public";
|
|
@@ -816,11 +991,8 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
|
|
|
816
991
|
}
|
|
817
992
|
const dbColumns = columnsByTable.get(fullTableName) ?? [];
|
|
818
993
|
const dbColumnMap = new Map(dbColumns.map((c) => [c.column_name, c]));
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
"created_on",
|
|
822
|
-
"updated_on"
|
|
823
|
-
]);
|
|
994
|
+
const tablePlan = tablePlans.get(`${schemaName}.${tableName}`);
|
|
995
|
+
if (tablePlan) issues.push(...primaryKeyIssues(tablePlan, pksByTable.get(fullTableName) ?? [], fullTableName));
|
|
824
996
|
for (const [propName, prop] of Object.entries(collection.properties ?? {})) {
|
|
825
997
|
if (prop.type === "relation") {
|
|
826
998
|
const relation = findRelation(resolveCollectionRelations(collection), prop.relation?.relationName ?? propName);
|
|
@@ -855,7 +1027,6 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
|
|
|
855
1027
|
continue;
|
|
856
1028
|
}
|
|
857
1029
|
const colName = resolveColumnName(propName, prop);
|
|
858
|
-
if (systemColumns.has(colName)) continue;
|
|
859
1030
|
const dbCol = dbColumnMap.get(colName);
|
|
860
1031
|
if (!dbCol) {
|
|
861
1032
|
issues.push({
|
|
@@ -888,29 +1059,32 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
|
|
|
888
1059
|
if (expectedUdt && dbCol.udt_name !== expectedUdt) isMismatch = true;
|
|
889
1060
|
}
|
|
890
1061
|
}
|
|
891
|
-
if (isMismatch)
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1062
|
+
if (isMismatch) {
|
|
1063
|
+
const planned = tablePlan?.columns.find((c) => c.column === colName)?.type;
|
|
1064
|
+
issues.push({
|
|
1065
|
+
severity: "warning",
|
|
1066
|
+
category: "type_mismatch",
|
|
1067
|
+
table: fullTableName,
|
|
1068
|
+
column: colName,
|
|
1069
|
+
expected: prop.type === "vector" ? "vector" : expectedType,
|
|
1070
|
+
actual: dbCol.udt_name === "vector" ? "vector" : actualType,
|
|
1071
|
+
message: `Column "${colName}" in table "${fullTableName}": expected type "${prop.type === "vector" ? "vector" : expectedType}" but found "${dbCol.udt_name === "vector" ? "vector" : actualType}".`,
|
|
1072
|
+
fix: typeMismatchFix(schemaName, tableName, colName, actualType, planned)
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
901
1075
|
}
|
|
902
1076
|
if (prop.type === "string" && prop.enum) {
|
|
903
1077
|
const enumValues = prop.enum;
|
|
904
1078
|
if (enumValues) {
|
|
905
|
-
const enumName = `${tableName}_${colName}
|
|
906
|
-
const dbEnumValues = enumsByName.get(enumName);
|
|
1079
|
+
const enumName = toPostgresIdentifier(`${tableName}_${colName}`);
|
|
1080
|
+
const dbEnumValues = enumsByName.get(`${schemaName}.${enumName}`);
|
|
907
1081
|
if (!dbEnumValues) issues.push({
|
|
908
1082
|
severity: "warning",
|
|
909
1083
|
category: "missing_enum",
|
|
910
1084
|
table: fullTableName,
|
|
911
1085
|
column: colName,
|
|
912
1086
|
expected: enumName,
|
|
913
|
-
message: `Enum type "${enumName}" is defined in collection but not found in the database.`,
|
|
1087
|
+
message: `Enum type "${schemaName}"."${enumName}" is defined in collection but not found in the database.`,
|
|
914
1088
|
fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
|
|
915
1089
|
});
|
|
916
1090
|
else {
|
|
@@ -948,6 +1122,11 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
|
|
|
948
1122
|
message: `Junction table "${fullJunctionTable}" for many-to-many relation "${relation.relationName}" is missing.`,
|
|
949
1123
|
fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
|
|
950
1124
|
});
|
|
1125
|
+
else if (!checkedJunctions.has(fullJunctionTable)) {
|
|
1126
|
+
checkedJunctions.add(fullJunctionTable);
|
|
1127
|
+
const junctionPlan = tablePlans.get(`${junctionSchema}.${junctionTable}`);
|
|
1128
|
+
if (junctionPlan) issues.push(...junctionKeyIssues(junctionPlan, columnsByTable.get(fullJunctionTable) ?? [], pksByTable.get(fullJunctionTable) ?? [], fullJunctionTable));
|
|
1129
|
+
}
|
|
951
1130
|
}
|
|
952
1131
|
}
|
|
953
1132
|
} finally {
|
|
@@ -1032,10 +1211,12 @@ function formatCategory(cat) {
|
|
|
1032
1211
|
missing_column: "Missing Column",
|
|
1033
1212
|
type_mismatch: "Type Mismatch",
|
|
1034
1213
|
missing_constraint: "Missing Constraint",
|
|
1214
|
+
primary_key_mismatch: "Primary Key Mismatch",
|
|
1035
1215
|
schema_stale: "Stale Schema",
|
|
1036
1216
|
missing_enum: "Missing Enum",
|
|
1037
1217
|
enum_value_mismatch: "Enum Value Mismatch",
|
|
1038
1218
|
missing_foreign_key: "Missing Foreign Key",
|
|
1219
|
+
orphaned_junction: "Orphaned Junction Table",
|
|
1039
1220
|
sdk_stale: "Stale SDK Types",
|
|
1040
1221
|
sdk_not_generated: "SDK Types Not Generated",
|
|
1041
1222
|
sdk_ungeneratable: "SDK Cannot Be Generated"
|
|
@@ -1105,4 +1286,4 @@ async function runDoctor(options) {
|
|
|
1105
1286
|
//#endregion
|
|
1106
1287
|
export { loadCollections as n, runDoctor as r, doctor_exports as t };
|
|
1107
1288
|
|
|
1108
|
-
//# sourceMappingURL=doctor-
|
|
1289
|
+
//# sourceMappingURL=doctor-C-sYWbmt.js.map
|