@rebasepro/server-postgres 0.23.0 → 0.24.1-canary.g19596ef
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/README.md +16 -0
- package/dist/{BranchService-DGPL6G_C.js → BranchService-UgKa416x.js} +47 -19
- package/dist/BranchService-UgKa416x.js.map +1 -0
- package/dist/PostgresBackendDriver.d.ts +187 -13
- package/dist/PostgresBootstrapper.d.ts +101 -2
- package/dist/auth/ensure-tables.d.ts +12 -0
- package/dist/auth/services.d.ts +78 -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/backup-cli.d.ts +75 -2
- package/dist/backup/backup-service.d.ts +2 -2
- package/dist/backup/pg-binary.d.ts +5 -0
- package/dist/{backup-cli-24v4OlSp.js → backup-cli-yiKd1GsC.js} +168 -75
- package/dist/backup-cli-yiKd1GsC.js.map +1 -0
- package/dist/{backup-service-HQ9GC4tN.js → backup-service-AZWc2wvI.js} +104 -35
- package/dist/backup-service-AZWc2wvI.js.map +1 -0
- package/dist/boot-ddl-B9ZmBJ75.js +105 -0
- package/dist/boot-ddl-B9ZmBJ75.js.map +1 -0
- package/dist/cli-console-DGee3Zl7.js +225 -0
- package/dist/cli-console-DGee3Zl7.js.map +1 -0
- package/dist/cli-console.d.ts +39 -0
- package/dist/cli-errors-oVUuD8k0.js +2180 -0
- package/dist/cli-errors-oVUuD8k0.js.map +1 -0
- package/dist/cli-errors.d.ts +133 -9
- package/dist/{cli-flags-BglvpjHv.js → cli-flags-DnPHXjRH.js} +56 -6
- package/dist/cli-flags-DnPHXjRH.js.map +1 -0
- package/dist/cli-flags.d.ts +15 -0
- package/dist/cli-helpers.d.ts +88 -34
- package/dist/cli-output.d.ts +16 -28
- package/dist/{arg-Dni7MzLB.js → cli-prompt-egtNKkZl.js} +49 -3
- package/dist/cli-prompt-egtNKkZl.js.map +1 -0
- package/dist/cli-prompt.d.ts +19 -0
- package/dist/cli-push.d.ts +87 -0
- package/dist/cli-scratch-database.d.ts +33 -8
- package/dist/cli.js +826 -230
- package/dist/cli.js.map +1 -1
- package/dist/{column-plan-helpers-DF-8dTVa.js → column-plan-helpers-D_V4oaZh.js} +43 -27
- package/dist/column-plan-helpers-D_V4oaZh.js.map +1 -0
- package/dist/{databasePoolManager-Bj5FbeAs.js → connection-C938nR3E.js} +25 -113
- package/dist/connection-C938nR3E.js.map +1 -0
- package/dist/connection.d.ts +13 -2
- package/dist/data-transformer.d.ts +0 -8
- package/dist/databasePoolManager-DZgnBOgj.js +119 -0
- package/dist/databasePoolManager-DZgnBOgj.js.map +1 -0
- package/dist/deferred-index-build-BqmEHGaD.js +1051 -0
- package/dist/deferred-index-build-BqmEHGaD.js.map +1 -0
- package/dist/{doctor-Cb2thZ8s.js → doctor-CUAAOFZm.js} +281 -89
- package/dist/doctor-CUAAOFZm.js.map +1 -0
- package/dist/ensure-collection-policies-Bb51V2OV.js +541 -0
- package/dist/ensure-collection-policies-Bb51V2OV.js.map +1 -0
- package/dist/ensure-collection-tables-egjGBjhv.js +532 -0
- package/dist/ensure-collection-tables-egjGBjhv.js.map +1 -0
- package/dist/{ensure-tables-Dhn9KM3B.js → ensure-tables-DMPWhJ6R.js} +180 -77
- package/dist/ensure-tables-DMPWhJ6R.js.map +1 -0
- package/dist/enum-labels-kept-Msq5gMcb.js +56 -0
- package/dist/enum-labels-kept-Msq5gMcb.js.map +1 -0
- package/dist/generate-drizzle-schema-DrA-YgXv.js +57 -0
- package/dist/generate-drizzle-schema-DrA-YgXv.js.map +1 -0
- package/dist/{generate-drizzle-schema-logic-BfzK7UQd.js → generate-drizzle-schema-logic-BaT0H7Gq.js} +29 -11
- package/dist/generate-drizzle-schema-logic-BaT0H7Gq.js.map +1 -0
- package/dist/generate-files-OoRJLEdk.js +199 -0
- package/dist/generate-files-OoRJLEdk.js.map +1 -0
- package/dist/generate-postgres-ddl-logic-B4k9djAV.js +152 -0
- package/dist/generate-postgres-ddl-logic-B4k9djAV.js.map +1 -0
- package/dist/{generated-schema-staleness-Di9c4ZxN.js → generated-schema-staleness-BWN_PsMi.js} +2 -2
- package/dist/{generated-schema-staleness-Di9c4ZxN.js.map → generated-schema-staleness-BWN_PsMi.js.map} +1 -1
- package/dist/generated-sql.d.ts +34 -0
- package/dist/history/HistoryService.d.ts +64 -5
- package/dist/history/ensure-history-table.d.ts +63 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +139 -16738
- package/dist/index.es.js.map +1 -1
- package/dist/{introspect-db-logic-WMuAfxvw.js → introspect-db-logic-BCc1Jym2.js} +654 -353
- package/dist/introspect-db-logic-BCc1Jym2.js.map +1 -0
- package/dist/introspect-db-queries-BhhJg0b8.js +364 -0
- package/dist/introspect-db-queries-BhhJg0b8.js.map +1 -0
- package/dist/open-rebase-CAjWpPjs.js +20895 -0
- package/dist/open-rebase-CAjWpPjs.js.map +1 -0
- package/dist/open-rebase.d.ts +94 -0
- package/dist/pg-error-utils-Bs_Ue4wn.js +395 -0
- package/dist/pg-error-utils-Bs_Ue4wn.js.map +1 -0
- package/dist/{plan-schema-C0fxM8dY.js → plan-schema-CsNgK0qi.js} +279 -534
- package/dist/plan-schema-CsNgK0qi.js.map +1 -0
- package/dist/{policy-drift-DljYdrpW.js → policy-drift-BKC50y2p.js} +33 -9
- package/dist/policy-drift-BKC50y2p.js.map +1 -0
- package/dist/push-index-prebuild-C_qhyoJa.js +45 -0
- package/dist/push-index-prebuild-C_qhyoJa.js.map +1 -0
- package/dist/push-legacy-renames-5g9h3HOa.js +34 -0
- package/dist/push-legacy-renames-5g9h3HOa.js.map +1 -0
- package/dist/{generate-postgres-ddl-logic-Bt2d2mRH.js → render-ddl-CrfEe__B.js} +24 -157
- package/dist/render-ddl-CrfEe__B.js.map +1 -0
- package/dist/{rls-bootstrap-sql-B8EclDyM.js → rls-bootstrap-sql-Bd-hJk08.js} +125 -1101
- package/dist/rls-bootstrap-sql-Bd-hJk08.js.map +1 -0
- package/dist/{rls-enforcement-C6Xk0lA6.js → rls-enforcement-D7P4-ZYX.js} +138 -18
- package/dist/rls-enforcement-D7P4-ZYX.js.map +1 -0
- package/dist/{__vite-browser-external-BnuHet1e.js → rolldown-runtime-V7rFbMRe.js} +1 -8
- package/dist/schema/atlas-exclude.d.ts +42 -0
- package/dist/schema/auth-schema.d.ts +172 -2
- package/dist/schema/boot-ddl.d.ts +47 -0
- package/dist/schema/carved-out-migration.d.ts +5 -4
- package/dist/schema/classify-change.d.ts +29 -1
- package/dist/schema/collection-index.d.ts +45 -0
- package/dist/schema/column-plan-helpers.d.ts +38 -21
- package/dist/schema/deferred-index-build.d.ts +131 -0
- package/dist/schema/destructive-sql.d.ts +50 -2
- package/dist/schema/doctor-cli.js +110 -6
- package/dist/schema/doctor-cli.js.map +1 -1
- package/dist/schema/doctor-search-scale.d.ts +45 -0
- package/dist/schema/doctor.d.ts +53 -1
- package/dist/schema/ensure-collection-policies.d.ts +95 -35
- package/dist/schema/ensure-collection-tables.d.ts +56 -3
- package/dist/schema/enum-labels-kept.d.ts +26 -0
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-files.d.ts +44 -0
- package/dist/schema/generate-next-step.d.ts +0 -13
- package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -5
- package/dist/schema/generate-postgres-ddl.js +10 -81
- package/dist/schema/generate-postgres-ddl.js.map +1 -1
- package/dist/schema/generate-schema-commit.d.ts +12 -0
- package/dist/schema/generated-schema-diff.d.ts +7 -0
- package/dist/schema/introspect-db-logic.d.ts +63 -0
- package/dist/schema/introspect-db-queries.d.ts +21 -2
- 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-structure.d.ts +10 -0
- package/dist/schema/introspect-db.js +29 -21
- package/dist/schema/introspect-db.js.map +1 -1
- package/dist/schema/migrate-baseline.d.ts +33 -0
- package/dist/schema/migration-post-process.d.ts +34 -0
- package/dist/schema/plan/diff-plan.d.ts +127 -1
- package/dist/schema/plan/plan-schema.d.ts +16 -8
- package/dist/schema/plan/render-ddl.d.ts +8 -1
- package/dist/schema/plan/types.d.ts +45 -11
- package/dist/schema/push-index-prebuild.d.ts +25 -0
- package/dist/schema/push-legacy-renames.d.ts +20 -0
- package/dist/schema/rls-bootstrap-sql.d.ts +15 -0
- package/dist/schema/undeclarable-constraints.d.ts +59 -0
- 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 +39 -4
- package/dist/services/BranchService.d.ts +29 -1
- package/dist/services/FetchService.d.ts +63 -12
- package/dist/services/PersistService.d.ts +15 -0
- package/dist/services/RelationService.d.ts +106 -3
- package/dist/services/cdc/CdcListener.d.ts +26 -10
- package/dist/services/cdc/identity-columns.d.ts +19 -0
- package/dist/services/cdc/trigger-cdc.d.ts +180 -18
- package/dist/services/channel-bus/PostgresChannelBus.d.ts +12 -7
- package/dist/services/channel-bus/index.d.ts +1 -1
- package/dist/services/channel-history.d.ts +32 -5
- package/dist/services/collection-helpers.d.ts +26 -1
- package/dist/services/field-viewer.d.ts +15 -1
- package/dist/services/pg-notify-listener.d.ts +95 -6
- package/dist/services/realtimeService.d.ts +467 -53
- package/dist/services/row-pipeline.d.ts +16 -9
- package/dist/services/socket-liveness.d.ts +85 -0
- package/dist/services/soft-delete.d.ts +12 -0
- package/dist/services/sql-script.d.ts +275 -0
- package/dist/services/vector-iterative-scan.d.ts +40 -0
- package/dist/services/write-depth.d.ts +22 -0
- package/dist/services/write-transaction-scope.d.ts +15 -3
- package/dist/socket-opt-ins.d.ts +64 -0
- package/dist/source-DB6xJv3p.js +873 -0
- package/dist/source-DB6xJv3p.js.map +1 -0
- package/dist/tsx-loader.d.ts +14 -0
- package/dist/utils/connection-string.d.ts +31 -0
- package/dist/utils/drizzle-conditions.d.ts +63 -3
- package/dist/utils/error-cause.d.ts +7 -0
- package/dist/utils/pg-error-utils.d.ts +2 -4
- package/dist/utils/result-rows.d.ts +10 -0
- package/dist/{vector-index-CMJKb3C9.js → vector-index-BFhyEaiz.js} +2 -2
- package/dist/{vector-index-CMJKb3C9.js.map → vector-index-BFhyEaiz.js.map} +1 -1
- package/dist/websocket-DUiiwQd2.js +1632 -0
- package/dist/websocket-DUiiwQd2.js.map +1 -0
- package/dist/websocket.d.ts +63 -18
- package/package.json +7 -7
- package/dist/BranchService-DGPL6G_C.js.map +0 -1
- package/dist/__vite-browser-external-BnuHet1e.js.map +0 -1
- package/dist/arg-Dni7MzLB.js.map +0 -1
- package/dist/backup-cli-24v4OlSp.js.map +0 -1
- package/dist/backup-service-HQ9GC4tN.js.map +0 -1
- package/dist/cli-errors-B8qHg02P.js +0 -798
- package/dist/cli-errors-B8qHg02P.js.map +0 -1
- package/dist/cli-flags-BglvpjHv.js.map +0 -1
- package/dist/cli-output-CNdMql-L.js +0 -47
- package/dist/cli-output-CNdMql-L.js.map +0 -1
- package/dist/collection-index-DdnsxiJ_.js +0 -286
- package/dist/collection-index-DdnsxiJ_.js.map +0 -1
- package/dist/column-plan-helpers-DF-8dTVa.js.map +0 -1
- package/dist/databasePoolManager-Bj5FbeAs.js.map +0 -1
- package/dist/doctor-Cb2thZ8s.js.map +0 -1
- package/dist/ensure-collection-policies-RHUcEp4v.js +0 -105
- package/dist/ensure-collection-policies-RHUcEp4v.js.map +0 -1
- package/dist/ensure-collection-tables-BEEjn5cn.js +0 -857
- 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-drizzle-schema-yzY_BLhr.js +0 -123
- package/dist/generate-drizzle-schema-yzY_BLhr.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
- package/dist/source-Br7L7GOI.js +0 -410
- package/dist/source-Br7L7GOI.js.map +0 -1
|
@@ -1,54 +1,20 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { i as
|
|
6
|
-
import { t as
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { n as describeDeclarationDifferences, t as compareGeneratedDeclarations } from "./generated-schema-staleness-
|
|
3
|
+
import { n as __exportAll } from "./rolldown-runtime-V7rFbMRe.js";
|
|
4
|
+
import { i as diagnoseDbError, n as deepestErrorMessage, s as parseHostInfo } from "./cli-errors-oVUuD8k0.js";
|
|
5
|
+
import { i as outError, r as out, t as chalk } from "./source-DB6xJv3p.js";
|
|
6
|
+
import { n as planSchema, t as columnPgType } from "./plan-schema-CsNgK0qi.js";
|
|
7
|
+
import { t as generateSchema } from "./generate-drizzle-schema-logic-BaT0H7Gq.js";
|
|
8
|
+
import { r as renderPgType } from "./render-ddl-CrfEe__B.js";
|
|
9
|
+
import { n as describeDeclarationDifferences, t as compareGeneratedDeclarations } from "./generated-schema-staleness-BWN_PsMi.js";
|
|
10
10
|
import { loadCollectionsFromDirectory } from "@rebasepro/server";
|
|
11
11
|
import { computeSchemaVersion, getDeclaredSubcollections, isManyToMany, isPostgresCollectionConfig } from "@rebasepro/types";
|
|
12
|
-
import { effectiveAccess, fieldKeyForColumn, findRelation, getTableName, isRelationRequired, relationalCollections, resolveCollectionRelations, resolveJunctionSpecs, sortCollectionsBySlug } from "@rebasepro/common";
|
|
13
|
-
import { toSnakeCase } from "@rebasepro/utils";
|
|
12
|
+
import { effectiveAccess, fieldKeyForColumn, findRelation, getTableName, isRelationRequired, relationalCollections, resolveCollectionRelations, resolveJunctionSpecs, sortCollectionsBySlug, toCollectionAccessor, toSafeIdentifier } from "@rebasepro/common";
|
|
13
|
+
import { toPostgresIdentifier, toSnakeCase } from "@rebasepro/utils";
|
|
14
14
|
import * as fs$1 from "fs";
|
|
15
15
|
import { promises } from "fs";
|
|
16
16
|
//#region ../codegen/dist/index.es.js
|
|
17
17
|
/**
|
|
18
|
-
* Utility functions for the SDK generator
|
|
19
|
-
*/
|
|
20
|
-
/**
|
|
21
|
-
* Convert a slug/snake_case string to PascalCase
|
|
22
|
-
* e.g. "private_notes" → "PrivateNotes"
|
|
23
|
-
*
|
|
24
|
-
* Capitals already inside a word are meaningful and are kept: lowercasing the
|
|
25
|
-
* tail of every chunk turned "TestEntities" into "Testentities", which is what
|
|
26
|
-
* ended up in the generated type names. SHOUTING_CASE is the one shape where
|
|
27
|
-
* the tail is not meaningful, so it is folded down.
|
|
28
|
-
*/
|
|
29
|
-
function toPascalCase(str) {
|
|
30
|
-
return str.split(/[_\-\s]+/).filter(Boolean).map((word) => {
|
|
31
|
-
const rest = /^[A-Z0-9]+$/.test(word) ? word.slice(1).toLowerCase() : word.slice(1);
|
|
32
|
-
return word.charAt(0).toUpperCase() + rest;
|
|
33
|
-
}).join("");
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Convert a slug/snake_case string to camelCase
|
|
37
|
-
* e.g. "private_notes" → "privateNotes"
|
|
38
|
-
*/
|
|
39
|
-
function toCamelCase(str) {
|
|
40
|
-
if (!/[_\-\s]/.test(str)) return str.charAt(0).toLowerCase() + str.slice(1);
|
|
41
|
-
const pascal = toPascalCase(str);
|
|
42
|
-
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Convert a slug to a safe JS identifier
|
|
46
|
-
* e.g. "private-notes" → "privateNotes"
|
|
47
|
-
*/
|
|
48
|
-
function toSafeIdentifier(str) {
|
|
49
|
-
return toCamelCase(str.replace(/[^a-zA-Z0-9_]/g, "_"));
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
18
|
* A schema that cannot be expressed as a valid TypeScript file.
|
|
53
19
|
*
|
|
54
20
|
* Thrown rather than emitted. The generator used to concatenate whatever it was
|
|
@@ -201,7 +167,9 @@ function includedRelationType(relation, accessors) {
|
|
|
201
167
|
const slug = resolveTargetCollection(relation)?.slug ?? relation.targetSlug;
|
|
202
168
|
const accessor = slug ? accessors.get(slug) : void 0;
|
|
203
169
|
const rowType = accessor ? `Database[${emitString(accessor)}]["Row"]` : "Record<string, unknown>";
|
|
204
|
-
|
|
170
|
+
const pivot = relation.kind === "manyToMany" ? pivotType(relation.through.properties, "read") : void 0;
|
|
171
|
+
const elementType = pivot ? `${rowType} & { _pivot?: ${pivot} }` : rowType;
|
|
172
|
+
return relation.cardinality === "many" ? `Array<${elementType}>` : elementType;
|
|
205
173
|
}
|
|
206
174
|
/**
|
|
207
175
|
* Map every slug to the property name it is reachable under on `client.data`.
|
|
@@ -221,13 +189,29 @@ function buildAccessors(collections) {
|
|
|
221
189
|
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`.");
|
|
222
190
|
const safe = toSafeIdentifier(slug);
|
|
223
191
|
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.`);
|
|
224
|
-
const
|
|
225
|
-
if (
|
|
226
|
-
bySafeName.
|
|
227
|
-
|
|
192
|
+
const accessor = toCollectionAccessor(slug);
|
|
193
|
+
if (accessor !== safe) console.warn(`[codegen] The collection ${emitString(slug)} would be \`client.data.${safe}\`, a name the client reserves, so its typed accessor is \`client.data.${accessor}\`. \`client.data.collection(${emitString(slug)})\` reaches it too.`);
|
|
194
|
+
const existing = bySafeName.get(accessor);
|
|
195
|
+
if (existing !== void 0) throw new CodegenError(`The collections ${emitString(existing)} and ${emitString(slug)} both generate the accessor "${accessor}", 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.`);
|
|
196
|
+
bySafeName.set(accessor, slug);
|
|
197
|
+
accessors.set(slug, accessor);
|
|
228
198
|
}
|
|
229
199
|
return accessors;
|
|
230
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* A column's type on a write: its own type (a date also as a `Date`), or
|
|
203
|
+
* `null` too when it is not required.
|
|
204
|
+
*
|
|
205
|
+
* `Row` has typed a non-required column `T | null` for a while; its write twins
|
|
206
|
+
* did not, so the one way to clear a value — `update(id, { publishedAt: null })`,
|
|
207
|
+
* `{ authorId: null }` to unassign, the edit-form round trip of a row that came
|
|
208
|
+
* back with nulls in it — was a compile error the server would have accepted.
|
|
209
|
+
* A primary key is never null, required or not.
|
|
210
|
+
*/
|
|
211
|
+
function writableType(prop, tsType) {
|
|
212
|
+
const written = prop.type === "date" ? `${tsType} | Date` : tsType;
|
|
213
|
+
return prop.validation?.required || isPrimaryKey(prop) ? written : `${written} | null`;
|
|
214
|
+
}
|
|
231
215
|
/** One emitted `key: type;` line, already indented. */
|
|
232
216
|
function line(key, type, optional) {
|
|
233
217
|
return ` ${emitKey(key)}${optional ? "?" : ""}: ${type};`;
|
|
@@ -239,9 +223,9 @@ function line(key, type, optional) {
|
|
|
239
223
|
* property. `Row` already honoured that; `Insert` and `Update` deliberately did
|
|
240
224
|
* not, on the reading that the column is stripped from responses rather than
|
|
241
225
|
* 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
|
-
*
|
|
226
|
+
* was still named, and it invited a client to send one. The server refuses
|
|
227
|
+
* such a field on a write (`VALIDATION_EXCLUDED_FIELDS`), so nothing generated
|
|
228
|
+
* advertises it.
|
|
245
229
|
*
|
|
246
230
|
* Read through `effectiveAccess`, so the flag and its longhand
|
|
247
231
|
* `access: { read: [], write: [] }` produce the same file. A *role* rule is
|
|
@@ -265,6 +249,28 @@ function excludedApiKeys(properties) {
|
|
|
265
249
|
}
|
|
266
250
|
return excluded;
|
|
267
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* The keys nobody may write: `access: { write: [] }`, readable or not.
|
|
254
|
+
*
|
|
255
|
+
* Off `Insert` and `Update` for the same reason `excludedApiKeys` takes a
|
|
256
|
+
* hidden property off all three — except that a field everyone can read but
|
|
257
|
+
* nobody can write stays on `Row`. The server refuses one on any write with
|
|
258
|
+
* `VALIDATION_EXCLUDED_FIELDS`, for every caller, `admin` included, so a
|
|
259
|
+
* generated `create({ computed })` compiled and could only ever be a 400.
|
|
260
|
+
*
|
|
261
|
+
* A write rule naming *roles* is not this: some caller can write it, and one
|
|
262
|
+
* generated shape cannot say which.
|
|
263
|
+
*/
|
|
264
|
+
function unwritableKeys(properties) {
|
|
265
|
+
const unwritable = /* @__PURE__ */ new Set();
|
|
266
|
+
for (const [key, rawProp] of Object.entries(properties)) {
|
|
267
|
+
const prop = rawProp;
|
|
268
|
+
if (effectiveAccess(prop)?.write?.length !== 0) continue;
|
|
269
|
+
unwritable.add(key);
|
|
270
|
+
if (prop.columnName) unwritable.add(prop.columnName);
|
|
271
|
+
}
|
|
272
|
+
return unwritable;
|
|
273
|
+
}
|
|
268
274
|
function generateTypedefs(input) {
|
|
269
275
|
const collections = sortCollectionsBySlug(input);
|
|
270
276
|
const accessors = buildAccessors(collections);
|
|
@@ -329,39 +335,42 @@ function generateTypedefs(input) {
|
|
|
329
335
|
}
|
|
330
336
|
lines.push(" };");
|
|
331
337
|
lines.push(" Insert: {");
|
|
338
|
+
const notWritable = /* @__PURE__ */ new Set([...excluded, ...unwritableKeys(properties)]);
|
|
332
339
|
emittedKeys.clear();
|
|
333
|
-
for (const key of
|
|
340
|
+
for (const key of notWritable) emittedKeys.add(key);
|
|
334
341
|
for (const [key, rawProp] of Object.entries(properties)) {
|
|
335
342
|
const prop = rawProp;
|
|
336
343
|
if (prop.type === "relation") continue;
|
|
337
|
-
if (
|
|
344
|
+
if (notWritable.has(key)) continue;
|
|
338
345
|
const tsType = propertyToTypeScriptType(prop);
|
|
339
|
-
const isOptional = !prop.validation?.required || isAutoAssignedId(prop);
|
|
340
|
-
lines.push(line(key, tsType, isOptional));
|
|
346
|
+
const isOptional = !prop.validation?.required || isAutoAssignedId(prop) || Boolean(prop.databaseDefault);
|
|
347
|
+
lines.push(line(key, writableType(prop, tsType), isOptional));
|
|
341
348
|
emittedKeys.add(key);
|
|
342
349
|
}
|
|
343
350
|
emitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, false);
|
|
344
351
|
lines.push(" };");
|
|
345
352
|
lines.push(" Update: {");
|
|
346
353
|
emittedKeys.clear();
|
|
347
|
-
for (const key of
|
|
354
|
+
for (const key of notWritable) emittedKeys.add(key);
|
|
348
355
|
for (const [key, rawProp] of Object.entries(properties)) {
|
|
349
356
|
const prop = rawProp;
|
|
350
357
|
if (prop.type === "relation") continue;
|
|
351
358
|
if (isPrimaryKey(prop)) continue;
|
|
352
|
-
if (
|
|
353
|
-
lines.push(line(key, propertyToTypeScriptType(prop), true));
|
|
359
|
+
if (notWritable.has(key)) continue;
|
|
360
|
+
lines.push(line(key, writableType(prop, propertyToTypeScriptType(prop)), true));
|
|
354
361
|
emittedKeys.add(key);
|
|
355
362
|
}
|
|
356
363
|
emitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, true);
|
|
357
364
|
lines.push(" };");
|
|
358
|
-
lines.push(" Relations:
|
|
365
|
+
if (Object.keys(resolvedRelations).length === 0) lines.push(" Relations: Record<never, never>;");
|
|
366
|
+
else lines.push(" Relations: {");
|
|
359
367
|
for (const [key, relation] of Object.entries(resolvedRelations)) {
|
|
360
368
|
const slug = resolveTargetCollection(relation)?.slug ?? relation.targetSlug;
|
|
361
369
|
const accessor = slug ? accessors.get(slug) : void 0;
|
|
362
370
|
lines.push(` ${emitKey(key)}: ${accessor ? emitString(accessor) : "never"};`);
|
|
363
371
|
}
|
|
364
|
-
lines.push(" };");
|
|
372
|
+
if (Object.keys(resolvedRelations).length > 0) lines.push(" };");
|
|
373
|
+
lines.push(` Slug: ${emitString(collection.slug)};`);
|
|
365
374
|
lines.push(" };");
|
|
366
375
|
}
|
|
367
376
|
lines.push("}");
|
|
@@ -484,8 +493,9 @@ function includeHelperLines() {
|
|
|
484
493
|
function emitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, allOptional) {
|
|
485
494
|
const emit = (key, relation) => {
|
|
486
495
|
if (emittedKeys.has(key)) return;
|
|
487
|
-
const
|
|
488
|
-
|
|
496
|
+
const required = isRelationRequired(collection, relation);
|
|
497
|
+
const type = required ? foreignKeyType(relation) : `${foreignKeyType(relation)} | null`;
|
|
498
|
+
lines.push(line(key, type, allOptional || !required));
|
|
489
499
|
emittedKeys.add(key);
|
|
490
500
|
};
|
|
491
501
|
for (const relation of Object.values(resolvedRelations)) if (relation.kind === "belongsTo" && relation.localKey) emit(fieldKeyForColumn(collection, relation.localKey), relation);
|
|
@@ -494,6 +504,31 @@ function emitWritableRelations(lines, collection, properties, resolvedRelations,
|
|
|
494
504
|
const relation = findRelation(resolvedRelations, key);
|
|
495
505
|
if (relation?.kind === "belongsTo" && relation.localKey) emit(key, relation);
|
|
496
506
|
}
|
|
507
|
+
for (const [key, relation] of Object.entries(resolvedRelations)) {
|
|
508
|
+
if (relation.cardinality !== "many" || !relation.writable) continue;
|
|
509
|
+
if (emittedKeys.has(key)) continue;
|
|
510
|
+
const fk = foreignKeyType(relation);
|
|
511
|
+
const pivot = relation.kind === "manyToMany" ? pivotType(relation.through.properties, "write") : void 0;
|
|
512
|
+
const element = `${fk} | { id: ${fk}${pivot ? `; _pivot?: ${pivot}` : ""} }`;
|
|
513
|
+
lines.push(line(key, `Array<${element}>`, true));
|
|
514
|
+
emittedKeys.add(key);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* The `_pivot` a many-to-many link carries: the junction's payload columns,
|
|
519
|
+
* typed as a row's columns are on a read and as a write's on a write. `undefined`
|
|
520
|
+
* when the junction declares none, so there is no `_pivot` to name.
|
|
521
|
+
*/
|
|
522
|
+
function pivotType(pivotProperties, direction) {
|
|
523
|
+
const entries = Object.entries(pivotProperties);
|
|
524
|
+
if (entries.length === 0) return void 0;
|
|
525
|
+
return `{ ${entries.map(([key, rawProp]) => {
|
|
526
|
+
const prop = rawProp;
|
|
527
|
+
const tsType = propertyToTypeScriptType(prop);
|
|
528
|
+
const required = Boolean(prop.validation?.required);
|
|
529
|
+
const type = direction === "write" ? writableType(prop, tsType) : required ? tsType : `${tsType} | null`;
|
|
530
|
+
return `${emitKey(key)}${required && direction === "read" ? "" : "?"}: ${type};`;
|
|
531
|
+
}).join(" ")} }`;
|
|
497
532
|
}
|
|
498
533
|
//#endregion
|
|
499
534
|
//#region src/schema/doctor.ts
|
|
@@ -511,11 +546,15 @@ var doctor_exports = /* @__PURE__ */ __exportAll({
|
|
|
511
546
|
checkCollectionsVsSchema: () => checkCollectionsVsSchema,
|
|
512
547
|
checkCollectionsVsSdk: () => checkCollectionsVsSdk,
|
|
513
548
|
getExpectedColumnType: () => getExpectedColumnType,
|
|
549
|
+
junctionKeyIssues: () => junctionKeyIssues,
|
|
514
550
|
loadCollections: () => loadCollections,
|
|
515
551
|
missingExtensionIssues: () => missingExtensionIssues,
|
|
552
|
+
orphanedJunctionIssues: () => orphanedJunctionIssues,
|
|
553
|
+
primaryKeyIssues: () => primaryKeyIssues,
|
|
516
554
|
renderReport: () => renderReport,
|
|
517
555
|
runDoctor: () => runDoctor,
|
|
518
|
-
schemaStampIssues: () => schemaStampIssues
|
|
556
|
+
schemaStampIssues: () => schemaStampIssues,
|
|
557
|
+
typeMismatchFix: () => typeMismatchFix
|
|
519
558
|
});
|
|
520
559
|
/**
|
|
521
560
|
* Resolve the SQL column name for a property.
|
|
@@ -736,6 +775,91 @@ async function schemaStampIssues(pool, collections) {
|
|
|
736
775
|
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
776
|
}];
|
|
738
777
|
}
|
|
778
|
+
/** `"public"."posts"` — quoted, for a statement the reader will paste. */
|
|
779
|
+
var quotedTable = (schema, table) => `"${schema}"."${table}"`;
|
|
780
|
+
/**
|
|
781
|
+
* What to do about a column whose type is not the one the collection plans.
|
|
782
|
+
*
|
|
783
|
+
* Both ways out, because the doctor cannot know which side is right: the
|
|
784
|
+
* property moved, or the database did. Putting the property back always clears
|
|
785
|
+
* the finding; converting the column clears it when every value converts.
|
|
786
|
+
*/
|
|
787
|
+
function typeMismatchFix(schema, table, column, actual, planned) {
|
|
788
|
+
const back = `Either put the property back to the column's type (${actual}), or`;
|
|
789
|
+
if (!planned) return `${back} run a migration that converts the column.`;
|
|
790
|
+
const type = renderPgType(planned);
|
|
791
|
+
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};`;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* The table's primary key against the one the collection reads it by.
|
|
795
|
+
*
|
|
796
|
+
* Rebase addresses a row by the key the plan names — the `isId` property, or
|
|
797
|
+
* the implicit `id` — so a table keyed on other columns, or on none, serves
|
|
798
|
+
* reads and writes against a key that does not hold. An error, because that is
|
|
799
|
+
* what it is at runtime.
|
|
800
|
+
*/
|
|
801
|
+
function primaryKeyIssues(tablePlan, dbPk, displayName) {
|
|
802
|
+
const expected = tablePlan.primaryKey;
|
|
803
|
+
if (dbPk.length === expected.length && [...dbPk].sort().join("\0") === [...expected].sort().join("\0")) return [];
|
|
804
|
+
const table = quotedTable(tablePlan.schema, tablePlan.table);
|
|
805
|
+
const keyList = expected.map((c) => `"${c}"`).join(", ");
|
|
806
|
+
if (dbPk.length === 0) return [{
|
|
807
|
+
severity: "error",
|
|
808
|
+
category: "primary_key_mismatch",
|
|
809
|
+
table: displayName,
|
|
810
|
+
expected: expected.join(", "),
|
|
811
|
+
actual: "(none)",
|
|
812
|
+
message: `Table "${displayName}" has no primary key; the collection reads its rows by ${keyList}.`,
|
|
813
|
+
fix: `ALTER TABLE ${table} ADD PRIMARY KEY (${keyList});`
|
|
814
|
+
}];
|
|
815
|
+
const only = expected.some((column) => !dbPk.includes(column)) ? "only " : "";
|
|
816
|
+
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}.`;
|
|
817
|
+
return [{
|
|
818
|
+
severity: "error",
|
|
819
|
+
category: "primary_key_mismatch",
|
|
820
|
+
table: displayName,
|
|
821
|
+
expected: expected.join(", "),
|
|
822
|
+
actual: dbPk.join(", "),
|
|
823
|
+
message: `Table "${displayName}" is keyed on (${dbPk.join(", ")}), but the collection reads its rows by ${keyList}.`,
|
|
824
|
+
fix
|
|
825
|
+
}];
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* A junction's two key columns: present, typed like the keys they point at,
|
|
829
|
+
* and the table keyed on the pair. Order is not compared — boot and `db push`
|
|
830
|
+
* have built the key in both orders, and either one serves the same rows.
|
|
831
|
+
*/
|
|
832
|
+
function junctionKeyIssues(junctionPlan, dbColumns, dbPk, displayName) {
|
|
833
|
+
const issues = [];
|
|
834
|
+
const byName = new Map(dbColumns.map((c) => [c.column_name, c]));
|
|
835
|
+
for (const column of junctionPlan.columns) {
|
|
836
|
+
const dbCol = byName.get(column.column);
|
|
837
|
+
if (!dbCol) {
|
|
838
|
+
issues.push({
|
|
839
|
+
severity: "error",
|
|
840
|
+
category: "missing_column",
|
|
841
|
+
table: displayName,
|
|
842
|
+
column: column.column,
|
|
843
|
+
message: `Junction table "${displayName}" has no "${column.column}" column.`,
|
|
844
|
+
fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
|
|
845
|
+
});
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
const expected = INFORMATION_SCHEMA_NAMES[column.type.kind];
|
|
849
|
+
if (dbCol.data_type !== expected) issues.push({
|
|
850
|
+
severity: "warning",
|
|
851
|
+
category: "type_mismatch",
|
|
852
|
+
table: displayName,
|
|
853
|
+
column: column.column,
|
|
854
|
+
expected,
|
|
855
|
+
actual: dbCol.data_type,
|
|
856
|
+
message: `Junction column "${column.column}" in "${displayName}" is ${dbCol.data_type}, but the key it points at is ${expected}.`,
|
|
857
|
+
fix: typeMismatchFix(junctionPlan.schema, junctionPlan.table, column.column, dbCol.data_type, column.type)
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
issues.push(...primaryKeyIssues(junctionPlan, dbPk, displayName));
|
|
861
|
+
return issues;
|
|
862
|
+
}
|
|
739
863
|
async function checkCollectionsVsDatabase(collections, databaseUrl) {
|
|
740
864
|
const issues = [];
|
|
741
865
|
const pgModule = await import("pg");
|
|
@@ -767,15 +891,27 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
|
|
|
767
891
|
if (!columnsByTable.has(key)) columnsByTable.set(key, []);
|
|
768
892
|
columnsByTable.get(key).push(row);
|
|
769
893
|
}
|
|
770
|
-
const enumsResult = await pool.query(`SELECT t.typname
|
|
894
|
+
const enumsResult = await pool.query(`SELECT n.nspname AS enum_schema, t.typname AS enum_name, e.enumlabel AS enum_value
|
|
771
895
|
FROM pg_type t
|
|
896
|
+
JOIN pg_namespace n ON n.oid = t.typnamespace
|
|
772
897
|
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
773
|
-
ORDER BY t.typname, e.enumsortorder`);
|
|
898
|
+
ORDER BY n.nspname, t.typname, e.enumsortorder`);
|
|
774
899
|
const enumsByName = /* @__PURE__ */ new Map();
|
|
775
900
|
for (const row of enumsResult.rows) {
|
|
776
|
-
|
|
777
|
-
enumsByName.
|
|
901
|
+
const key = `${row.enum_schema}.${row.enum_name}`;
|
|
902
|
+
if (!enumsByName.has(key)) enumsByName.set(key, []);
|
|
903
|
+
enumsByName.get(key).push(row.enum_value);
|
|
778
904
|
}
|
|
905
|
+
const pksResult = await pool.query(`SELECT n.nspname AS table_schema, c.relname AS table_name,
|
|
906
|
+
array_agg(a.attname::text ORDER BY array_position(i.indkey::int2[], a.attnum)) AS columns
|
|
907
|
+
FROM pg_index i
|
|
908
|
+
JOIN pg_class c ON c.oid = i.indrelid
|
|
909
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
910
|
+
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(i.indkey::int2[])
|
|
911
|
+
WHERE i.indisprimary AND n.nspname = ANY($1)
|
|
912
|
+
GROUP BY n.nspname, c.relname`, [schemas]);
|
|
913
|
+
const pksByTable = /* @__PURE__ */ new Map();
|
|
914
|
+
for (const row of pksResult.rows) pksByTable.set(row.table_schema === "public" ? row.table_name : `${row.table_schema}.${row.table_name}`, row.columns);
|
|
779
915
|
const fksResult = await pool.query(`SELECT
|
|
780
916
|
tc.constraint_name,
|
|
781
917
|
tc.table_schema,
|
|
@@ -800,6 +936,14 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
|
|
|
800
936
|
}
|
|
801
937
|
const postgresCollections = relationalCollections(collections);
|
|
802
938
|
const junctionSpecs = resolveJunctionSpecs(postgresCollections);
|
|
939
|
+
let plan;
|
|
940
|
+
try {
|
|
941
|
+
plan = planSchema(postgresCollections);
|
|
942
|
+
} catch {
|
|
943
|
+
plan = void 0;
|
|
944
|
+
}
|
|
945
|
+
const tablePlans = new Map((plan?.tables ?? []).map((t) => [t.qualified, t]));
|
|
946
|
+
const checkedJunctions = /* @__PURE__ */ new Set();
|
|
803
947
|
for (const collection of postgresCollections) {
|
|
804
948
|
const tableName = getTableName(collection);
|
|
805
949
|
const schemaName = collection.schema || "public";
|
|
@@ -816,11 +960,8 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
|
|
|
816
960
|
}
|
|
817
961
|
const dbColumns = columnsByTable.get(fullTableName) ?? [];
|
|
818
962
|
const dbColumnMap = new Map(dbColumns.map((c) => [c.column_name, c]));
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
"created_on",
|
|
822
|
-
"updated_on"
|
|
823
|
-
]);
|
|
963
|
+
const tablePlan = tablePlans.get(`${schemaName}.${tableName}`);
|
|
964
|
+
if (tablePlan) issues.push(...primaryKeyIssues(tablePlan, pksByTable.get(fullTableName) ?? [], fullTableName));
|
|
824
965
|
for (const [propName, prop] of Object.entries(collection.properties ?? {})) {
|
|
825
966
|
if (prop.type === "relation") {
|
|
826
967
|
const relation = findRelation(resolveCollectionRelations(collection), prop.relation?.relationName ?? propName);
|
|
@@ -855,7 +996,6 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
|
|
|
855
996
|
continue;
|
|
856
997
|
}
|
|
857
998
|
const colName = resolveColumnName(propName, prop);
|
|
858
|
-
if (systemColumns.has(colName)) continue;
|
|
859
999
|
const dbCol = dbColumnMap.get(colName);
|
|
860
1000
|
if (!dbCol) {
|
|
861
1001
|
issues.push({
|
|
@@ -888,29 +1028,32 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
|
|
|
888
1028
|
if (expectedUdt && dbCol.udt_name !== expectedUdt) isMismatch = true;
|
|
889
1029
|
}
|
|
890
1030
|
}
|
|
891
|
-
if (isMismatch)
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1031
|
+
if (isMismatch) {
|
|
1032
|
+
const planned = tablePlan?.columns.find((c) => c.column === colName)?.type;
|
|
1033
|
+
issues.push({
|
|
1034
|
+
severity: "warning",
|
|
1035
|
+
category: "type_mismatch",
|
|
1036
|
+
table: fullTableName,
|
|
1037
|
+
column: colName,
|
|
1038
|
+
expected: prop.type === "vector" ? "vector" : expectedType,
|
|
1039
|
+
actual: dbCol.udt_name === "vector" ? "vector" : actualType,
|
|
1040
|
+
message: `Column "${colName}" in table "${fullTableName}": expected type "${prop.type === "vector" ? "vector" : expectedType}" but found "${dbCol.udt_name === "vector" ? "vector" : actualType}".`,
|
|
1041
|
+
fix: typeMismatchFix(schemaName, tableName, colName, actualType, planned)
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
901
1044
|
}
|
|
902
1045
|
if (prop.type === "string" && prop.enum) {
|
|
903
1046
|
const enumValues = prop.enum;
|
|
904
1047
|
if (enumValues) {
|
|
905
|
-
const enumName = `${tableName}_${colName}
|
|
906
|
-
const dbEnumValues = enumsByName.get(enumName);
|
|
1048
|
+
const enumName = toPostgresIdentifier(`${tableName}_${colName}`);
|
|
1049
|
+
const dbEnumValues = enumsByName.get(`${schemaName}.${enumName}`);
|
|
907
1050
|
if (!dbEnumValues) issues.push({
|
|
908
1051
|
severity: "warning",
|
|
909
1052
|
category: "missing_enum",
|
|
910
1053
|
table: fullTableName,
|
|
911
1054
|
column: colName,
|
|
912
1055
|
expected: enumName,
|
|
913
|
-
message: `Enum type "${enumName}" is defined in collection but not found in the database.`,
|
|
1056
|
+
message: `Enum type "${schemaName}"."${enumName}" is defined in collection but not found in the database.`,
|
|
914
1057
|
fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
|
|
915
1058
|
});
|
|
916
1059
|
else {
|
|
@@ -929,7 +1072,7 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
|
|
|
929
1072
|
expected: expectedValues.join(", "),
|
|
930
1073
|
actual: dbEnumValues.join(", "),
|
|
931
1074
|
message: `Enum values for "${colName}" in table "${fullTableName}" are out of sync (${parts.join("; ")}).`,
|
|
932
|
-
fix: "Run `rebase db push` to
|
|
1075
|
+
fix: extra.length > 0 && missing.length === 0 ? `PostgreSQL cannot drop an enum label, so \`rebase db push\` leaves ${extra.join(", ")} in place. Harmless unless something still writes it; to retire it, UPDATE its rows to another value, then rewrite the type in a migration (\`rebase db generate\`: CREATE TYPE …, ALTER TABLE … TYPE … USING, DROP TYPE) and \`rebase db migrate\`` : "Run `rebase db push` to add the missing labels" + (extra.length > 0 ? `; it cannot remove ${extra.join(", ")} (PostgreSQL has no ALTER TYPE … DROP VALUE) — retire those with a migration that rewrites the type` : "")
|
|
933
1076
|
});
|
|
934
1077
|
}
|
|
935
1078
|
}
|
|
@@ -948,16 +1091,63 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
|
|
|
948
1091
|
message: `Junction table "${fullJunctionTable}" for many-to-many relation "${relation.relationName}" is missing.`,
|
|
949
1092
|
fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
|
|
950
1093
|
});
|
|
1094
|
+
else if (!checkedJunctions.has(fullJunctionTable)) {
|
|
1095
|
+
checkedJunctions.add(fullJunctionTable);
|
|
1096
|
+
const junctionPlan = tablePlans.get(`${junctionSchema}.${junctionTable}`);
|
|
1097
|
+
if (junctionPlan) issues.push(...junctionKeyIssues(junctionPlan, columnsByTable.get(fullJunctionTable) ?? [], pksByTable.get(fullJunctionTable) ?? [], fullJunctionTable));
|
|
1098
|
+
}
|
|
951
1099
|
}
|
|
952
1100
|
}
|
|
1101
|
+
if (plan) issues.push(...orphanedJunctionIssues(plan, columnsByTable, pksByTable, fksByTable));
|
|
953
1102
|
} finally {
|
|
954
1103
|
await pool.end();
|
|
955
1104
|
}
|
|
956
1105
|
return {
|
|
957
|
-
passed: issues.
|
|
1106
|
+
passed: !issues.some((issue) => issue.severity !== "info"),
|
|
958
1107
|
issues
|
|
959
1108
|
};
|
|
960
1109
|
}
|
|
1110
|
+
/**
|
|
1111
|
+
* A `public` table in the junction shape that no collection declares.
|
|
1112
|
+
*
|
|
1113
|
+
* The shape is the planner's: exactly two columns, together the primary key,
|
|
1114
|
+
* each a single-column foreign key to a collection's table. Renaming or
|
|
1115
|
+
* removing a many-to-many leaves such a table behind — push and boot only
|
|
1116
|
+
* create — with every link row still in it and nothing reading it.
|
|
1117
|
+
*
|
|
1118
|
+
* Info, never more: the table may be data somebody still wants. The remedy is
|
|
1119
|
+
* shown, never run.
|
|
1120
|
+
*/
|
|
1121
|
+
function orphanedJunctionIssues(plan, columnsByTable, pksByTable, fksByTable) {
|
|
1122
|
+
const declared = new Set(plan.tables.map((table) => table.qualified));
|
|
1123
|
+
const collectionTables = new Set(plan.tables.filter((table) => table.kind === "collection").map((table) => table.qualified));
|
|
1124
|
+
const issues = [];
|
|
1125
|
+
for (const [table, columns] of columnsByTable) {
|
|
1126
|
+
if (table.includes(".") || declared.has(`public.${table}`)) continue;
|
|
1127
|
+
if (columns.length !== 2 || (pksByTable.get(table) ?? []).length !== 2) continue;
|
|
1128
|
+
const byConstraint = /* @__PURE__ */ new Map();
|
|
1129
|
+
for (const fk of fksByTable.get(table) ?? []) {
|
|
1130
|
+
const target = `${fk.foreign_table_schema}.${fk.foreign_table_name}`;
|
|
1131
|
+
byConstraint.set(fk.constraint_name, [...byConstraint.get(fk.constraint_name) ?? [], {
|
|
1132
|
+
column: fk.column_name,
|
|
1133
|
+
target
|
|
1134
|
+
}]);
|
|
1135
|
+
}
|
|
1136
|
+
const links = [...byConstraint.values()];
|
|
1137
|
+
if (links.length !== 2 || links.some((link) => link.length !== 1)) continue;
|
|
1138
|
+
if (new Set(links.map(([link]) => link.column)).size !== 2) continue;
|
|
1139
|
+
if (!links.every(([link]) => collectionTables.has(link.target))) continue;
|
|
1140
|
+
const targets = links.map(([link]) => link.target.replace(/^public\./, ""));
|
|
1141
|
+
issues.push({
|
|
1142
|
+
severity: "info",
|
|
1143
|
+
category: "orphaned_junction",
|
|
1144
|
+
table,
|
|
1145
|
+
message: `Table "${table}" links "${targets[0]}" and "${targets[1]}" like a many-to-many junction, but no collection declares it — left behind by a renamed or removed relation?`,
|
|
1146
|
+
fix: `DROP TABLE "public"."${table}";`
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
return issues;
|
|
1150
|
+
}
|
|
961
1151
|
function renderReport(report) {
|
|
962
1152
|
out();
|
|
963
1153
|
out(chalk.bold(" 🩺 Rebase Schema Doctor"));
|
|
@@ -1032,10 +1222,12 @@ function formatCategory(cat) {
|
|
|
1032
1222
|
missing_column: "Missing Column",
|
|
1033
1223
|
type_mismatch: "Type Mismatch",
|
|
1034
1224
|
missing_constraint: "Missing Constraint",
|
|
1225
|
+
primary_key_mismatch: "Primary Key Mismatch",
|
|
1035
1226
|
schema_stale: "Stale Schema",
|
|
1036
1227
|
missing_enum: "Missing Enum",
|
|
1037
1228
|
enum_value_mismatch: "Enum Value Mismatch",
|
|
1038
1229
|
missing_foreign_key: "Missing Foreign Key",
|
|
1230
|
+
orphaned_junction: "Orphaned Junction Table",
|
|
1039
1231
|
sdk_stale: "Stale SDK Types",
|
|
1040
1232
|
sdk_not_generated: "SDK Types Not Generated",
|
|
1041
1233
|
sdk_ungeneratable: "SDK Cannot Be Generated"
|
|
@@ -1105,4 +1297,4 @@ async function runDoctor(options) {
|
|
|
1105
1297
|
//#endregion
|
|
1106
1298
|
export { loadCollections as n, runDoctor as r, doctor_exports as t };
|
|
1107
1299
|
|
|
1108
|
-
//# sourceMappingURL=doctor-
|
|
1300
|
+
//# sourceMappingURL=doctor-CUAAOFZm.js.map
|