@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,317 +1,10 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* Introspection reads table and schema names out of `pg_class` and puts them
|
|
9
|
-
* straight back into SQL, and wrapping a name in double quotes is not the same
|
|
10
|
-
* as escaping it: a table named `x" ; DROP TABLE users; --` closes the quote
|
|
11
|
-
* and then the statement. Postgres's own escape for a quote inside a quoted
|
|
12
|
-
* identifier is to double it, which is what this does.
|
|
13
|
-
*
|
|
14
|
-
* "The catalogue is trusted" is true of your own database, and is exactly the
|
|
15
|
-
* assumption this tool cannot make. Introspection exists to be pointed at
|
|
16
|
-
* databases somebody else built, and it runs with the privileges of whoever
|
|
17
|
-
* ran it — which is usually a superuser, because that is who has a connection
|
|
18
|
-
* string to a database they are adopting.
|
|
19
|
-
*/
|
|
20
|
-
function quoteIdentifier(name) {
|
|
21
|
-
return `"${name.replace(/"/g, "\"\"")}"`;
|
|
22
|
-
}
|
|
23
|
-
/** `"schema"."table"`, both halves quoted. */
|
|
24
|
-
function quoteRelation(schema, table) {
|
|
25
|
-
return `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Can this table's name be the name of the file describing it?
|
|
29
|
-
*
|
|
30
|
-
* Introspection writes `<collectionsDir>/<tableName>.ts`, and the table name
|
|
31
|
-
* came from a database this tool did not create. Postgres will hold a table
|
|
32
|
-
* called `../../../etc/cron.d/x`, and `path.join` resolves it — so without this
|
|
33
|
-
* the generator writes a file wherever the name points, as whoever ran the
|
|
34
|
-
* command.
|
|
35
|
-
*
|
|
36
|
-
* Deliberately narrow, and deliberately not a blocklist of `..` and `/`: the
|
|
37
|
-
* question is "is this a plain file name?", and every answer that is not
|
|
38
|
-
* obviously yes is a table better written by hand. NUL and the Windows
|
|
39
|
-
* separator are in scope because a collections directory is checked out on
|
|
40
|
-
* whatever machine the developer has.
|
|
41
|
-
*/
|
|
42
|
-
function isUsableFileName(name) {
|
|
43
|
-
if (name.length === 0 || name.length > 200) return false;
|
|
44
|
-
if (name === "." || name === "..") return false;
|
|
45
|
-
if (/[/\\:\u0000-\u001f]/.test(name)) return false;
|
|
46
|
-
return !name.startsWith(".") && !name.endsWith(".");
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Base tables, excluding partitions.
|
|
50
|
-
*
|
|
51
|
-
* `relispartition` is the reason this reads `pg_class` rather than
|
|
52
|
-
* `information_schema.tables`, which reports every partition as a base table of
|
|
53
|
-
* its own. Pagila partitions `payment` by month; introspected through
|
|
54
|
-
* information_schema it yields `payment` plus 26 near-identical
|
|
55
|
-
* `payment_p2022_*` collections, each with its own nav entry.
|
|
56
|
-
*
|
|
57
|
-
* `relkind = 'p'` (the partitioned parent) is included and `'r'` partitions are
|
|
58
|
-
* dropped, which is the right way round: the parent is the queryable table.
|
|
59
|
-
*/
|
|
60
|
-
var TABLES_QUERY = `
|
|
61
|
-
SELECT c.relname AS table_name,
|
|
62
|
-
c.relkind = 'p' AS is_partitioned
|
|
63
|
-
FROM pg_class c
|
|
64
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
65
|
-
WHERE n.nspname = $1
|
|
66
|
-
AND c.relkind IN ('r', 'p')
|
|
67
|
-
AND NOT c.relispartition
|
|
68
|
-
AND c.relname NOT LIKE 'drizzle_%'
|
|
69
|
-
AND c.relname NOT LIKE 'rebase_%'
|
|
70
|
-
ORDER BY c.relname
|
|
71
|
-
`;
|
|
72
|
-
/**
|
|
73
|
-
* Columns, with the catalog facts that say a column is not the user's to edit.
|
|
74
|
-
*
|
|
75
|
-
* `is_generated`/`is_identity` are read for that reason: a generated column
|
|
76
|
-
* rejects any write, so a form that offers it is offering a field that always
|
|
77
|
-
* errors. `character_maximum_length` is the declared `varchar(n)` bound —
|
|
78
|
-
* already enforced by the database, and free to surface as validation.
|
|
79
|
-
*/
|
|
80
|
-
var COLUMNS_QUERY = `
|
|
81
|
-
SELECT
|
|
82
|
-
c.table_name,
|
|
83
|
-
c.column_name,
|
|
84
|
-
c.data_type,
|
|
85
|
-
c.udt_name,
|
|
86
|
-
c.is_nullable,
|
|
87
|
-
c.column_default,
|
|
88
|
-
c.ordinal_position,
|
|
89
|
-
c.is_generated,
|
|
90
|
-
c.is_identity,
|
|
91
|
-
c.identity_generation,
|
|
92
|
-
c.character_maximum_length,
|
|
93
|
-
c.numeric_precision,
|
|
94
|
-
c.numeric_scale,
|
|
95
|
-
(SELECT a.atttypmod FROM pg_attribute a
|
|
96
|
-
JOIN pg_class pc ON a.attrelid = pc.oid
|
|
97
|
-
WHERE pc.relname = c.table_name
|
|
98
|
-
AND a.attname = c.column_name
|
|
99
|
-
AND pc.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = c.table_schema)) as atttypmod
|
|
100
|
-
FROM information_schema.columns c
|
|
101
|
-
WHERE c.table_schema = $1
|
|
102
|
-
ORDER BY c.table_name, c.ordinal_position
|
|
103
|
-
`;
|
|
104
|
-
var ENUMS_QUERY = `
|
|
105
|
-
SELECT t.typname AS enum_name,
|
|
106
|
-
e.enumlabel AS enum_value,
|
|
107
|
-
e.enumsortorder AS sort_order
|
|
108
|
-
FROM pg_type t
|
|
109
|
-
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
110
|
-
JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
111
|
-
WHERE n.nspname = $1
|
|
112
|
-
ORDER BY t.typname, e.enumsortorder
|
|
113
|
-
`;
|
|
114
|
-
/**
|
|
115
|
-
* Primary key columns, in key order.
|
|
116
|
-
*
|
|
117
|
-
* `ORDER BY k.ord` matters for composite keys: the position of a column inside
|
|
118
|
-
* the key is what tells a two-column PK made of two foreign keys apart from an
|
|
119
|
-
* ordinary composite key, and `= ANY(i.indkey)` (the shape this replaced)
|
|
120
|
-
* returns catalog order instead.
|
|
121
|
-
*/
|
|
122
|
-
var PRIMARY_KEYS_QUERY = `
|
|
123
|
-
SELECT t.relname AS table_name,
|
|
124
|
-
a.attname AS column_name
|
|
125
|
-
FROM pg_index i
|
|
126
|
-
JOIN pg_class t ON t.oid = i.indrelid
|
|
127
|
-
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
128
|
-
CROSS JOIN LATERAL unnest(i.indkey) WITH ORDINALITY AS k(attnum, ord)
|
|
129
|
-
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = k.attnum
|
|
130
|
-
WHERE i.indisprimary AND n.nspname = $1
|
|
131
|
-
ORDER BY t.relname, k.ord
|
|
132
|
-
`;
|
|
133
|
-
/**
|
|
134
|
-
* Foreign keys, one row per referencing column, with the delete rule.
|
|
135
|
-
*
|
|
136
|
-
* Read from `pg_constraint` rather than through
|
|
137
|
-
* `information_schema.constraint_column_usage`, which does not preserve the
|
|
138
|
-
* pairing on a composite foreign key: it reports the cross product of the
|
|
139
|
-
* constraint's columns, so a two-column FK comes back as four rows, two of them
|
|
140
|
-
* pairing the wrong columns. `unnest(...) WITH ORDINALITY` on both sides joined
|
|
141
|
-
* on the ordinal keeps each referencing column with the column it references.
|
|
142
|
-
*
|
|
143
|
-
* `confdeltype` is carried because `ON DELETE CASCADE` is the schema author
|
|
144
|
-
* saying, in the database, that the child cannot outlive the parent — the one
|
|
145
|
-
* unambiguous declaration of ownership a schema contains.
|
|
146
|
-
*
|
|
147
|
-
* Constraints declared on a *partition* are attributed to the partition root,
|
|
148
|
-
* because that is the table this run generates a collection for. Pagila declares
|
|
149
|
-
* `payment`'s three foreign keys on each monthly partition and none on the
|
|
150
|
-
* parent, so reading `pg_constraint` at face value gives the `payment`
|
|
151
|
-
* collection no relations at all. `pick = 1` then keeps one row per logical key,
|
|
152
|
-
* preferring the root's own constraint when it has one, so a schema that
|
|
153
|
-
* declares the key on the parent *and* inherits it onto 26 partitions does not
|
|
154
|
-
* yield 27 copies of the same relation.
|
|
155
|
-
*/
|
|
156
|
-
var FOREIGN_KEYS_QUERY = `
|
|
157
|
-
WITH fk AS (
|
|
158
|
-
SELECT con.oid,
|
|
159
|
-
con.conname,
|
|
160
|
-
con.conrelid,
|
|
161
|
-
con.confrelid,
|
|
162
|
-
con.conkey,
|
|
163
|
-
con.confkey,
|
|
164
|
-
con.confdeltype,
|
|
165
|
-
COALESCE(pg_partition_root(con.conrelid), con.conrelid) AS root_oid,
|
|
166
|
-
row_number() OVER (
|
|
167
|
-
PARTITION BY COALESCE(pg_partition_root(con.conrelid), con.conrelid),
|
|
168
|
-
con.confrelid, con.conkey, con.confkey
|
|
169
|
-
ORDER BY (con.conrelid = COALESCE(pg_partition_root(con.conrelid), con.conrelid)) DESC,
|
|
170
|
-
con.oid
|
|
171
|
-
) AS pick
|
|
172
|
-
FROM pg_constraint con
|
|
173
|
-
JOIN pg_namespace n ON n.oid = con.connamespace
|
|
174
|
-
WHERE con.contype = 'f' AND n.nspname = $1
|
|
175
|
-
)
|
|
176
|
-
SELECT root.relname AS table_name,
|
|
177
|
-
sa.attname AS column_name,
|
|
178
|
-
tgt.relname AS foreign_table_name,
|
|
179
|
-
ta.attname AS foreign_column_name,
|
|
180
|
-
fk.conname AS constraint_name,
|
|
181
|
-
s.ord::int AS ordinal,
|
|
182
|
-
CASE fk.confdeltype
|
|
183
|
-
WHEN 'a' THEN 'NO ACTION'
|
|
184
|
-
WHEN 'r' THEN 'RESTRICT'
|
|
185
|
-
WHEN 'c' THEN 'CASCADE'
|
|
186
|
-
WHEN 'n' THEN 'SET NULL'
|
|
187
|
-
WHEN 'd' THEN 'SET DEFAULT'
|
|
188
|
-
END AS delete_rule
|
|
189
|
-
FROM fk
|
|
190
|
-
JOIN pg_class root ON root.oid = fk.root_oid
|
|
191
|
-
JOIN pg_class tgt ON tgt.oid = fk.confrelid
|
|
192
|
-
CROSS JOIN LATERAL unnest(fk.conkey) WITH ORDINALITY AS s(attnum, ord)
|
|
193
|
-
JOIN pg_attribute sa ON sa.attrelid = fk.conrelid AND sa.attnum = s.attnum
|
|
194
|
-
CROSS JOIN LATERAL unnest(fk.confkey) WITH ORDINALITY AS f(attnum, ord)
|
|
195
|
-
JOIN pg_attribute ta ON ta.attrelid = fk.confrelid AND ta.attnum = f.attnum
|
|
196
|
-
WHERE fk.pick = 1 AND s.ord = f.ord
|
|
197
|
-
ORDER BY root.relname, fk.conname, s.ord
|
|
198
|
-
`;
|
|
199
|
-
/**
|
|
200
|
-
* Unique constraints and unique indexes, as ordered column lists.
|
|
201
|
-
*
|
|
202
|
-
* Indexes rather than constraints alone, because `CREATE UNIQUE INDEX` and
|
|
203
|
-
* `ADD CONSTRAINT ... UNIQUE` produce the same guarantee and schemas use both.
|
|
204
|
-
* Partial indexes (`indpred IS NOT NULL`) are excluded: they promise uniqueness
|
|
205
|
-
* only over the rows matching their predicate, which is not the promise a
|
|
206
|
-
* `validation: { unique: true }` field makes.
|
|
207
|
-
*/
|
|
208
|
-
var UNIQUE_CONSTRAINTS_QUERY = `
|
|
209
|
-
SELECT t.relname AS table_name,
|
|
210
|
-
ic.relname AS constraint_name,
|
|
211
|
-
-- ::text, because the driver has no parser for an array of \`name\`
|
|
212
|
-
-- and hands back the raw \`{a,b}\` literal as a string. Every consumer
|
|
213
|
-
-- here indexes and compares it as an array, and a string of that
|
|
214
|
-
-- shape fails those silently rather than loudly.
|
|
215
|
-
array_agg(a.attname::text ORDER BY k.ord) AS column_names
|
|
216
|
-
FROM pg_index i
|
|
217
|
-
JOIN pg_class t ON t.oid = i.indrelid
|
|
218
|
-
JOIN pg_class ic ON ic.oid = i.indexrelid
|
|
219
|
-
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
220
|
-
CROSS JOIN LATERAL unnest(i.indkey) WITH ORDINALITY AS k(attnum, ord)
|
|
221
|
-
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = k.attnum
|
|
222
|
-
WHERE i.indisunique
|
|
223
|
-
AND NOT i.indisprimary
|
|
224
|
-
AND i.indpred IS NULL
|
|
225
|
-
AND n.nspname = $1
|
|
226
|
-
AND NOT t.relispartition
|
|
227
|
-
GROUP BY t.relname, ic.relname
|
|
228
|
-
ORDER BY t.relname, ic.relname
|
|
229
|
-
`;
|
|
230
|
-
/**
|
|
231
|
-
* CHECK constraints, as the source text Postgres reproduces them from.
|
|
232
|
-
*
|
|
233
|
-
* `pg_get_constraintdef` output is normalized by the server — the parser in
|
|
234
|
-
* {@link ./introspect-db-constraints.ts} reads that normalized form, not
|
|
235
|
-
* whatever the author typed.
|
|
236
|
-
*/
|
|
237
|
-
var CHECK_CONSTRAINTS_QUERY = `
|
|
238
|
-
SELECT DISTINCT
|
|
239
|
-
root.relname AS table_name,
|
|
240
|
-
con.conname AS constraint_name,
|
|
241
|
-
pg_get_constraintdef(con.oid) AS definition
|
|
242
|
-
FROM pg_constraint con
|
|
243
|
-
JOIN pg_namespace n ON n.oid = con.connamespace
|
|
244
|
-
JOIN pg_class root ON root.oid = COALESCE(pg_partition_root(con.conrelid), con.conrelid)
|
|
245
|
-
WHERE con.contype = 'c'
|
|
246
|
-
AND n.nspname = $1
|
|
247
|
-
ORDER BY root.relname, con.conname
|
|
248
|
-
`;
|
|
249
|
-
/**
|
|
250
|
-
* `COMMENT ON TABLE` / `COMMENT ON COLUMN`, which is documentation the author
|
|
251
|
-
* already wrote and which nothing downstream has ever read.
|
|
252
|
-
*/
|
|
253
|
-
var COMMENTS_QUERY = `
|
|
254
|
-
SELECT t.relname AS table_name,
|
|
255
|
-
a.attname AS column_name,
|
|
256
|
-
d.description AS comment
|
|
257
|
-
FROM pg_description d
|
|
258
|
-
JOIN pg_class t ON t.oid = d.objoid
|
|
259
|
-
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
260
|
-
LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = d.objsubid
|
|
261
|
-
WHERE n.nspname = $1
|
|
262
|
-
AND t.relkind IN ('r', 'p')
|
|
263
|
-
AND (d.objsubid = 0 OR a.attname IS NOT NULL)
|
|
264
|
-
ORDER BY t.relname, d.objsubid
|
|
265
|
-
`;
|
|
266
|
-
/**
|
|
267
|
-
* Counts a table's rows, stopping once the answer can only be "more than
|
|
268
|
-
* `limit`".
|
|
269
|
-
*
|
|
270
|
-
* Introspection asks this to tell a small reference table from a table that
|
|
271
|
-
* merely looks like one (see `classifyTables`). `reltuples` would answer for
|
|
272
|
-
* free but lies on a table that has never been analyzed — which is every table
|
|
273
|
-
* in a database restored from a dump, i.e. exactly the case introspection meets
|
|
274
|
-
* — and reports -1 there rather than an error, so the lie is silent.
|
|
275
|
-
*
|
|
276
|
-
* The subquery's LIMIT caps the work at `limit + 1` rows however large the
|
|
277
|
-
* table is, so this stays cheap on a table with a billion rows.
|
|
278
|
-
*/
|
|
279
|
-
async function countRowsUpTo(client, schema, table, limit) {
|
|
280
|
-
const { rows } = await client.query(`SELECT count(*)::text AS count FROM (SELECT 1 FROM ${quoteRelation(schema, table)} LIMIT ${limit + 1}) probe`);
|
|
281
|
-
return Number(rows[0]?.count ?? 0);
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* Reads everything the generator needs, in one pass.
|
|
285
|
-
*
|
|
286
|
-
* `rowCounts` is deliberately absent here: it costs a query per table and only
|
|
287
|
-
* a handful of tables can possibly need it. The caller fills it in for the
|
|
288
|
-
* candidates `lookupCandidates()` names.
|
|
289
|
-
*/
|
|
290
|
-
async function readSchemaMetadata(client, schema) {
|
|
291
|
-
const tables = await client.query(TABLES_QUERY, [schema]);
|
|
292
|
-
const columns = await client.query(COLUMNS_QUERY, [schema]);
|
|
293
|
-
const enumValues = await client.query(ENUMS_QUERY, [schema]);
|
|
294
|
-
const pks = await client.query(PRIMARY_KEYS_QUERY, [schema]);
|
|
295
|
-
const fks = await client.query(FOREIGN_KEYS_QUERY, [schema]);
|
|
296
|
-
const uniques = await client.query(UNIQUE_CONSTRAINTS_QUERY, [schema]);
|
|
297
|
-
const checks = await client.query(CHECK_CONSTRAINTS_QUERY, [schema]);
|
|
298
|
-
const comments = await client.query(COMMENTS_QUERY, [schema]);
|
|
299
|
-
const known = new Set(tables.rows.map((t) => t.table_name));
|
|
300
|
-
const scoped = (rows) => rows.filter((r) => known.has(r.table_name));
|
|
301
|
-
return {
|
|
302
|
-
schema,
|
|
303
|
-
tables: tables.rows,
|
|
304
|
-
columns: scoped(columns.rows),
|
|
305
|
-
enumValues: enumValues.rows,
|
|
306
|
-
pks: scoped(pks.rows),
|
|
307
|
-
fks: scoped(fks.rows).filter((fk) => known.has(fk.foreign_table_name)),
|
|
308
|
-
uniques: scoped(uniques.rows),
|
|
309
|
-
checks: scoped(checks.rows),
|
|
310
|
-
comments: scoped(comments.rows),
|
|
311
|
-
rowCounts: {}
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
//#endregion
|
|
3
|
+
import { a as buildSearchColumnSpec, u as searchColumnStamps } from "./search-column-BM-GV6vH.js";
|
|
4
|
+
import { p as foreignKeyIndexSpec } from "./source-DB6xJv3p.js";
|
|
5
|
+
import { DEFAULT_SEARCH_COLUMN, DEFAULT_SEARCH_LANGUAGE } from "@rebasepro/types";
|
|
6
|
+
import { getGeneratedPolicyNames, policyToSecurityRule } from "@rebasepro/common";
|
|
7
|
+
import { firstFreeKey, toPostgresIdentifier, toWireKey } from "@rebasepro/utils";
|
|
315
8
|
//#region src/schema/introspect-db-naming.ts
|
|
316
9
|
/**
|
|
317
10
|
* Naming helpers shared by the introspection modules. These live apart from
|
|
@@ -495,6 +188,359 @@ function mapPgType(dataType) {
|
|
|
495
188
|
if (dt === "uuid") return "string";
|
|
496
189
|
return "string";
|
|
497
190
|
}
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region src/schema/introspect-db-storage.ts
|
|
193
|
+
var literal = (value) => JSON.stringify(value);
|
|
194
|
+
/** `character varying` → `varchar`, the spellings the planner's `columnType` uses. */
|
|
195
|
+
var NUMBER_COLUMN_TYPE = {
|
|
196
|
+
"integer": "integer",
|
|
197
|
+
"bigint": "bigint",
|
|
198
|
+
"real": "real",
|
|
199
|
+
"double precision": "double precision"
|
|
200
|
+
};
|
|
201
|
+
/** A sequence default — `nextval('orders_id_seq'::regclass)`. */
|
|
202
|
+
var SEQUENCE_DEFAULT = /^nextval\('([^']+)'::regclass\)$/;
|
|
203
|
+
/**
|
|
204
|
+
* Whether a `nextval` default names the sequence `SERIAL` would have made for
|
|
205
|
+
* this column — the only one the planner can declare, because it declares the
|
|
206
|
+
* column as `SERIAL` and lets Postgres name the sequence.
|
|
207
|
+
*/
|
|
208
|
+
var isConventionalSequence = (col, sequence) => {
|
|
209
|
+
return sequence.replace(/^.*\./, "").replace(/^"|"$/g, "") === serialSequenceName(col.table_name, col.column_name);
|
|
210
|
+
};
|
|
211
|
+
/**
|
|
212
|
+
* The name Postgres gives a `SERIAL` column's sequence — `ChooseRelationName`
|
|
213
|
+
* with the `seq` label: `<table>_<column>_seq`, and when that is over 63 bytes
|
|
214
|
+
* the longer of the two names is shortened, one character at a time, until it
|
|
215
|
+
* fits.
|
|
216
|
+
*/
|
|
217
|
+
function serialSequenceName(table, column) {
|
|
218
|
+
const available = 58;
|
|
219
|
+
let tableChars = table.length;
|
|
220
|
+
let columnChars = column.length;
|
|
221
|
+
while (tableChars + columnChars > available) if (tableChars > columnChars) tableChars--;
|
|
222
|
+
else columnChars--;
|
|
223
|
+
return `${table.slice(0, tableChars)}_${column.slice(0, columnChars)}_seq`;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* The `isId` (and `columnType`) a single-column primary key needs.
|
|
227
|
+
*
|
|
228
|
+
* `isId: "increment"` is an identity column, and only an INTEGER one. A serial
|
|
229
|
+
* key — an integer with a `nextval` default — is `SERIAL`/`BIGSERIAL`, declared
|
|
230
|
+
* through `columnType` beside `isId: "manual"`. A uuid key is `"uuid"` only when
|
|
231
|
+
* the database generates it; a key with no default is `"manual"`, which keeps
|
|
232
|
+
* the planner from inventing one.
|
|
233
|
+
*/
|
|
234
|
+
function primaryKeyStorage(col, propType) {
|
|
235
|
+
const dataType = col.data_type.toLowerCase();
|
|
236
|
+
const fallback = col.column_default?.trim();
|
|
237
|
+
if (propType === "number") {
|
|
238
|
+
if (col.is_identity === "YES") {
|
|
239
|
+
if (dataType === "integer") return { keys: [`isId: ${literal("increment")}`] };
|
|
240
|
+
return {
|
|
241
|
+
keys: [`isId: ${literal("increment")}`],
|
|
242
|
+
note: `"${col.column_name}" is a ${dataType} identity key; \`isId: "increment"\` is always INTEGER, so a push would plan a narrowing type change (and ask before applying it).`
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
const sequence = fallback ? SEQUENCE_DEFAULT.exec(fallback) : null;
|
|
246
|
+
if (sequence) {
|
|
247
|
+
const serial = dataType === "bigint" ? "bigserial" : dataType === "integer" ? "serial" : void 0;
|
|
248
|
+
if (serial && isConventionalSequence(col, sequence[1])) return { keys: [`isId: ${literal("manual")}`, `columnType: ${literal(serial)}`] };
|
|
249
|
+
return {
|
|
250
|
+
keys: [`isId: ${literal("manual")}`, ...numberColumnType(dataType)],
|
|
251
|
+
note: `"${col.column_name}" takes its value from the sequence ${sequence[1]}, which no property can declare; a push would plan dropping that default.`
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
if (fallback) return { keys: [`isId: ${literal(`sql\`${fallback}\``)}`, ...numberColumnType(dataType)] };
|
|
255
|
+
return { keys: [`isId: ${literal("manual")}`, ...numberColumnType(dataType)] };
|
|
256
|
+
}
|
|
257
|
+
if (dataType === "uuid") {
|
|
258
|
+
if (fallback === "gen_random_uuid()") return { keys: [`isId: ${literal("uuid")}`] };
|
|
259
|
+
if (fallback) return { keys: [`isId: ${literal(`sql\`${fallback}\``)}`, `columnType: ${literal("uuid")}`] };
|
|
260
|
+
return { keys: [`isId: ${literal("manual")}`, `columnType: ${literal("uuid")}`] };
|
|
261
|
+
}
|
|
262
|
+
const strategy = fallback ? `sql\`${fallback}\`` : "manual";
|
|
263
|
+
const stored = stringStorage(col);
|
|
264
|
+
return {
|
|
265
|
+
keys: [`isId: ${literal(strategy)}`, ...stored.keys],
|
|
266
|
+
note: stored.note
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/** `columnType` for a number column the planner would otherwise make NUMERIC. */
|
|
270
|
+
function numberColumnType(dataType) {
|
|
271
|
+
const mapped = NUMBER_COLUMN_TYPE[dataType];
|
|
272
|
+
return mapped ? [`columnType: ${literal(mapped)}`] : [];
|
|
273
|
+
}
|
|
274
|
+
/** The keys a `string` column needs; the bound itself is emitted as `validation.max`. */
|
|
275
|
+
function stringStorage(col) {
|
|
276
|
+
switch (col.data_type.toLowerCase()) {
|
|
277
|
+
case "text": return { keys: [] };
|
|
278
|
+
case "character varying":
|
|
279
|
+
if (typeof col.character_maximum_length === "number" && col.character_maximum_length > 0) return { keys: [`columnType: ${literal("varchar")}`] };
|
|
280
|
+
return {
|
|
281
|
+
keys: [],
|
|
282
|
+
note: `"${col.column_name}" is a varchar with no length, declared as text: a push would plan converting it to text, which keeps every value.`
|
|
283
|
+
};
|
|
284
|
+
case "character": return { keys: [`columnType: ${literal("char")}`] };
|
|
285
|
+
case "uuid": return { keys: [`columnType: ${literal("uuid")}`] };
|
|
286
|
+
default: return {
|
|
287
|
+
keys: [],
|
|
288
|
+
note: `"${col.column_name}" is ${col.data_type === "USER-DEFINED" ? col.udt_name : col.data_type}, which no property type stores; it is read as text, and a push would plan converting the column to text.`
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* The keys a non-key column needs, for the property type introspection chose.
|
|
294
|
+
*
|
|
295
|
+
* `enumTypeName` is the type the planner would name for this column
|
|
296
|
+
* (`<table>_<column>`, cut to 63 bytes); an enum column whose type is called
|
|
297
|
+
* anything else cannot be declared, and the note says how to rename it.
|
|
298
|
+
*/
|
|
299
|
+
function columnStorage(col, propType, context) {
|
|
300
|
+
const dataType = col.data_type.toLowerCase();
|
|
301
|
+
if (context.isEnumColumn) {
|
|
302
|
+
if (col.udt_name === context.enumTypeName) return { keys: [] };
|
|
303
|
+
return {
|
|
304
|
+
keys: [],
|
|
305
|
+
note: context.enumTypeShared ? `"${col.column_name}" uses the enum type "${col.udt_name}", which other columns share; Rebase gives each column its own type (<table>_<column>), so a push would plan "${context.enumTypeName}" and a conversion Postgres cannot cast.` : `"${col.column_name}" uses the enum type "${col.udt_name}", and Rebase names a column's enum type <table>_<column>. Rename it before pushing: ALTER TYPE "${col.udt_name}" RENAME TO "${context.enumTypeName}";`
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
switch (propType) {
|
|
309
|
+
case "number": {
|
|
310
|
+
if (dataType === "numeric") {
|
|
311
|
+
const keys = [];
|
|
312
|
+
if (typeof col.numeric_precision === "number") keys.push(`precision: ${col.numeric_precision}`);
|
|
313
|
+
if (typeof col.numeric_precision === "number" && typeof col.numeric_scale === "number") keys.push(`scale: ${col.numeric_scale}`);
|
|
314
|
+
return { keys };
|
|
315
|
+
}
|
|
316
|
+
if (dataType === "smallint") return {
|
|
317
|
+
keys: [`columnType: ${literal("integer")}`],
|
|
318
|
+
note: `"${col.column_name}" is smallint, which no \`columnType\` names; it is declared integer, so a push widens it (every value survives).`
|
|
319
|
+
};
|
|
320
|
+
const mapped = numberColumnType(dataType);
|
|
321
|
+
if (mapped.length > 0) return { keys: mapped };
|
|
322
|
+
return {
|
|
323
|
+
keys: [],
|
|
324
|
+
note: `"${col.column_name}" is ${col.data_type}, which no \`columnType\` names; it is declared numeric, and a push would plan converting it.`
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
case "string": return stringStorage(col);
|
|
328
|
+
case "date": switch (dataType) {
|
|
329
|
+
case "timestamp with time zone": return { keys: [] };
|
|
330
|
+
case "date": return { keys: [`columnType: ${literal("date")}`] };
|
|
331
|
+
case "time without time zone": return { keys: [`columnType: ${literal("time")}`] };
|
|
332
|
+
default: return {
|
|
333
|
+
keys: [],
|
|
334
|
+
note: `"${col.column_name}" is ${col.data_type}; a date property stores timestamptz, date or time, so a push would plan converting it to timestamptz (and ask before applying it).`
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
case "map": return dataType === "json" ? { keys: [`columnType: ${literal("json")}`] } : { keys: [] };
|
|
338
|
+
default: return { keys: [] };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* The `columnType` an array column needs, or a note when its element type is
|
|
343
|
+
* not one the planner stores natively.
|
|
344
|
+
*
|
|
345
|
+
* Only `text[]`, `integer[]`, `boolean[]` and `numeric[]` are. Reading
|
|
346
|
+
* `bigint[]` as `integer[]` — what this used to do — planned a narrowing.
|
|
347
|
+
*/
|
|
348
|
+
function arrayStorage(col) {
|
|
349
|
+
switch (col.udt_name) {
|
|
350
|
+
case "_text": return {
|
|
351
|
+
columnType: "text[]",
|
|
352
|
+
innerType: "string"
|
|
353
|
+
};
|
|
354
|
+
case "_int4": return {
|
|
355
|
+
columnType: "integer[]",
|
|
356
|
+
innerType: "number"
|
|
357
|
+
};
|
|
358
|
+
case "_bool": return {
|
|
359
|
+
columnType: "boolean[]",
|
|
360
|
+
innerType: "boolean"
|
|
361
|
+
};
|
|
362
|
+
case "_numeric": return {
|
|
363
|
+
columnType: "numeric[]",
|
|
364
|
+
innerType: "number"
|
|
365
|
+
};
|
|
366
|
+
default: {
|
|
367
|
+
const element = col.udt_name.replace(/^_/, "");
|
|
368
|
+
return {
|
|
369
|
+
innerType: "string",
|
|
370
|
+
note: `"${col.column_name}" is an array of ${element}; arrays are stored as text[], integer[], boolean[] or numeric[] (anything else as jsonb), so a push would plan converting it.`
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* A column default as a `defaultValue`, when it is a literal a property can carry.
|
|
377
|
+
*
|
|
378
|
+
* `undefined` when there is no default to carry, or when the default is one the
|
|
379
|
+
* generator expresses another way (`now()` is `autoValue`, a key's default is
|
|
380
|
+
* its `isId`). `{ note }` when there is a default and no property can say it —
|
|
381
|
+
* a push would plan dropping it.
|
|
382
|
+
*/
|
|
383
|
+
function literalDefaultOf(col, propType) {
|
|
384
|
+
const raw = col.column_default?.trim();
|
|
385
|
+
if (!raw) return void 0;
|
|
386
|
+
if (/^(now\(\)|CURRENT_TIMESTAMP)$/i.test(raw)) return void 0;
|
|
387
|
+
const quoted = /^'((?:[^']|'')*)'(?:::[\w\s."[\]]+)?$/.exec(raw);
|
|
388
|
+
const unquoted = quoted ? quoted[1].replace(/''/g, "'") : void 0;
|
|
389
|
+
switch (propType) {
|
|
390
|
+
case "string":
|
|
391
|
+
if (unquoted !== void 0) return { source: literal(unquoted) };
|
|
392
|
+
break;
|
|
393
|
+
case "number": {
|
|
394
|
+
const text = unquoted ?? raw.replace(/^\((.*)\)$/, "$1").replace(/::[\w\s]+$/, "");
|
|
395
|
+
if (/^-?\d+(\.\d+)?$/.test(text) && Number.isFinite(Number(text))) return { source: String(Number(text)) };
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
case "boolean":
|
|
399
|
+
if (/^true$/i.test(raw)) return { source: "true" };
|
|
400
|
+
if (/^false$/i.test(raw)) return { source: "false" };
|
|
401
|
+
break;
|
|
402
|
+
case "map":
|
|
403
|
+
if (unquoted !== void 0) try {
|
|
404
|
+
const parsed = JSON.parse(unquoted);
|
|
405
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) return { source: JSON.stringify(parsed) };
|
|
406
|
+
} catch {}
|
|
407
|
+
break;
|
|
408
|
+
default: break;
|
|
409
|
+
}
|
|
410
|
+
return { note: `"${col.column_name}" defaults to ${raw}, which no \`defaultValue\` can express; a push would plan dropping that default.` };
|
|
411
|
+
}
|
|
412
|
+
/** `ON DELETE` as the relation's `onDelete`, when it is not the default the planner would choose. */
|
|
413
|
+
function onDeleteOf(fk, required) {
|
|
414
|
+
const rule = fk.delete_rule?.toUpperCase();
|
|
415
|
+
if (!rule) return void 0;
|
|
416
|
+
if (rule === (required ? "RESTRICT" : "SET NULL")) return void 0;
|
|
417
|
+
return rule.toLowerCase();
|
|
418
|
+
}
|
|
419
|
+
//#endregion
|
|
420
|
+
//#region src/schema/introspect-db-search.ts
|
|
421
|
+
/**
|
|
422
|
+
* A collection's `search` block, read back out of the column it built.
|
|
423
|
+
*
|
|
424
|
+
* A Rebase search column is `tsvector GENERATED ALWAYS AS (…) STORED`, stamped
|
|
425
|
+
* with a fingerprint of the expression in its `COMMENT`. Introspection used to
|
|
426
|
+
* emit it as an ordinary read-only string property, which the planner makes a
|
|
427
|
+
* `text` column — so the first push after adopting a Rebase database planned
|
|
428
|
+
* `ALTER COLUMN "search_vector" TYPE text … DROP EXPRESSION`, and search was
|
|
429
|
+
* gone for whoever approved it.
|
|
430
|
+
*
|
|
431
|
+
* Reading the block back is a guess checked by the stamp: the deparsed
|
|
432
|
+
* expression names each field's column, weight and language, which is enough
|
|
433
|
+
* to write a candidate block; the candidate is then compiled by the very code
|
|
434
|
+
* that built the column, and kept only if its fingerprint is the one the column
|
|
435
|
+
* carries. A block that does not reproduce the stamp is not emitted at all —
|
|
436
|
+
* the note says so — because a wrong one would rebuild the column differently.
|
|
437
|
+
*
|
|
438
|
+
* `mode` and `fuzzyThreshold` act at query time and are stored nowhere, so they
|
|
439
|
+
* come back as their defaults.
|
|
440
|
+
*/
|
|
441
|
+
/** One `setweight(to_tsvector('<lang>'::regconfig, <text>), '<W>'::"char")`, deparsed. */
|
|
442
|
+
var SEGMENT = /setweight\(to_tsvector\('((?:[^']|'')+)'::regconfig, ([\s\S]+?)\), '([A-D])'::"char"\)/g;
|
|
443
|
+
/** A property with the column's shape, for the candidate block to resolve against. */
|
|
444
|
+
function propertyFor(col) {
|
|
445
|
+
if (col.data_type === "text" || col.data_type === "character varying" || col.data_type === "character") return {
|
|
446
|
+
name: col.column_name,
|
|
447
|
+
type: "string",
|
|
448
|
+
columnName: col.column_name
|
|
449
|
+
};
|
|
450
|
+
if (col.udt_name === "_text") return {
|
|
451
|
+
name: col.column_name,
|
|
452
|
+
type: "array",
|
|
453
|
+
columnName: col.column_name,
|
|
454
|
+
of: {
|
|
455
|
+
name: col.column_name,
|
|
456
|
+
type: "string"
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
if (col.data_type === "jsonb") return {
|
|
460
|
+
name: col.column_name,
|
|
461
|
+
type: "map",
|
|
462
|
+
columnName: col.column_name,
|
|
463
|
+
keyValue: true
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* The search block a table's stamped generated columns were built from, or a
|
|
468
|
+
* note saying why it could not be read back.
|
|
469
|
+
*/
|
|
470
|
+
function reconstructSearchBlock(input) {
|
|
471
|
+
const stamped = input.columns.filter((col) => col.is_generated === "ALWAYS" && (input.comments.get(col.column_name) ?? "").startsWith("rebase:search:v1:"));
|
|
472
|
+
const main = stamped.find((col) => col.udt_name === "tsvector");
|
|
473
|
+
if (!main) return { columns: /* @__PURE__ */ new Set() };
|
|
474
|
+
const fuzzy = stamped.find((col) => col.column_name === `${main.column_name}_text`);
|
|
475
|
+
const columns = new Set(stamped.map((col) => col.column_name));
|
|
476
|
+
const unreadable = (why) => ({
|
|
477
|
+
columns,
|
|
478
|
+
note: `"${main.column_name}" is a Rebase search column whose \`search\` block could not be read back (${why}). Declare the block again before pushing — without it a push plans dropping the column.`
|
|
479
|
+
});
|
|
480
|
+
const expression = main.generation_expression ?? "";
|
|
481
|
+
const byColumn = new Map(input.columns.map((col) => [col.column_name, col]));
|
|
482
|
+
const fields = [];
|
|
483
|
+
let language;
|
|
484
|
+
let unaccent = false;
|
|
485
|
+
for (const [, lang, inner, weight] of expression.matchAll(SEGMENT)) {
|
|
486
|
+
language ??= lang.replace(/''/g, "'");
|
|
487
|
+
if (lang.replace(/''/g, "'") !== language) return unreadable("its fields use different languages");
|
|
488
|
+
if (inner.includes("rebase_search_unaccent")) unaccent = true;
|
|
489
|
+
const column = [...inner.matchAll(/"?([A-Za-z_][A-Za-z0-9_$]*)"?/g)].map((m) => m[1]).find((name) => byColumn.has(name) && !columns.has(name));
|
|
490
|
+
if (!column) return unreadable("a field reads no column of this table");
|
|
491
|
+
const key = input.keyByColumn.get(column);
|
|
492
|
+
if (!key) return unreadable(`the column "${column}" is not a property`);
|
|
493
|
+
const jsonPath = [...inner.matchAll(/(?:->|#>)\s*'([^']+)'/g)].flatMap((m) => m[1].replace(/^\{|\}$/g, "").split(","));
|
|
494
|
+
fields.push({
|
|
495
|
+
path: [key, ...jsonPath].join("."),
|
|
496
|
+
weight
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
if (fields.length === 0 || !language) return unreadable("its expression is not one Rebase writes");
|
|
500
|
+
const search = {
|
|
501
|
+
...main.column_name !== DEFAULT_SEARCH_COLUMN ? { column: main.column_name } : {},
|
|
502
|
+
...language !== DEFAULT_SEARCH_LANGUAGE ? { language } : {},
|
|
503
|
+
...unaccent ? { unaccent: true } : {},
|
|
504
|
+
...fuzzy ? { fuzzy: true } : {},
|
|
505
|
+
fields: fields.map((f) => ({
|
|
506
|
+
path: f.path,
|
|
507
|
+
weight: f.weight
|
|
508
|
+
}))
|
|
509
|
+
};
|
|
510
|
+
const properties = {};
|
|
511
|
+
for (const [column, key] of input.keyByColumn) {
|
|
512
|
+
const col = byColumn.get(column);
|
|
513
|
+
const prop = col ? propertyFor(col) : void 0;
|
|
514
|
+
if (prop) properties[key] = prop;
|
|
515
|
+
}
|
|
516
|
+
const candidate = {
|
|
517
|
+
slug: input.table,
|
|
518
|
+
name: input.table,
|
|
519
|
+
table: input.table,
|
|
520
|
+
...input.schema !== "public" ? { schema: input.schema } : {},
|
|
521
|
+
properties,
|
|
522
|
+
search
|
|
523
|
+
};
|
|
524
|
+
let fingerprints;
|
|
525
|
+
try {
|
|
526
|
+
const spec = buildSearchColumnSpec(candidate);
|
|
527
|
+
if (!spec) return unreadable("no block compiles from it");
|
|
528
|
+
fingerprints = new Map(searchColumnStamps(spec).map((stamp) => [stamp.column, stamp.fingerprint]));
|
|
529
|
+
} catch (err) {
|
|
530
|
+
return unreadable(err instanceof Error ? err.message : String(err));
|
|
531
|
+
}
|
|
532
|
+
for (const col of stamped) if (fingerprints.get(col.column_name) !== input.comments.get(col.column_name)) return unreadable("the block read from its expression does not reproduce the column's stamp");
|
|
533
|
+
return {
|
|
534
|
+
columns,
|
|
535
|
+
block: `\n search: {\n ${[
|
|
536
|
+
...search.column ? [`column: ${JSON.stringify(search.column)}`] : [],
|
|
537
|
+
...search.language ? [`language: ${JSON.stringify(search.language)}`] : [],
|
|
538
|
+
...search.unaccent ? ["unaccent: true"] : [],
|
|
539
|
+
...search.fuzzy ? ["fuzzy: true"] : [],
|
|
540
|
+
`fields: [\n${fields.map((f) => ` { path: ${JSON.stringify(f.path)}, weight: ${JSON.stringify(f.weight)} }`).join(",\n")}\n ]`
|
|
541
|
+
].join(",\n ")}\n },`
|
|
542
|
+
};
|
|
543
|
+
}
|
|
498
544
|
/**
|
|
499
545
|
* A timestamp the database maintains: a temporal column defaulting to the
|
|
500
546
|
* transaction clock.
|
|
@@ -664,6 +710,41 @@ function classifyJunction(table, context) {
|
|
|
664
710
|
};
|
|
665
711
|
}
|
|
666
712
|
/**
|
|
713
|
+
* A policy expression that reads who is asking: the `app.uid` setting every
|
|
714
|
+
* request runs with, `rebase.uid()` (or the retired `auth.uid()`), the JWT,
|
|
715
|
+
* or the session's own role.
|
|
716
|
+
*/
|
|
717
|
+
var CALLER_IDENTITY = /current_setting\(\s*'app\.(uid|user_id|jwt)'|\b(rebase|auth)\.(uid|jwt)\s*\(|\bcurrent_user\b|\bsession_user\b/i;
|
|
718
|
+
/**
|
|
719
|
+
* The tables whose row-level security hands each caller rows of their own,
|
|
720
|
+
* with the policies that do it.
|
|
721
|
+
*
|
|
722
|
+
* Structure in the sense this module means: a policy is a rule the database
|
|
723
|
+
* enforces, and one that compares a row to the caller's identity says the rows
|
|
724
|
+
* belong to users. That is the app's data. A young database holds three
|
|
725
|
+
* customers and one order; by row count the customers are "a 3-row code list"
|
|
726
|
+
* and the orders a child reached only through them, and both go missing from
|
|
727
|
+
* the navigation of the app that owns them.
|
|
728
|
+
*/
|
|
729
|
+
function callerScopedTables(metadata) {
|
|
730
|
+
const scoped = /* @__PURE__ */ new Map();
|
|
731
|
+
for (const policy of metadata.policies ?? []) {
|
|
732
|
+
if (!CALLER_IDENTITY.test(policy.qual ?? "") && !CALLER_IDENTITY.test(policy.with_check ?? "")) continue;
|
|
733
|
+
scoped.set(policy.table_name, [...scoped.get(policy.table_name) ?? [], policy.policy_name]);
|
|
734
|
+
}
|
|
735
|
+
return scoped;
|
|
736
|
+
}
|
|
737
|
+
/** An entity because its policies give each user their own rows — see {@link callerScopedTables}. */
|
|
738
|
+
function classifyCallerScoped(table, perCaller) {
|
|
739
|
+
const policies = perCaller.get(table);
|
|
740
|
+
if (!policies) return null;
|
|
741
|
+
return {
|
|
742
|
+
table,
|
|
743
|
+
role: "entity",
|
|
744
|
+
reason: `its row-level security gives each user their own rows (${policies.join(", ")}), so they are the app's data`
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
667
748
|
* A small, referenced, self-contained code list.
|
|
668
749
|
*
|
|
669
750
|
* Requires: something references it, it references nothing, it carries few and
|
|
@@ -789,8 +870,9 @@ function pickOwner(meta, constraints) {
|
|
|
789
870
|
function classifyTables(metadata, tables) {
|
|
790
871
|
const context = buildContext(metadata, tables);
|
|
791
872
|
const result = /* @__PURE__ */ new Map();
|
|
873
|
+
const perCaller = callerScopedTables(metadata);
|
|
792
874
|
for (const table of tables.keys()) {
|
|
793
|
-
const classification = classifyJunction(table, context) ?? classifyLookup(table, context) ?? classifyOwnedChild(table, context) ?? {
|
|
875
|
+
const classification = classifyJunction(table, context) ?? classifyCallerScoped(table, perCaller) ?? classifyLookup(table, context) ?? classifyOwnedChild(table, context) ?? {
|
|
794
876
|
table,
|
|
795
877
|
role: "entity",
|
|
796
878
|
reason: "no structural evidence that it is a join table, a code list, or owned by another table"
|
|
@@ -869,6 +951,32 @@ function deriveKanbanProperty(facts) {
|
|
|
869
951
|
return fact.column.column_name;
|
|
870
952
|
}
|
|
871
953
|
}
|
|
954
|
+
/** Names a string column goes by when it holds a board's card order (a fractional-indexing key). */
|
|
955
|
+
var ORDER_KEY_NAMES = /* @__PURE__ */ new Set([
|
|
956
|
+
"__order",
|
|
957
|
+
"order_key",
|
|
958
|
+
"sort_key",
|
|
959
|
+
"rank",
|
|
960
|
+
"lexorank",
|
|
961
|
+
"position_key"
|
|
962
|
+
]);
|
|
963
|
+
/**
|
|
964
|
+
* The string column that can hold a board's card order, or nothing.
|
|
965
|
+
*
|
|
966
|
+
* A board without `orderProperty` cannot keep a card's position within a
|
|
967
|
+
* column, and the config validator says so on every generate — so a board is
|
|
968
|
+
* emitted only when the table already has somewhere to store the order. A
|
|
969
|
+
* legacy table almost never does; asking its owner to add a hidden column
|
|
970
|
+
* just to satisfy generated output was the wrong way round.
|
|
971
|
+
*/
|
|
972
|
+
function deriveKanbanOrderProperty(facts) {
|
|
973
|
+
for (const fact of facts.values()) {
|
|
974
|
+
if (fact.isPk || fact.isFk || fact.isGenerated || fact.enumValues) continue;
|
|
975
|
+
if (fact.propType !== "string") continue;
|
|
976
|
+
if (!/^(text|character varying)$/.test(fact.column.data_type)) continue;
|
|
977
|
+
if (ORDER_KEY_NAMES.has(fact.column.column_name.toLowerCase())) return fact.column.column_name;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
872
980
|
/**
|
|
873
981
|
* The column a list should be sorted by, newest first.
|
|
874
982
|
*
|
|
@@ -908,6 +1016,34 @@ function deriveListProperties(propertiesOrder, hidden = /* @__PURE__ */ new Set(
|
|
|
908
1016
|
* no process.exit. It is imported by introspect-db.ts (the CLI entry-point)
|
|
909
1017
|
* and consumed directly by tests.
|
|
910
1018
|
*/
|
|
1019
|
+
/**
|
|
1020
|
+
* Why a table keyed on several columns is not generated as a collection, or
|
|
1021
|
+
* `undefined` when it is.
|
|
1022
|
+
*
|
|
1023
|
+
* Several `isId` properties are one composite key, and the planner builds that
|
|
1024
|
+
* table under the same `PRIMARY KEY (a, b)` — so such a table round-trips when
|
|
1025
|
+
* two things hold:
|
|
1026
|
+
*
|
|
1027
|
+
* - **Every key column can carry `isId`,** which exists on string and number
|
|
1028
|
+
* properties only. Read from the column's SQL type, not a data sample: a
|
|
1029
|
+
* `line_no` holding only 1s is not a boolean. A key with a column of another
|
|
1030
|
+
* type — pagila's `payment` is keyed on a timestamp too — can be marked on
|
|
1031
|
+
* some of its columns only, and those would be read as the whole key.
|
|
1032
|
+
* - **Nothing points at it.** A foreign key into it, or a junction, is a link
|
|
1033
|
+
* into a composite key, which the planner refuses: the relation has one
|
|
1034
|
+
* column to point with.
|
|
1035
|
+
*/
|
|
1036
|
+
function compositeKeySkipReason(meta, allFks) {
|
|
1037
|
+
if (meta.pks.length < 2) return void 0;
|
|
1038
|
+
const unmarkable = meta.pks.filter((pk) => {
|
|
1039
|
+
const column = meta.columns.find((c) => c.column_name === pk);
|
|
1040
|
+
const type = column ? mapPgType(column.data_type) : void 0;
|
|
1041
|
+
return type !== "string" && type !== "number";
|
|
1042
|
+
});
|
|
1043
|
+
if (unmarkable.length > 0) return `its key column${unmarkable.length > 1 ? "s" : ""} ${unmarkable.map((c) => `"${c}"`).join(", ")} cannot carry \`isId\`, which a string or number property takes`;
|
|
1044
|
+
const referencing = [...new Set(allFks.filter((fk) => fk.foreign_table_name === meta.name && fk.table_name !== meta.name).map((fk) => fk.table_name))];
|
|
1045
|
+
if (referencing.length > 0) return `${referencing.map((t) => `"${t}"`).join(", ")} point${referencing.length > 1 ? "" : "s"} at it, and a link cannot point at a composite key`;
|
|
1046
|
+
}
|
|
911
1047
|
var IRREGULAR_SINGULARS = {
|
|
912
1048
|
people: "person",
|
|
913
1049
|
children: "child",
|
|
@@ -1230,6 +1366,57 @@ function sortPropertiesOrder(entries) {
|
|
|
1230
1366
|
var ADMIN_TYPES_PACKAGE = "@rebasepro/cms-types";
|
|
1231
1367
|
var COMMON_PACKAGE = "@rebasepro/common";
|
|
1232
1368
|
var TYPES_PACKAGE = "@rebasepro/types";
|
|
1369
|
+
/**
|
|
1370
|
+
* The table's row-level security policies, as the collection's `securityRules`.
|
|
1371
|
+
*
|
|
1372
|
+
* A table's policies are its access model, and a collection that describes
|
|
1373
|
+
* none leaves every one of them an "orphan": `doctor` fails on it, every push
|
|
1374
|
+
* warns about it, and a push drops one whose name has the shape an unnamed
|
|
1375
|
+
* rule compiles to (`<table>_<op>_<hash>`, which is every rule on a database
|
|
1376
|
+
* Rebase built). Described here, each is recreated from its rule as it was.
|
|
1377
|
+
*
|
|
1378
|
+
* Read back with `policyToSecurityRule` — what Studio's "Import from table"
|
|
1379
|
+
* and "Import to codebase" use — so the `TO` list, the mode and both clauses
|
|
1380
|
+
* come back as they are. The framework's own defaults
|
|
1381
|
+
* (`<table>_default_admin_read` and friends) are left out: every collection is
|
|
1382
|
+
* given them anyway, and described twice they would be written twice.
|
|
1383
|
+
*/
|
|
1384
|
+
function renderSecurityRules(tableName, policies) {
|
|
1385
|
+
const injected = getGeneratedPolicyNames({
|
|
1386
|
+
name: tableName,
|
|
1387
|
+
slug: tableName,
|
|
1388
|
+
table: tableName,
|
|
1389
|
+
properties: {}
|
|
1390
|
+
});
|
|
1391
|
+
const own = policies.filter((policy) => policy.table_name === tableName && !injected.has(policy.policy_name));
|
|
1392
|
+
if (own.length === 0) return "";
|
|
1393
|
+
return `
|
|
1394
|
+
// The table's row-level security policies, read from the database. \`rebase db push\`
|
|
1395
|
+
// recreates each from its rule, so a policy is changed here from now on.\n securityRules: [${own.map((policy) => {
|
|
1396
|
+
const rule = policyToSecurityRule({
|
|
1397
|
+
policyname: policy.policy_name,
|
|
1398
|
+
cmd: toPolicyCommand(policy.cmd),
|
|
1399
|
+
permissive: policy.permissive.toUpperCase() === "RESTRICTIVE" ? "RESTRICTIVE" : "PERMISSIVE",
|
|
1400
|
+
roles: policy.roles,
|
|
1401
|
+
qual: policy.qual,
|
|
1402
|
+
with_check: policy.with_check
|
|
1403
|
+
});
|
|
1404
|
+
return `\n {\n${Object.entries(rule).map(([key, value]) => {
|
|
1405
|
+
return ` ${key}: ${Array.isArray(value) ? `[${value.map((item) => quote(String(item))).join(", ")}]` : quote(String(value))}`;
|
|
1406
|
+
}).join(",\n")}\n },`;
|
|
1407
|
+
}).join("")}\n ],`;
|
|
1408
|
+
}
|
|
1409
|
+
/** `pg_policies.cmd`, narrowed to the commands a policy can have. */
|
|
1410
|
+
function toPolicyCommand(cmd) {
|
|
1411
|
+
switch (cmd.toUpperCase()) {
|
|
1412
|
+
case "SELECT": return "SELECT";
|
|
1413
|
+
case "INSERT": return "INSERT";
|
|
1414
|
+
case "UPDATE": return "UPDATE";
|
|
1415
|
+
case "DELETE": return "DELETE";
|
|
1416
|
+
case "ALL": return "ALL";
|
|
1417
|
+
default: return;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1233
1420
|
/** Adds entries to a property's `validation` block, creating it if absent. */
|
|
1234
1421
|
function withValidation(extra, entries) {
|
|
1235
1422
|
if (entries.length === 0) return extra;
|
|
@@ -1265,6 +1452,8 @@ function withAdminOptions(extra, entries, emitAdmin = true) {
|
|
|
1265
1452
|
if (extra.includes("admin: {")) return extra.replace("admin: {", `admin: {\n${block},`);
|
|
1266
1453
|
return `${extra}\n admin: {\n${block}\n },`;
|
|
1267
1454
|
}
|
|
1455
|
+
/** A SQL string literal. */
|
|
1456
|
+
var quoteSqlString = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
1268
1457
|
/** A TypeScript string literal, escaped. */
|
|
1269
1458
|
function quote(value) {
|
|
1270
1459
|
return JSON.stringify(value);
|
|
@@ -1369,6 +1558,9 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1369
1558
|
};
|
|
1370
1559
|
let propsOutput = "";
|
|
1371
1560
|
let relationsOutput = "";
|
|
1561
|
+
/** What the planner will not reproduce exactly, column by column. */
|
|
1562
|
+
const fileNotes = [];
|
|
1563
|
+
const skippedTables = context.skippedTables ?? /* @__PURE__ */ new Set();
|
|
1372
1564
|
const orderEntries = [];
|
|
1373
1565
|
const propertyBlocks = /* @__PURE__ */ new Map();
|
|
1374
1566
|
/**
|
|
@@ -1386,25 +1578,45 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1386
1578
|
const hiddenFromCollection = /* @__PURE__ */ new Set();
|
|
1387
1579
|
let columnIndex = 0;
|
|
1388
1580
|
const isCompositePk = meta.pks.length > 1;
|
|
1581
|
+
/**
|
|
1582
|
+
* The property type a column is generated as, with what the data sample
|
|
1583
|
+
* adds to it. A column of a composite key keeps its SQL type's reading:
|
|
1584
|
+
* it carries `isId`, which only a string or number property takes, and a
|
|
1585
|
+
* key column holding only 1s and 0s is still a key, not a boolean.
|
|
1586
|
+
*/
|
|
1587
|
+
const propertyTypeOf = (col) => {
|
|
1588
|
+
const isEnumColumn = col.data_type === "USER-DEFINED" && enumMap.get(col.udt_name) !== void 0;
|
|
1589
|
+
const propType = isEnumColumn ? "string" : col.udt_name === "vector" ? "vector" : mapPgType(col.data_type);
|
|
1590
|
+
if (isEnumColumn || !sampleData || sampleData.length === 0 || isCompositePk && meta.pks.includes(col.column_name)) return {
|
|
1591
|
+
propType,
|
|
1592
|
+
finalPropType: propType,
|
|
1593
|
+
inferenceExtra: ""
|
|
1594
|
+
};
|
|
1595
|
+
const values = sampleData.map((r) => r[col.column_name]);
|
|
1596
|
+
const inferred = inferPropertyFromData(col.column_name, col.data_type, propType, values, meta.pks.includes(col.column_name), emitAdmin);
|
|
1597
|
+
return {
|
|
1598
|
+
propType,
|
|
1599
|
+
finalPropType: inferred.propType || propType,
|
|
1600
|
+
inferenceExtra: inferred.extra || ""
|
|
1601
|
+
};
|
|
1602
|
+
};
|
|
1603
|
+
const compositeKeyMarkable = isCompositePk && meta.pks.every((pk) => {
|
|
1604
|
+
const keyColumn = meta.columns.find((c) => c.column_name === pk);
|
|
1605
|
+
if (!keyColumn) return false;
|
|
1606
|
+
const { finalPropType } = propertyTypeOf(keyColumn);
|
|
1607
|
+
return finalPropType === "string" || finalPropType === "number";
|
|
1608
|
+
});
|
|
1389
1609
|
for (const col of meta.columns) {
|
|
1610
|
+
if (col.is_generated === "ALWAYS" && (columnComments.get(col.column_name) ?? "").startsWith("rebase:search:v1:")) continue;
|
|
1390
1611
|
if (meta.fks.some((fk) => fk.column_name === col.column_name) && !meta.pks.includes(col.column_name)) continue;
|
|
1391
1612
|
const currentIndex = columnIndex++;
|
|
1392
1613
|
const propertyKey = firstFreeKey([toWireKey(col.column_name), col.column_name], propertyBlocks);
|
|
1393
1614
|
keyByColumn.set(col.column_name, propertyKey);
|
|
1394
1615
|
const colEnumValues = enumMap.get(col.udt_name);
|
|
1395
1616
|
const isEnumColumn = col.data_type === "USER-DEFINED" && colEnumValues !== void 0;
|
|
1396
|
-
const isVectorColumn = col.udt_name === "vector";
|
|
1397
|
-
const propType = isEnumColumn ? "string" : isVectorColumn ? "vector" : mapPgType(col.data_type);
|
|
1398
1617
|
let extra = "";
|
|
1399
1618
|
const colNameLower = col.column_name.toLowerCase();
|
|
1400
|
-
|
|
1401
|
-
let inferenceExtra = "";
|
|
1402
|
-
if (!isEnumColumn && sampleData && sampleData.length > 0) {
|
|
1403
|
-
const values = sampleData.map((r) => r[col.column_name]);
|
|
1404
|
-
const inferred = inferPropertyFromData(col.column_name, col.data_type, propType, values, meta.pks.includes(col.column_name), emitAdmin);
|
|
1405
|
-
if (inferred.propType) finalPropType = inferred.propType;
|
|
1406
|
-
if (inferred.extra) inferenceExtra = inferred.extra;
|
|
1407
|
-
}
|
|
1619
|
+
const { propType, finalPropType, inferenceExtra } = propertyTypeOf(col);
|
|
1408
1620
|
const columnChecks = tableChecks?.get(col.column_name);
|
|
1409
1621
|
if (isEnumColumn && colEnumValues) {
|
|
1410
1622
|
const enumEntries = colEnumValues.map((v) => `{ id: ${quote(v)}, label: ${quote(humanize(v))} }`).join(", ");
|
|
@@ -1412,6 +1624,13 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1412
1624
|
} else if (columnChecks?.enumValues && !inferenceExtra.includes("enum:") && propType === "string") {
|
|
1413
1625
|
const enumEntries = columnChecks.enumValues.map((v) => `{ id: ${quote(v)}, label: ${quote(humanize(v))} }`).join(", ");
|
|
1414
1626
|
extra += `\n enum: [${enumEntries}],`;
|
|
1627
|
+
const enumType = toPostgresIdentifier(`${tableName}_${col.column_name}`);
|
|
1628
|
+
const labels = columnChecks.enumValues.map((v) => `'${v.replace(/'/g, "''")}'`).join(", ");
|
|
1629
|
+
const check = (context.metadata?.checks ?? []).find((c) => c.table_name === tableName && c.definition.includes(col.column_name));
|
|
1630
|
+
const table = `"${tableName}"`;
|
|
1631
|
+
const column = `"${col.column_name}"`;
|
|
1632
|
+
const restoreDefault = literalDefaultOf(col, "string");
|
|
1633
|
+
fileNotes.push(`"${col.column_name}" is text with CHECK (… IN …), declared here as an \`enum\`, which Rebase stores as the enum type "${enumType}". Convert the column before pushing: CREATE TYPE "${enumType}" AS ENUM (${labels}); ALTER TABLE ${table} ${check ? `DROP CONSTRAINT "${check.constraint_name}", ` : ""}ALTER COLUMN ${column} DROP DEFAULT, ALTER COLUMN ${column} TYPE "${enumType}" USING ${column}::"${enumType}"` + (restoreDefault && "source" in restoreDefault ? `, ALTER COLUMN ${column} SET DEFAULT ${quoteSqlString(JSON.parse(restoreDefault.source))}` : "") + "; — or delete the `enum` to keep the column as it is.");
|
|
1415
1634
|
}
|
|
1416
1635
|
if (finalPropType === "date") {
|
|
1417
1636
|
if (colNameLower === "created_at" || colNameLower === "createdat") {
|
|
@@ -1428,16 +1647,10 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1428
1647
|
}
|
|
1429
1648
|
}
|
|
1430
1649
|
if (finalPropType === "array" && !inferenceExtra.includes("of: {")) {
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
if (
|
|
1434
|
-
|
|
1435
|
-
if (innerType === "string") colType = "text[]";
|
|
1436
|
-
else if (innerType === "number") colType = col.udt_name === "_numeric" ? "numeric[]" : "integer[]";
|
|
1437
|
-
else if (innerType === "boolean") colType = "boolean[]";
|
|
1438
|
-
}
|
|
1439
|
-
if (colType) extra += `\n columnType: ${quote(colType)},`;
|
|
1440
|
-
extra += `\n of: { name: ${quote(`${humanize(col.column_name)} Item`)}, type: ${quote(innerType)} },`;
|
|
1650
|
+
const stored = arrayStorage(col);
|
|
1651
|
+
if (stored.columnType) extra += `\n columnType: ${quote(stored.columnType)},`;
|
|
1652
|
+
if (stored.note) fileNotes.push(stored.note);
|
|
1653
|
+
extra += `\n of: { name: ${quote(`${humanize(col.column_name)} Item`)}, type: ${quote(stored.innerType)} },`;
|
|
1441
1654
|
} else if (finalPropType === "map" && !inferenceExtra.includes("keyValue: true") && !inferenceExtra.includes("properties: {")) extra += "\n keyValue: true,";
|
|
1442
1655
|
if (finalPropType === "string" && !isEnumColumn && !inferenceExtra) {
|
|
1443
1656
|
const isUrl = colNameLower.endsWith("_url") || colNameLower.endsWith("_uri") || colNameLower.endsWith("_link");
|
|
@@ -1451,6 +1664,29 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1451
1664
|
extra += inferenceExtra;
|
|
1452
1665
|
if (!extra.endsWith(",")) extra += ",";
|
|
1453
1666
|
}
|
|
1667
|
+
if (!meta.pks.includes(col.column_name) && !isReadOnlyColumn(col)) {
|
|
1668
|
+
const enumTypeName = toPostgresIdentifier(`${tableName}_${col.column_name}`);
|
|
1669
|
+
const enumTypeShared = isEnumColumn && (context.metadata?.columns ?? meta.columns).filter((other) => other.udt_name === col.udt_name && other.data_type === "USER-DEFINED").length > 1;
|
|
1670
|
+
const stored = finalPropType === "array" ? { keys: [] } : columnStorage(col, finalPropType, {
|
|
1671
|
+
isEnumColumn,
|
|
1672
|
+
enumTypeName,
|
|
1673
|
+
enumTypeShared
|
|
1674
|
+
});
|
|
1675
|
+
for (const key of stored.keys) {
|
|
1676
|
+
const name = key.slice(0, key.indexOf(":"));
|
|
1677
|
+
if (!hasGeneratedKey(extra, name) && !hasGeneratedKey(inferenceExtra, name)) extra += `\n ${key},`;
|
|
1678
|
+
}
|
|
1679
|
+
if (stored.note) fileNotes.push(stored.note);
|
|
1680
|
+
if (!hasGeneratedKey(extra, "autoValue") && !hasGeneratedKey(extra, "defaultValue")) {
|
|
1681
|
+
const fallback = literalDefaultOf(col, finalPropType);
|
|
1682
|
+
if (fallback && "source" in fallback) extra += `\n defaultValue: ${fallback.source},`;
|
|
1683
|
+
else if (fallback) {
|
|
1684
|
+
const expression = col.column_default?.trim();
|
|
1685
|
+
if (col.is_nullable === "NO" && expression) extra += `\n databaseDefault: ${JSON.stringify(`sql\`${expression}\``)},`;
|
|
1686
|
+
else fileNotes.push(fallback.note);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1454
1690
|
const declaredValidation = [];
|
|
1455
1691
|
if (finalPropType === "string" && typeof col.character_maximum_length === "number" && col.character_maximum_length > 0 && !hasGeneratedKey(extra, "max")) declaredValidation.push(`max: ${col.character_maximum_length}`);
|
|
1456
1692
|
if (columnChecks) {
|
|
@@ -1478,16 +1714,30 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1478
1714
|
const columnComment = columnComments.get(col.column_name);
|
|
1479
1715
|
if (columnComment) extra = `\n description: ${quote(columnComment)},${extra}`;
|
|
1480
1716
|
if (meta.pks.includes(col.column_name)) {
|
|
1481
|
-
if (isCompositePk)
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1717
|
+
if (isCompositePk) {
|
|
1718
|
+
extra += `\n // Part of composite primary key (${commentText(meta.pks.join(", "))})`;
|
|
1719
|
+
if (compositeKeyMarkable && !inferenceExtra.includes("isId:")) {
|
|
1720
|
+
const stored = primaryKeyStorage(col, finalPropType);
|
|
1721
|
+
for (const key of stored.keys) {
|
|
1722
|
+
const name = key.slice(0, key.indexOf(":"));
|
|
1723
|
+
if (!hasGeneratedKey(extra, name)) extra += `\n ${key},`;
|
|
1724
|
+
}
|
|
1725
|
+
if (stored.note) fileNotes.push(stored.note);
|
|
1726
|
+
}
|
|
1727
|
+
} else if (!inferenceExtra.includes("isId:")) {
|
|
1728
|
+
const stored = primaryKeyStorage(col, finalPropType);
|
|
1729
|
+
for (const key of stored.keys) {
|
|
1730
|
+
const name = key.slice(0, key.indexOf(":"));
|
|
1731
|
+
if (!hasGeneratedKey(extra, name)) extra += `\n ${key},`;
|
|
1732
|
+
}
|
|
1733
|
+
if (stored.note) fileNotes.push(stored.note);
|
|
1734
|
+
}
|
|
1485
1735
|
}
|
|
1486
1736
|
if (finalPropType === "vector") {
|
|
1487
1737
|
const dims = col.atttypmod && col.atttypmod > 0 ? col.atttypmod : 1536;
|
|
1488
1738
|
extra += `\n dimensions: ${dims},`;
|
|
1489
1739
|
}
|
|
1490
|
-
if (col.is_nullable === "NO" && !meta.pks.includes(col.column_name) && !
|
|
1740
|
+
if (col.is_nullable === "NO" && !meta.pks.includes(col.column_name) && !isReadOnlyColumn(col)) if (extra.includes("validation: {")) extra = extra.replace("validation: {", "validation: {\n required: true,");
|
|
1491
1741
|
else extra += "\n validation: {\n required: true\n },";
|
|
1492
1742
|
const humanName = humanize(col.column_name);
|
|
1493
1743
|
orderEntries.push({
|
|
@@ -1510,6 +1760,7 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1510
1760
|
}
|
|
1511
1761
|
for (const fk of meta.fks) {
|
|
1512
1762
|
const targetTableName = fk.foreign_table_name;
|
|
1763
|
+
if (skippedTables.has(targetTableName)) continue;
|
|
1513
1764
|
if (!joinTables.has(targetTableName)) {
|
|
1514
1765
|
const stripped = toWireKey(fk.column_name.replace(/_id$/, ""));
|
|
1515
1766
|
const relName = firstFreeKey([
|
|
@@ -1530,27 +1781,50 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1530
1781
|
});
|
|
1531
1782
|
const targetCollectionCamel = importCollection(targetTableName);
|
|
1532
1783
|
const relHumanName = humanize(relName);
|
|
1784
|
+
const required = meta.columns.find((c) => c.column_name === fk.column_name)?.is_nullable === "NO";
|
|
1785
|
+
const onDelete = onDeleteOf(fk, required);
|
|
1786
|
+
const requiredLine = required ? "\n validation: { required: true }," : "";
|
|
1787
|
+
const onDeleteLine = onDelete ? `,\n onDelete: ${quote(onDelete)}` : "";
|
|
1788
|
+
const derivedIndex = foreignKeyIndexSpec(context.metadata?.schema ?? "public", tableName, fk.column_name);
|
|
1789
|
+
const indexLine = context.metadata?.indexes !== void 0 && meta.pks[0] !== fk.column_name && !context.metadata.indexes.some((i) => i.table_name === tableName && i.index_name === derivedIndex.indexName) ? `,\n // The database has no index of Rebase's on ${commentText(fk.column_name)}. Delete this line to have one built.
|
|
1790
|
+
index: false` : "";
|
|
1533
1791
|
propertyBlocks.set(relName, `
|
|
1534
1792
|
${propKey(relName)}: {
|
|
1535
1793
|
name: ${quote(relHumanName)},
|
|
1536
|
-
type: "relation"
|
|
1794
|
+
type: "relation",${requiredLine}
|
|
1537
1795
|
// mapped from foreign key: ${commentText(fk.column_name)} -> ${commentText(targetTableName)}(${commentText(fk.foreign_column_name)})
|
|
1538
1796
|
relation: {
|
|
1539
1797
|
kind: "belongsTo",
|
|
1540
1798
|
target: ${relationTarget(targetCollectionCamel)},
|
|
1541
|
-
localKey: ${quote(fk.column_name)}
|
|
1799
|
+
localKey: ${quote(fk.column_name)}${onDeleteLine}${indexLine}
|
|
1542
1800
|
}
|
|
1543
1801
|
},`);
|
|
1544
1802
|
}
|
|
1545
1803
|
}
|
|
1546
|
-
|
|
1804
|
+
/**
|
|
1805
|
+
* A name for an entry in the `relations` array that no property and no
|
|
1806
|
+
* earlier entry has taken.
|
|
1807
|
+
*
|
|
1808
|
+
* Relations and properties share one namespace — a relation is found by
|
|
1809
|
+
* name — so a `belongsTo` property called `series` beside a many-to-many
|
|
1810
|
+
* also called `series` left one of them unreachable: the planner resolved
|
|
1811
|
+
* the property's name to the many-to-many, planned no `series_id` column,
|
|
1812
|
+
* and the first push dropped it.
|
|
1813
|
+
*/
|
|
1814
|
+
const usedRelationNames = /* @__PURE__ */ new Set();
|
|
1815
|
+
const relationNameFor = (preferred, fallback) => {
|
|
1816
|
+
const name = firstFreeKey([preferred, fallback], { has: (key) => propertyBlocks.has(key) || usedRelationNames.has(key) });
|
|
1817
|
+
usedRelationNames.add(name);
|
|
1818
|
+
return name;
|
|
1819
|
+
};
|
|
1820
|
+
const inverseFks = allFks.filter((fk) => fk.foreign_table_name === tableName && !joinTables.has(fk.table_name) && !skippedTables.has(fk.table_name));
|
|
1547
1821
|
for (const fk of inverseFks) {
|
|
1548
1822
|
const sourceTableName = fk.table_name;
|
|
1549
1823
|
const targetCollectionCamel = importCollection(sourceTableName);
|
|
1550
1824
|
relationsOutput += `
|
|
1551
1825
|
{
|
|
1552
1826
|
kind: "hasMany",
|
|
1553
|
-
relationName: ${quote(sourceTableName)},
|
|
1827
|
+
relationName: ${quote(relationNameFor(sourceTableName, `${sourceTableName}_by_${fk.column_name}`))},
|
|
1554
1828
|
target: ${relationTarget(targetCollectionCamel)},
|
|
1555
1829
|
// the ${commentText(sourceTableName)}.${commentText(fk.column_name)} FK points back here
|
|
1556
1830
|
foreignKeyOnTarget: ${quote(fk.column_name)}
|
|
@@ -1564,7 +1838,11 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1564
1838
|
const jtMeta = tablesMap.get(jt);
|
|
1565
1839
|
if (!jtMeta) continue;
|
|
1566
1840
|
const joinFks = jtMeta.fks;
|
|
1567
|
-
const
|
|
1841
|
+
const keyPosition = (fk) => {
|
|
1842
|
+
const position = jtMeta.pks.indexOf(fk.column_name);
|
|
1843
|
+
return position === -1 ? Number.MAX_SAFE_INTEGER : position;
|
|
1844
|
+
};
|
|
1845
|
+
const selfRefFks = joinFks.filter((fk) => fk.foreign_table_name === tableName).sort((a, b) => keyPosition(a) - keyPosition(b));
|
|
1568
1846
|
if (selfRefFks.length === 2) {
|
|
1569
1847
|
const thisFk = selfRefFks[0];
|
|
1570
1848
|
const otherFk = selfRefFks[1];
|
|
@@ -1572,7 +1850,7 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1572
1850
|
relationsOutput += `
|
|
1573
1851
|
{
|
|
1574
1852
|
kind: "manyToMany",
|
|
1575
|
-
relationName: ${quote(relPropName)},
|
|
1853
|
+
relationName: ${quote(relationNameFor(relPropName, `${relPropName}_${jt}`))},
|
|
1576
1854
|
target: ${relationTarget(toCollectionVarName(tableName))},
|
|
1577
1855
|
through: {
|
|
1578
1856
|
table: ${quote(jt)},
|
|
@@ -1583,7 +1861,14 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1583
1861
|
continue;
|
|
1584
1862
|
}
|
|
1585
1863
|
const otherFk = joinFks.find((fk) => fk.foreign_table_name !== tableName);
|
|
1586
|
-
|
|
1864
|
+
const leadingTable = joinFks.find((fk) => fk.column_name === jtMeta.pks[0])?.foreign_table_name;
|
|
1865
|
+
const sortsFirst = otherFk ? [tableName, otherFk.foreign_table_name].sort()[0] : tableName;
|
|
1866
|
+
const declareHere = !leadingTable || leadingTable === sortsFirst || leadingTable === tableName;
|
|
1867
|
+
if (otherFk && !declareHere) relationsOutput += `
|
|
1868
|
+
// The many-to-many with ${commentText(otherFk.foreign_table_name)} through ${commentText(jt)} is declared on
|
|
1869
|
+
// ${commentText(otherFk.foreign_table_name)} only: declaring it here too would re-key ${commentText(jt)}
|
|
1870
|
+
// from (${commentText(jtMeta.pks.join(", "))}) on the next push.`;
|
|
1871
|
+
if (otherFk && declareHere && !skippedTables.has(otherFk.foreign_table_name)) {
|
|
1587
1872
|
const targetTableName = otherFk.foreign_table_name;
|
|
1588
1873
|
const targetCollectionCamel = importCollection(targetTableName);
|
|
1589
1874
|
const thisFk = joinFks.find((fk) => fk.foreign_table_name === tableName);
|
|
@@ -1591,12 +1876,22 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1591
1876
|
relationsOutput += `
|
|
1592
1877
|
{
|
|
1593
1878
|
kind: "manyToMany",
|
|
1594
|
-
relationName: ${quote(targetTableName)},
|
|
1879
|
+
relationName: ${quote(relationNameFor(targetTableName, `${targetTableName}_via_${jt}`))},
|
|
1595
1880
|
target: ${relationTarget(targetCollectionCamel)},${throughCode}
|
|
1596
1881
|
},`;
|
|
1597
1882
|
}
|
|
1598
1883
|
}
|
|
1884
|
+
const search = reconstructSearchBlock({
|
|
1885
|
+
schema: context.metadata?.schema ?? "public",
|
|
1886
|
+
table: tableName,
|
|
1887
|
+
columns: meta.columns,
|
|
1888
|
+
comments: columnComments,
|
|
1889
|
+
keyByColumn
|
|
1890
|
+
});
|
|
1891
|
+
if (search.note) fileNotes.push(search.note);
|
|
1892
|
+
const searchBlock = search.block ?? "";
|
|
1599
1893
|
const relationsBlock = relationsOutput ? `\n relations: [${relationsOutput}\n ],` : "";
|
|
1894
|
+
const securityRulesBlock = renderSecurityRules(tableName, context.metadata?.policies ?? []);
|
|
1600
1895
|
const sortedPropertiesOrder = sortPropertiesOrder(orderEntries);
|
|
1601
1896
|
for (const key of sortedPropertiesOrder) propsOutput += propertyBlocks.get(key) || "";
|
|
1602
1897
|
const adminEntries = [`icon: ${quote(icon)}`];
|
|
@@ -1609,7 +1904,11 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1609
1904
|
const titleProperty = deriveTitleProperty(derivedFacts);
|
|
1610
1905
|
if (titleProperty) adminEntries.push(`display: { title: ${quote(asProperty(titleProperty))} }`);
|
|
1611
1906
|
const kanbanProperty = deriveKanbanProperty(derivedFacts);
|
|
1612
|
-
|
|
1907
|
+
const orderProperty = kanbanProperty ? deriveKanbanOrderProperty(derivedFacts) : void 0;
|
|
1908
|
+
if (kanbanProperty && orderProperty) {
|
|
1909
|
+
adminEntries.push(`kanban: {\n columnProperty: ${quote(asProperty(kanbanProperty))}\n }`);
|
|
1910
|
+
adminEntries.push(`orderProperty: ${quote(asProperty(orderProperty))}`);
|
|
1911
|
+
} else if (kanbanProperty) adminEntries.push(`// A board over ${quote(asProperty(kanbanProperty))} fits this table, but a board needs a string column to keep each card's order in. Add one, then declare kanban: { columnProperty: ${quote(asProperty(kanbanProperty))} } and orderProperty: "<that property>".`);
|
|
1613
1912
|
const sort = deriveSort(derivedFacts);
|
|
1614
1913
|
if (sort) adminEntries.push(`sort: [${quote(asProperty(sort[0]))}, "desc"]`);
|
|
1615
1914
|
}
|
|
@@ -1621,19 +1920,21 @@ function generateCollectionFile(tableName, meta, allFks, joinTables, tablesMap,
|
|
|
1621
1920
|
const descriptionBlock = tableComment ? `\n description: ${quote(tableComment)},` : "";
|
|
1622
1921
|
const pgSchema = context.metadata?.schema;
|
|
1623
1922
|
const schemaBlock = pgSchema && pgSchema !== "public" ? `\n schema: ${quote(pgSchema)},` : "";
|
|
1624
|
-
const classificationNote = classification && classification.role !== "entity" ? `\n// Introspected as a ${commentText(classification.role)}: ${commentText(classification.reason)}.\n` : "";
|
|
1923
|
+
const classificationNote = classification && classification.role !== "entity" ? `\n// Introspected as ${/^[aeiou]/.test(classification.role) ? "an" : "a"} ${commentText(classification.role)}: ${commentText(classification.reason)}.\n` : "";
|
|
1924
|
+
for (const note of fileNotes) context.notes?.push(`${tableName}: ${note}`);
|
|
1925
|
+
const storageNote = fileNotes.length > 0 ? `\n// ⚠ Not reproduced exactly by \`rebase db push\` — read before pushing:\n${fileNotes.map((n) => `// - ${commentText(n)}`).join("\n")}\n` : "";
|
|
1625
1926
|
const collectionVarName = toCollectionVarName(tableName);
|
|
1626
1927
|
const [open, close] = builder === "annotation" ? [`const ${collectionVarName}: PostgresCollectionConfig = {`, "};"] : [`const ${collectionVarName} = defineCollection({`, "});"];
|
|
1627
1928
|
const importLines = Array.from(imports);
|
|
1628
1929
|
return `${[...importLines.filter((line) => !line.includes("from \"./")), ...importLines.filter((line) => line.includes("from \"./"))].join("\n")}
|
|
1629
|
-
${classificationNote}
|
|
1930
|
+
${classificationNote}${storageNote}
|
|
1630
1931
|
${open}
|
|
1631
1932
|
name: ${quote(collectionName)},
|
|
1632
1933
|
singularName: ${quote(singular)},
|
|
1633
1934
|
slug: ${quote(tableName)},
|
|
1634
|
-
table: ${quote(tableName)},${schemaBlock}${descriptionBlock}
|
|
1935
|
+
table: ${quote(tableName)},${schemaBlock}${descriptionBlock}${searchBlock}
|
|
1635
1936
|
properties: {${propsOutput}
|
|
1636
|
-
},${relationsBlock}${adminBlock}
|
|
1937
|
+
},${relationsBlock}${securityRulesBlock}${adminBlock}
|
|
1637
1938
|
${close}
|
|
1638
1939
|
|
|
1639
1940
|
export default ${collectionVarName};
|
|
@@ -1692,6 +1993,6 @@ function safeHostFromUrl(url) {
|
|
|
1692
1993
|
return url.includes("@") ? url.split("@")[1] : "(local connection)";
|
|
1693
1994
|
}
|
|
1694
1995
|
//#endregion
|
|
1695
|
-
export {
|
|
1996
|
+
export { compositeKeySkipReason as a, getIconForTable as c, safeHostFromUrl as d, singularize as f, humanize as g, mapPgType as h, buildTablesMap as i, identifyJoinTables as l, lookupCandidates as m, COMMON_PACKAGE as n, generateCollectionFile as o, classifyTables as p, buildEnumMap as r, generateIndexContent as s, ADMIN_TYPES_PACKAGE as t, mergeIndexContent as u };
|
|
1696
1997
|
|
|
1697
|
-
//# sourceMappingURL=introspect-db-logic-
|
|
1998
|
+
//# sourceMappingURL=introspect-db-logic-BCc1Jym2.js.map
|