@rebasepro/server-postgres 0.21.2-canary.g1ea48be → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BranchService-CucnFcSE.js → BranchService-DGPL6G_C.js} +2 -2
- package/dist/{BranchService-CucnFcSE.js.map → BranchService-DGPL6G_C.js.map} +1 -1
- package/dist/PostgresBackendDriver.d.ts +41 -1
- package/dist/auth/services.d.ts +27 -8
- package/dist/backup/backup-cli.d.ts +22 -0
- package/dist/{backup-cli-CkjsJEcu.js → backup-cli-24v4OlSp.js} +17 -14
- package/dist/backup-cli-24v4OlSp.js.map +1 -0
- package/dist/{backup-service-BNLwvxuy.js → backup-service-HQ9GC4tN.js} +2 -2
- package/dist/{backup-service-BNLwvxuy.js.map → backup-service-HQ9GC4tN.js.map} +1 -1
- package/dist/{cli-errors-Dka89exj.js → cli-errors-B8qHg02P.js} +6 -1
- package/dist/cli-errors-B8qHg02P.js.map +1 -0
- package/dist/cli-flags-BglvpjHv.js +138 -0
- package/dist/cli-flags-BglvpjHv.js.map +1 -0
- package/dist/cli-flags.d.ts +28 -0
- package/dist/cli-helpers.d.ts +6 -0
- package/dist/cli-scratch-database.d.ts +34 -0
- package/dist/cli.js +97 -261
- package/dist/cli.js.map +1 -1
- package/dist/{column-plan-helpers-CpILzHJS.js → column-plan-helpers-DF-8dTVa.js} +16 -20
- package/dist/column-plan-helpers-DF-8dTVa.js.map +1 -0
- package/dist/{doctor-Gzzd8wTq.js → doctor-Cb2thZ8s.js} +8 -7
- package/dist/doctor-Cb2thZ8s.js.map +1 -0
- package/dist/{ensure-collection-policies-B4IDFtQ-.js → ensure-collection-policies-RHUcEp4v.js} +5 -5
- package/dist/{ensure-collection-policies-B4IDFtQ-.js.map → ensure-collection-policies-RHUcEp4v.js.map} +1 -1
- package/dist/{ensure-collection-tables-Cr2ye5JC.js → ensure-collection-tables-BEEjn5cn.js} +53 -22
- package/dist/ensure-collection-tables-BEEjn5cn.js.map +1 -0
- package/dist/{ensure-tables-BnEvEJPr.js → ensure-tables-Dhn9KM3B.js} +2 -2
- package/dist/{ensure-tables-BnEvEJPr.js.map → ensure-tables-Dhn9KM3B.js.map} +1 -1
- package/dist/{generate-drizzle-schema-logic-DFa9qy9u.js → generate-drizzle-schema-logic-BfzK7UQd.js} +2 -2
- package/dist/{generate-drizzle-schema-logic-DFa9qy9u.js.map → generate-drizzle-schema-logic-BfzK7UQd.js.map} +1 -1
- package/dist/{generate-drizzle-schema-D2MDdJt-.js → generate-drizzle-schema-yzY_BLhr.js} +2 -2
- package/dist/{generate-drizzle-schema-D2MDdJt-.js.map → generate-drizzle-schema-yzY_BLhr.js.map} +1 -1
- package/dist/{generate-postgres-ddl-logic-CR2xcS7e.js → generate-postgres-ddl-logic-Bt2d2mRH.js} +2 -2
- package/dist/{generate-postgres-ddl-logic-CR2xcS7e.js.map → generate-postgres-ddl-logic-Bt2d2mRH.js.map} +1 -1
- package/dist/index.es.js +1882 -392
- package/dist/index.es.js.map +1 -1
- package/dist/{introspect-db-logic-C6LQdTxj.js → introspect-db-logic-WMuAfxvw.js} +323 -4
- package/dist/introspect-db-logic-WMuAfxvw.js.map +1 -0
- package/dist/{plan-schema-Hgl62S-w.js → plan-schema-C0fxM8dY.js} +170 -38
- package/dist/plan-schema-C0fxM8dY.js.map +1 -0
- package/dist/{policy-drift-B0GDRh7_.js → policy-drift-DljYdrpW.js} +2 -2
- package/dist/{policy-drift-B0GDRh7_.js.map → policy-drift-DljYdrpW.js.map} +1 -1
- package/dist/{rls-bootstrap-sql-BHA6jLtj.js → rls-bootstrap-sql-B8EclDyM.js} +320 -17
- package/dist/rls-bootstrap-sql-B8EclDyM.js.map +1 -0
- package/dist/schema/atlas-argv.d.ts +15 -0
- package/dist/schema/column-plan-helpers.d.ts +23 -4
- package/dist/schema/destructive-sql.d.ts +52 -1
- package/dist/schema/doctor-cli.js +3 -3
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-postgres-ddl.js +1 -1
- package/dist/schema/introspect-db.js +1 -312
- package/dist/schema/introspect-db.js.map +1 -1
- package/dist/schema/plan/diff-plan.d.ts +3 -2
- package/dist/schema/plan/plan-schema.d.ts +10 -3
- package/dist/schema/plan/types.d.ts +6 -1
- package/dist/schema/search-column.d.ts +21 -1
- package/dist/services/FetchService.d.ts +147 -30
- package/dist/services/PersistService.d.ts +56 -5
- package/dist/services/RelationService.d.ts +42 -1
- package/dist/services/RelationWriteService.d.ts +22 -1
- package/dist/services/cdc/CdcListener.d.ts +6 -1
- package/dist/services/collection-helpers.d.ts +11 -0
- package/dist/services/dataService.d.ts +19 -21
- package/dist/services/field-op-sql.d.ts +71 -0
- package/dist/services/junction-writes.d.ts +19 -3
- package/dist/services/pg-notify-listener.d.ts +7 -0
- package/dist/services/read-field-access.d.ts +19 -0
- package/dist/services/read-scope.d.ts +100 -0
- package/dist/services/realtimeService.d.ts +77 -3
- package/dist/services/row-pipeline.d.ts +5 -0
- package/dist/services/soft-delete.d.ts +0 -2
- package/dist/services/write-transaction-scope.d.ts +39 -0
- package/dist/utils/drizzle-conditions.d.ts +63 -8
- package/dist/websocket.d.ts +10 -3
- package/package.json +9 -9
- package/dist/backup-cli-CkjsJEcu.js.map +0 -1
- package/dist/cli-errors-Dka89exj.js.map +0 -1
- package/dist/column-plan-helpers-CpILzHJS.js.map +0 -1
- package/dist/doctor-Gzzd8wTq.js.map +0 -1
- package/dist/ensure-collection-tables-Cr2ye5JC.js.map +0 -1
- package/dist/introspect-db-logic-C6LQdTxj.js.map +0 -1
- package/dist/plan-schema-Hgl62S-w.js.map +0 -1
- package/dist/rls-bootstrap-sql-BHA6jLtj.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"introspect-db.js","names":[],"sources":["../../src/schema/introspect-db-project.ts","../../src/schema/introspect-db-queries.ts","../../src/schema/introspect-db-constraints.ts","../../src/schema/introspect-db.ts"],"sourcesContent":["/**\n * What the project being introspected into can actually import.\n *\n * The generator is pure — `introspect-db-logic.ts` reads no files — so the one\n * question it cannot answer for itself lives here: which `defineCollection`, if\n * any, will resolve from the directory the collections are written to.\n */\nimport fs from \"fs\";\nimport path from \"path\";\n\nimport { ADMIN_TYPES_PACKAGE, COMMON_PACKAGE, type CollectionBuilder } from \"./introspect-db-logic\";\n\n/** How far up to look before giving up, if no project root announces itself. */\nconst MAX_LEVELS = 8;\n\n/** Every dependency name a manifest declares, in any of the three fields. */\nfunction declaredDependencies(manifestPath: string): Set<string> {\n const names = new Set<string>();\n let parsed: unknown;\n try {\n parsed = JSON.parse(fs.readFileSync(manifestPath, \"utf-8\"));\n } catch {\n // A malformed or unreadable package.json is not this function's problem\n // to report — it answers \"can this import resolve\", and the answer is no.\n return names;\n }\n if (typeof parsed !== \"object\" || parsed === null) return names;\n const manifest = parsed as Record<string, unknown>;\n for (const field of [\"dependencies\", \"devDependencies\", \"peerDependencies\"]) {\n const block = manifest[field];\n if (typeof block === \"object\" && block !== null) {\n for (const name of Object.keys(block)) names.add(name);\n }\n }\n return names;\n}\n\n/**\n * Which `defineCollection` to generate against, for collections written to `outDir`.\n *\n * **The detection is the package manifests above the output directory**, unioned from\n * `outDir` up to and including the project root — the first ancestor holding a\n * `rebase.json`, or `MAX_LEVELS` up if there is none.\n *\n * That is the rule Node itself applies: a file in `config/collections` resolves a bare\n * specifier through `config/node_modules`, then `<project>/node_modules`, and so on up.\n * Reading the manifests along that same path answers the only question that matters —\n * *will this import resolve in the project I am writing into* — from the state on disk\n * at the moment of generation. The alternatives are all proxies for it: `rebase.json`'s\n * `apps` block says a CMS scaffold declared an admin app, and a `frontend/` directory\n * says one was scaffolded, but neither is what the compiler consults, and either can be\n * true of a project whose `config` package does not depend on `@rebasepro/cms-types`.\n *\n * Ambiguity resolves towards the admin panel: a project that declares both packages has\n * a panel, and `@rebasepro/cms-types` is the flavour that keeps the `admin` block.\n */\nexport function detectCollectionBuilder(outDir: string): CollectionBuilder {\n let dir = path.resolve(outDir);\n const declared = new Set<string>();\n\n for (let level = 0; level < MAX_LEVELS; level++) {\n const manifest = path.join(dir, \"package.json\");\n if (fs.existsSync(manifest)) {\n for (const name of declaredDependencies(manifest)) declared.add(name);\n }\n // The project root, and the last level worth reading: anything above it\n // belongs to whatever the project happens to be checked out inside.\n if (fs.existsSync(path.join(dir, \"rebase.json\"))) break;\n\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n\n if (declared.has(ADMIN_TYPES_PACKAGE)) return \"admin-types\";\n if (declared.has(COMMON_PACKAGE)) return \"common\";\n return \"annotation\";\n}\n","/**\n * The catalog queries introspection runs, and the shape they produce.\n *\n * These live apart from {@link ./introspect-db.ts} (the CLI entry point) on\n * purpose: the fixture-capture script and the live e2e read a database through\n * this module too, so what the tests see is what the CLI sees. A query that\n * only exists inside the CLI's `main()` can only be tested by running the CLI.\n *\n * No side effects beyond `SELECT`. Introspection reads a database it does not\n * own — it must never `ANALYZE`, create a temp table, or otherwise write.\n */\nimport type {\n TableRow,\n TableColumn,\n EnumValue,\n PrimaryKeyRow,\n ForeignKeyRow,\n UniqueConstraintRow,\n CheckConstraintRow,\n CommentRow,\n SchemaMetadata\n} from \"./introspect-db-logic\";\n\n/** The subset of `pg.Client` this module needs, so tests can pass a fake. */\nexport interface QueryableClient {\n query<R = Record<string, unknown>>(text: string, values?: unknown[]): Promise<{ rows: R[] }>;\n}\n\n/**\n * A Postgres identifier, quoted so that whatever is inside it stays inside it.\n *\n * Introspection reads table and schema names out of `pg_class` and puts them\n * straight back into SQL, and wrapping a name in double quotes is not the same\n * as escaping it: a table named `x\" ; DROP TABLE users; --` closes the quote\n * and then the statement. Postgres's own escape for a quote inside a quoted\n * identifier is to double it, which is what this does.\n *\n * \"The catalogue is trusted\" is true of your own database, and is exactly the\n * assumption this tool cannot make. Introspection exists to be pointed at\n * databases somebody else built, and it runs with the privileges of whoever\n * ran it — which is usually a superuser, because that is who has a connection\n * string to a database they are adopting.\n */\nexport function quoteIdentifier(name: string): string {\n return `\"${name.replace(/\"/g, \"\\\"\\\"\")}\"`;\n}\n\n/** `\"schema\".\"table\"`, both halves quoted. */\nexport function quoteRelation(schema: string, table: string): string {\n return `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;\n}\n\n/**\n * Can this table's name be the name of the file describing it?\n *\n * Introspection writes `<collectionsDir>/<tableName>.ts`, and the table name\n * came from a database this tool did not create. Postgres will hold a table\n * called `../../../etc/cron.d/x`, and `path.join` resolves it — so without this\n * the generator writes a file wherever the name points, as whoever ran the\n * command.\n *\n * Deliberately narrow, and deliberately not a blocklist of `..` and `/`: the\n * question is \"is this a plain file name?\", and every answer that is not\n * obviously yes is a table better written by hand. NUL and the Windows\n * separator are in scope because a collections directory is checked out on\n * whatever machine the developer has.\n */\nexport function isUsableFileName(name: string): boolean {\n if (name.length === 0 || name.length > 200) return false;\n if (name === \".\" || name === \"..\") return false;\n // Path separators (both platforms'), the Windows drive separator, and any\n // control character — a NUL truncates the path at the OS boundary, so a\n // name that reads as harmless in a log can be something else on disk.\n // eslint-disable-next-line no-control-regex\n if (/[/\\\\:\\u0000-\\u001f]/.test(name)) return false;\n // A leading dot makes a hidden file, and a trailing one is invalid on\n // Windows; neither is a table anyone means to generate.\n return !name.startsWith(\".\") && !name.endsWith(\".\");\n}\n\n/**\n * Base tables, excluding partitions.\n *\n * `relispartition` is the reason this reads `pg_class` rather than\n * `information_schema.tables`, which reports every partition as a base table of\n * its own. Pagila partitions `payment` by month; introspected through\n * information_schema it yields `payment` plus 26 near-identical\n * `payment_p2022_*` collections, each with its own nav entry.\n *\n * `relkind = 'p'` (the partitioned parent) is included and `'r'` partitions are\n * dropped, which is the right way round: the parent is the queryable table.\n */\nexport const TABLES_QUERY = `\n SELECT c.relname AS table_name,\n c.relkind = 'p' AS is_partitioned\n FROM pg_class c\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE n.nspname = $1\n AND c.relkind IN ('r', 'p')\n AND NOT c.relispartition\n AND c.relname NOT LIKE 'drizzle_%'\n AND c.relname NOT LIKE 'rebase_%'\n ORDER BY c.relname\n`;\n\n/**\n * Columns, with the catalog facts that say a column is not the user's to edit.\n *\n * `is_generated`/`is_identity` are read for that reason: a generated column\n * rejects any write, so a form that offers it is offering a field that always\n * errors. `character_maximum_length` is the declared `varchar(n)` bound —\n * already enforced by the database, and free to surface as validation.\n */\nexport const COLUMNS_QUERY = `\n SELECT\n c.table_name,\n c.column_name,\n c.data_type,\n c.udt_name,\n c.is_nullable,\n c.column_default,\n c.ordinal_position,\n c.is_generated,\n c.is_identity,\n c.identity_generation,\n c.character_maximum_length,\n c.numeric_precision,\n c.numeric_scale,\n (SELECT a.atttypmod FROM pg_attribute a\n JOIN pg_class pc ON a.attrelid = pc.oid\n WHERE pc.relname = c.table_name\n AND a.attname = c.column_name\n AND pc.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = c.table_schema)) as atttypmod\n FROM information_schema.columns c\n WHERE c.table_schema = $1\n ORDER BY c.table_name, c.ordinal_position\n`;\n\nexport const ENUMS_QUERY = `\n SELECT t.typname AS enum_name,\n e.enumlabel AS enum_value,\n e.enumsortorder AS sort_order\n FROM pg_type t\n JOIN pg_enum e ON t.oid = e.enumtypid\n JOIN pg_namespace n ON t.typnamespace = n.oid\n WHERE n.nspname = $1\n ORDER BY t.typname, e.enumsortorder\n`;\n\n/**\n * Primary key columns, in key order.\n *\n * `ORDER BY k.ord` matters for composite keys: the position of a column inside\n * the key is what tells a two-column PK made of two foreign keys apart from an\n * ordinary composite key, and `= ANY(i.indkey)` (the shape this replaced)\n * returns catalog order instead.\n */\nexport const PRIMARY_KEYS_QUERY = `\n SELECT t.relname AS table_name,\n a.attname AS column_name\n FROM pg_index i\n JOIN pg_class t ON t.oid = i.indrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n CROSS JOIN LATERAL unnest(i.indkey) WITH ORDINALITY AS k(attnum, ord)\n JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = k.attnum\n WHERE i.indisprimary AND n.nspname = $1\n ORDER BY t.relname, k.ord\n`;\n\n/**\n * Foreign keys, one row per referencing column, with the delete rule.\n *\n * Read from `pg_constraint` rather than through\n * `information_schema.constraint_column_usage`, which does not preserve the\n * pairing on a composite foreign key: it reports the cross product of the\n * constraint's columns, so a two-column FK comes back as four rows, two of them\n * pairing the wrong columns. `unnest(...) WITH ORDINALITY` on both sides joined\n * on the ordinal keeps each referencing column with the column it references.\n *\n * `confdeltype` is carried because `ON DELETE CASCADE` is the schema author\n * saying, in the database, that the child cannot outlive the parent — the one\n * unambiguous declaration of ownership a schema contains.\n *\n * Constraints declared on a *partition* are attributed to the partition root,\n * because that is the table this run generates a collection for. Pagila declares\n * `payment`'s three foreign keys on each monthly partition and none on the\n * parent, so reading `pg_constraint` at face value gives the `payment`\n * collection no relations at all. `pick = 1` then keeps one row per logical key,\n * preferring the root's own constraint when it has one, so a schema that\n * declares the key on the parent *and* inherits it onto 26 partitions does not\n * yield 27 copies of the same relation.\n */\nexport const FOREIGN_KEYS_QUERY = `\n WITH fk AS (\n SELECT con.oid,\n con.conname,\n con.conrelid,\n con.confrelid,\n con.conkey,\n con.confkey,\n con.confdeltype,\n COALESCE(pg_partition_root(con.conrelid), con.conrelid) AS root_oid,\n row_number() OVER (\n PARTITION BY COALESCE(pg_partition_root(con.conrelid), con.conrelid),\n con.confrelid, con.conkey, con.confkey\n ORDER BY (con.conrelid = COALESCE(pg_partition_root(con.conrelid), con.conrelid)) DESC,\n con.oid\n ) AS pick\n FROM pg_constraint con\n JOIN pg_namespace n ON n.oid = con.connamespace\n WHERE con.contype = 'f' AND n.nspname = $1\n )\n SELECT root.relname AS table_name,\n sa.attname AS column_name,\n tgt.relname AS foreign_table_name,\n ta.attname AS foreign_column_name,\n fk.conname AS constraint_name,\n s.ord::int AS ordinal,\n CASE fk.confdeltype\n WHEN 'a' THEN 'NO ACTION'\n WHEN 'r' THEN 'RESTRICT'\n WHEN 'c' THEN 'CASCADE'\n WHEN 'n' THEN 'SET NULL'\n WHEN 'd' THEN 'SET DEFAULT'\n END AS delete_rule\n FROM fk\n JOIN pg_class root ON root.oid = fk.root_oid\n JOIN pg_class tgt ON tgt.oid = fk.confrelid\n CROSS JOIN LATERAL unnest(fk.conkey) WITH ORDINALITY AS s(attnum, ord)\n JOIN pg_attribute sa ON sa.attrelid = fk.conrelid AND sa.attnum = s.attnum\n CROSS JOIN LATERAL unnest(fk.confkey) WITH ORDINALITY AS f(attnum, ord)\n JOIN pg_attribute ta ON ta.attrelid = fk.confrelid AND ta.attnum = f.attnum\n WHERE fk.pick = 1 AND s.ord = f.ord\n ORDER BY root.relname, fk.conname, s.ord\n`;\n\n/**\n * Unique constraints and unique indexes, as ordered column lists.\n *\n * Indexes rather than constraints alone, because `CREATE UNIQUE INDEX` and\n * `ADD CONSTRAINT ... UNIQUE` produce the same guarantee and schemas use both.\n * Partial indexes (`indpred IS NOT NULL`) are excluded: they promise uniqueness\n * only over the rows matching their predicate, which is not the promise a\n * `validation: { unique: true }` field makes.\n */\nexport const UNIQUE_CONSTRAINTS_QUERY = `\n SELECT t.relname AS table_name,\n ic.relname AS constraint_name,\n -- ::text, because the driver has no parser for an array of \\`name\\`\n -- and hands back the raw \\`{a,b}\\` literal as a string. Every consumer\n -- here indexes and compares it as an array, and a string of that\n -- shape fails those silently rather than loudly.\n array_agg(a.attname::text ORDER BY k.ord) AS column_names\n FROM pg_index i\n JOIN pg_class t ON t.oid = i.indrelid\n JOIN pg_class ic ON ic.oid = i.indexrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n CROSS JOIN LATERAL unnest(i.indkey) WITH ORDINALITY AS k(attnum, ord)\n JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = k.attnum\n WHERE i.indisunique\n AND NOT i.indisprimary\n AND i.indpred IS NULL\n AND n.nspname = $1\n AND NOT t.relispartition\n GROUP BY t.relname, ic.relname\n ORDER BY t.relname, ic.relname\n`;\n\n/**\n * CHECK constraints, as the source text Postgres reproduces them from.\n *\n * `pg_get_constraintdef` output is normalized by the server — the parser in\n * {@link ./introspect-db-constraints.ts} reads that normalized form, not\n * whatever the author typed.\n */\nexport const CHECK_CONSTRAINTS_QUERY = `\n SELECT DISTINCT\n root.relname AS table_name,\n con.conname AS constraint_name,\n pg_get_constraintdef(con.oid) AS definition\n FROM pg_constraint con\n JOIN pg_namespace n ON n.oid = con.connamespace\n JOIN pg_class root ON root.oid = COALESCE(pg_partition_root(con.conrelid), con.conrelid)\n WHERE con.contype = 'c'\n AND n.nspname = $1\n ORDER BY root.relname, con.conname\n`;\n\n/**\n * `COMMENT ON TABLE` / `COMMENT ON COLUMN`, which is documentation the author\n * already wrote and which nothing downstream has ever read.\n */\nexport const COMMENTS_QUERY = `\n SELECT t.relname AS table_name,\n a.attname AS column_name,\n d.description AS comment\n FROM pg_description d\n JOIN pg_class t ON t.oid = d.objoid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = d.objsubid\n WHERE n.nspname = $1\n AND t.relkind IN ('r', 'p')\n AND (d.objsubid = 0 OR a.attname IS NOT NULL)\n ORDER BY t.relname, d.objsubid\n`;\n\n/**\n * Counts a table's rows, stopping once the answer can only be \"more than\n * `limit`\".\n *\n * Introspection asks this to tell a small reference table from a table that\n * merely looks like one (see `classifyTables`). `reltuples` would answer for\n * free but lies on a table that has never been analyzed — which is every table\n * in a database restored from a dump, i.e. exactly the case introspection meets\n * — and reports -1 there rather than an error, so the lie is silent.\n *\n * The subquery's LIMIT caps the work at `limit + 1` rows however large the\n * table is, so this stays cheap on a table with a billion rows.\n */\nexport async function countRowsUpTo(\n client: QueryableClient,\n schema: string,\n table: string,\n limit: number\n): Promise<number> {\n const { rows } = await client.query<{ count: string }>(\n `SELECT count(*)::text AS count FROM (SELECT 1 FROM ${quoteRelation(schema, table)} LIMIT ${limit + 1}) probe`\n );\n return Number(rows[0]?.count ?? 0);\n}\n\n/**\n * Reads everything the generator needs, in one pass.\n *\n * `rowCounts` is deliberately absent here: it costs a query per table and only\n * a handful of tables can possibly need it. The caller fills it in for the\n * candidates `lookupCandidates()` names.\n */\nexport async function readSchemaMetadata(\n client: QueryableClient,\n schema: string\n): Promise<SchemaMetadata> {\n // Sequential, not `Promise.all`. A `pg.Client` is a single connection with\n // one protocol stream: concurrent `query()` calls on one are queued and\n // deprecated (removed in pg@9), so the parallel version buys nothing and\n // warns while doing it.\n const tables = await client.query<TableRow>(TABLES_QUERY, [schema]);\n const columns = await client.query<TableColumn>(COLUMNS_QUERY, [schema]);\n const enumValues = await client.query<EnumValue>(ENUMS_QUERY, [schema]);\n const pks = await client.query<PrimaryKeyRow>(PRIMARY_KEYS_QUERY, [schema]);\n const fks = await client.query<ForeignKeyRow>(FOREIGN_KEYS_QUERY, [schema]);\n const uniques = await client.query<UniqueConstraintRow>(UNIQUE_CONSTRAINTS_QUERY, [schema]);\n const checks = await client.query<CheckConstraintRow>(CHECK_CONSTRAINTS_QUERY, [schema]);\n const comments = await client.query<CommentRow>(COMMENTS_QUERY, [schema]);\n\n // A column list scoped to the schema still names tables this run excludes —\n // views, partitions, the drizzle bookkeeping tables. Narrow every row set to\n // the tables actually being introspected so downstream code never has to\n // ask whether a table it found in `columns` really exists.\n const known = new Set(tables.rows.map((t) => t.table_name));\n const scoped = <T extends { table_name: string }>(rows: T[]) => rows.filter((r) => known.has(r.table_name));\n\n return {\n schema,\n tables: tables.rows,\n columns: scoped(columns.rows),\n enumValues: enumValues.rows,\n pks: scoped(pks.rows),\n // A foreign key pointing *out* of the introspected set (into another\n // schema, or at a view) cannot be turned into a relation: there is no\n // collection on the other end to link to.\n fks: scoped(fks.rows).filter((fk) => known.has(fk.foreign_table_name)),\n uniques: scoped(uniques.rows),\n checks: scoped(checks.rows),\n comments: scoped(comments.rows),\n rowCounts: {}\n };\n}\n","/**\n * Reading validation rules out of CHECK constraints.\n *\n * A CHECK constraint is the schema author stating a rule the database already\n * enforces. Introspection has never read them, so a generated form let the user\n * type a value the database was always going to reject — the rule was written\n * down, in the schema, and the UI asked anyway.\n *\n * Everything here parses the *normalized* text `pg_get_constraintdef` produces,\n * not what the author typed: Postgres re-renders the expression from its parse\n * tree, so `CHECK (price > 0)` on a `numeric` column always comes back as\n * `CHECK ((price > (0)::numeric))`. That normalization is what makes a\n * string-level parser tractable — the input is generated, and the shapes are\n * few.\n *\n * Constraints this cannot read are skipped in full. A partially-understood\n * constraint is worse than an unread one: it would produce validation that\n * *narrows* differently from the database, so a value the UI accepts still\n * fails on write, or — worse — a value the database allows is refused in the\n * form with no way to see why.\n *\n * Pure module: no I/O, no logging.\n */\nimport type { CheckConstraintRow } from \"./introspect-db-logic\";\n\n/** What a table's CHECK constraints say about one column. */\nexport interface ColumnCheckFacts {\n /** Allowed values, from `IN (…)` / `= ANY (ARRAY[…])` / `= 'literal'`. */\n enumValues?: string[];\n /** `x >= n` */\n min?: number;\n /** `x <= n` */\n max?: number;\n /** `x > n` */\n moreThan?: number;\n /** `x < n` */\n lessThan?: number;\n /** `length(x) >= n` */\n lengthMin?: number;\n /** `length(x) <= n` */\n lengthMax?: number;\n}\n\n/** Per-table, per-column facts. */\nexport type CheckFactsByTable = Map<string, Map<string, ColumnCheckFacts>>;\n\n/** The length functions whose argument is the column being constrained. */\nconst LENGTH_FUNCTIONS = new Set([\"length\", \"char_length\", \"character_length\", \"octet_length\"]);\n\n/**\n * Splits on a separator that appears at paren depth 0 and outside string\n * literals.\n *\n * Depth tracking is the whole job: `ARRAY['a', 'b']` contains a comma that must\n * not split the enclosing argument list, and `'it''s'` contains a quote that\n * must not end the literal.\n */\nfunction splitTopLevel(input: string, separator: string): string[] {\n const parts: string[] = [];\n let depth = 0;\n let inString = false;\n let current = \"\";\n\n for (let i = 0; i < input.length; i++) {\n const ch = input[i];\n\n if (inString) {\n current += ch;\n if (ch === \"'\") {\n // '' inside a literal is an escaped quote, not the end of it.\n if (input[i + 1] === \"'\") {\n current += input[++i];\n } else {\n inString = false;\n }\n }\n continue;\n }\n\n if (ch === \"'\") {\n inString = true;\n current += ch;\n continue;\n }\n if (ch === \"(\" || ch === \"[\") depth++;\n if (ch === \")\" || ch === \"]\") depth--;\n\n if (depth === 0 && input.startsWith(separator, i)) {\n // A word separator must not match inside an identifier: \"AND\" is a\n // separator in `a AND b` but not in `brand = 1`.\n const before = input[i - 1];\n const after = input[i + separator.length];\n const wordish = /[A-Za-z]/.test(separator[0]);\n const boundedOk = !wordish || ((before === undefined || !/[A-Za-z0-9_]/.test(before)) &&\n (after === undefined || !/[A-Za-z0-9_]/.test(after)));\n if (boundedOk) {\n parts.push(current);\n current = \"\";\n i += separator.length - 1;\n continue;\n }\n }\n\n current += ch;\n }\n\n parts.push(current);\n return parts.map((p) => p.trim()).filter((p) => p.length > 0);\n}\n\n/** Removes one layer of wrapping parentheses, repeatedly, when balanced. */\nexport function unwrapParens(input: string): string {\n let s = input.trim();\n while (s.startsWith(\"(\") && s.endsWith(\")\")) {\n let depth = 0;\n let balanced = true;\n let inString = false;\n for (let i = 0; i < s.length; i++) {\n const ch = s[i];\n if (inString) {\n if (ch === \"'\") {\n if (s[i + 1] === \"'\") i++;\n else inString = false;\n }\n continue;\n }\n if (ch === \"'\") { inString = true; continue; }\n if (ch === \"(\") depth++;\n else if (ch === \")\") {\n depth--;\n // The opening paren closed before the end, so the outer pair is\n // not a wrapper: `(a) AND (b)`.\n if (depth === 0 && i < s.length - 1) { balanced = false; break; }\n }\n }\n if (!balanced) break;\n s = s.slice(1, -1).trim();\n }\n return s;\n}\n\n/** Drops trailing `::type` casts, including array and quoted forms. */\nexport function stripCasts(input: string): string {\n let s = unwrapParens(input);\n let previous: string;\n do {\n previous = s;\n s = unwrapParens(\n s.replace(/::\\s*\"?[A-Za-z_][A-Za-z0-9_ ]*\"?(\\s*\\(\\s*\\d+\\s*(,\\s*\\d+\\s*)?\\))?(\\s*\\[\\s*\\])*\\s*$/, \"\").trim()\n );\n } while (s !== previous);\n return s;\n}\n\n/** A bare, unquoted column reference — anything else is an expression. */\nfunction asColumnName(input: string): string | null {\n const s = stripCasts(input);\n if (/^[a-z_][a-z0-9_$]*$/i.test(s)) return s;\n // A quoted identifier keeps its case: \"userId\".\n const quoted = s.match(/^\"([^\"]+)\"$/);\n return quoted ? quoted[1] : null;\n}\n\n/** `length(col)` and friends → the column they measure. */\nfunction asLengthOfColumn(input: string): string | null {\n const s = stripCasts(input);\n const match = s.match(/^([a-z_]+)\\s*\\((.*)\\)$/is);\n if (!match || !LENGTH_FUNCTIONS.has(match[1].toLowerCase())) return null;\n return asColumnName(match[2]);\n}\n\n/** A numeric literal, after casts are removed. Rejects anything else. */\nfunction asNumber(input: string): number | null {\n const s = stripCasts(input).replace(/^'(.*)'$/s, \"$1\");\n if (!/^[+-]?(\\d+\\.?\\d*|\\.\\d+)$/.test(s)) return null;\n const n = Number(s);\n return Number.isFinite(n) ? n : null;\n}\n\n/** A single-quoted string literal, with `''` unescaped. */\nfunction asStringLiteral(input: string): string | null {\n const s = stripCasts(input);\n if (!s.startsWith(\"'\") || !s.endsWith(\"'\") || s.length < 2) return null;\n const body = s.slice(1, -1);\n // Any unescaped quote inside means this was not one literal.\n if (/(^|[^'])'($|[^'])/.test(body)) return null;\n return body.replace(/''/g, \"'\");\n}\n\n/** The element list of `ARRAY[…]`, or of a parenthesised `IN (…)` list. */\nfunction asValueList(input: string): string[] | null {\n const s = stripCasts(input);\n const arrayMatch = s.match(/^ARRAY\\s*\\[(.*)\\]$/is);\n const body = arrayMatch ? arrayMatch[1] : (s.startsWith(\"(\") && s.endsWith(\")\") ? s.slice(1, -1) : null);\n if (body === null) return null;\n if (body.trim() === \"\") return null;\n\n const values: string[] = [];\n for (const element of splitTopLevel(body, \",\")) {\n const literal = asStringLiteral(element);\n if (literal !== null) {\n values.push(literal);\n continue;\n }\n const numeric = asNumber(element);\n if (numeric !== null) {\n values.push(String(numeric));\n continue;\n }\n return null; // A non-literal element means this is not an enumeration.\n }\n return values;\n}\n\n/** One understood comparison, already resolved to the column it constrains. */\ninterface CheckTerm {\n column: string;\n apply: (facts: ColumnCheckFacts) => void;\n}\n\n/**\n * Parses one conjunct. Returns null when the shape is not one of the few this\n * understands, which is the signal to abandon the whole constraint.\n */\nfunction parseTerm(rawTerm: string): CheckTerm | null {\n const term = unwrapParens(rawTerm);\n\n // ── Membership: `x = ANY (ARRAY[…])`, `x IN (…)` ──────────────────\n const anyMatch = term.match(/^(.*?)\\s*=\\s*ANY\\s*(\\(.*\\))$/is);\n const inMatch = anyMatch ? null : term.match(/^(.*?)\\s+IN\\s*(\\(.*\\))$/is);\n const membership = anyMatch ?? inMatch;\n if (membership) {\n const column = asColumnName(membership[1]);\n const values = asValueList(membership[2]);\n if (!column || !values || values.length === 0) return null;\n return {\n column,\n apply: (facts) => {\n // Two membership checks on one column mean both must hold.\n facts.enumValues = facts.enumValues\n ? facts.enumValues.filter((v) => values.includes(v))\n : values;\n }\n };\n }\n\n // ── Comparison: `x >= n`, `length(x) <= n`, `x = 'literal'` ───────\n // Longest operators first so `>=` is not read as `>`.\n for (const operator of [\">=\", \"<=\", \"<>\", \"!=\", \"=\", \">\", \"<\"] as const) {\n const parts = splitTopLevel(term, operator);\n if (parts.length !== 2) continue;\n const [rawLeft, rawRight] = parts;\n\n const lengthColumn = asLengthOfColumn(rawLeft);\n if (lengthColumn) {\n const bound = asNumber(rawRight);\n if (bound === null) return null;\n switch (operator) {\n case \">=\": return { column: lengthColumn, apply: (f) => { f.lengthMin = Math.max(f.lengthMin ?? bound, bound); } };\n case \">\": return { column: lengthColumn, apply: (f) => { f.lengthMin = Math.max(f.lengthMin ?? bound + 1, bound + 1); } };\n case \"<=\": return { column: lengthColumn, apply: (f) => { f.lengthMax = Math.min(f.lengthMax ?? bound, bound); } };\n case \"<\": return { column: lengthColumn, apply: (f) => { f.lengthMax = Math.min(f.lengthMax ?? bound - 1, bound - 1); } };\n case \"=\": return { column: lengthColumn, apply: (f) => { f.lengthMin = bound; f.lengthMax = bound; } };\n default: return null;\n }\n }\n\n const column = asColumnName(rawLeft);\n if (!column) continue;\n\n if (operator === \"=\") {\n const literal = asStringLiteral(rawRight);\n if (literal !== null) {\n return {\n column,\n apply: (facts) => {\n facts.enumValues = facts.enumValues\n ? facts.enumValues.filter((v) => v === literal)\n : [literal];\n }\n };\n }\n // `x = 5` pins a number. Expressed as both bounds rather than as a\n // one-value enum, which for a number would render as a dropdown.\n const pinned = asNumber(rawRight);\n if (pinned === null) return null;\n return { column, apply: (f) => { f.min = pinned; f.max = pinned; } };\n }\n\n const bound = asNumber(rawRight);\n if (bound === null) return null;\n switch (operator) {\n case \">=\": return { column, apply: (f) => { f.min = Math.max(f.min ?? bound, bound); } };\n case \">\": return { column, apply: (f) => { f.moreThan = Math.max(f.moreThan ?? bound, bound); } };\n case \"<=\": return { column, apply: (f) => { f.max = Math.min(f.max ?? bound, bound); } };\n case \"<\": return { column, apply: (f) => { f.lessThan = Math.min(f.lessThan ?? bound, bound); } };\n default: return null; // `<>` / `!=` exclude rather than bound.\n }\n }\n\n return null;\n}\n\n/**\n * Reads one constraint definition into per-column facts.\n *\n * Returns an empty map for anything not understood — including a constraint\n * that is understood but spans two columns (`start_date < end_date`), which no\n * per-property validation rule can express.\n */\nexport function parseCheckDefinition(definition: string): Map<string, ColumnCheckFacts> {\n const result = new Map<string, ColumnCheckFacts>();\n\n const body = definition\n .trim()\n .replace(/\\s+NOT\\s+VALID\\s*$/i, \"\")\n .replace(/^CHECK\\s*/i, \"\")\n .trim();\n if (body === definition.trim()) return result; // Not a CHECK definition.\n\n const expression = unwrapParens(body);\n if (!expression) return result;\n\n // Only conjunctions. A disjunction (`a OR b`) does not narrow a column: each\n // branch may allow what the other forbids, so no single bound follows.\n if (splitTopLevel(expression, \" OR \").length > 1) return result;\n\n const terms = splitTopLevel(expression, \" AND \");\n const parsed: CheckTerm[] = [];\n for (const term of terms) {\n const one = parseTerm(term);\n if (!one) return result; // One unreadable conjunct voids the constraint.\n parsed.push(one);\n }\n\n // A CHECK naming two columns constrains their *relationship*; keeping the\n // half that mentions one column would assert something the database does not.\n const columns = new Set(parsed.map((t) => t.column));\n if (columns.size !== 1) return result;\n\n const facts: ColumnCheckFacts = {};\n for (const term of parsed) term.apply(facts);\n if (Object.keys(facts).length === 0) return result;\n\n result.set(parsed[0].column, facts);\n return result;\n}\n\n/** Merges every readable CHECK in the schema into per-table, per-column facts. */\nexport function parseCheckConstraints(rows: CheckConstraintRow[]): CheckFactsByTable {\n const byTable: CheckFactsByTable = new Map();\n\n for (const row of rows) {\n const parsed = parseCheckDefinition(row.definition);\n if (parsed.size === 0) continue;\n\n let table = byTable.get(row.table_name);\n if (!table) {\n table = new Map();\n byTable.set(row.table_name, table);\n }\n\n for (const [column, facts] of parsed) {\n const existing = table.get(column);\n if (!existing) {\n table.set(column, facts);\n continue;\n }\n // Separate constraints on the same column all hold at once.\n if (facts.enumValues) {\n existing.enumValues = existing.enumValues\n ? existing.enumValues.filter((v) => facts.enumValues!.includes(v))\n : facts.enumValues;\n }\n if (facts.min !== undefined) existing.min = Math.max(existing.min ?? facts.min, facts.min);\n if (facts.max !== undefined) existing.max = Math.min(existing.max ?? facts.max, facts.max);\n if (facts.moreThan !== undefined) existing.moreThan = Math.max(existing.moreThan ?? facts.moreThan, facts.moreThan);\n if (facts.lessThan !== undefined) existing.lessThan = Math.min(existing.lessThan ?? facts.lessThan, facts.lessThan);\n if (facts.lengthMin !== undefined) existing.lengthMin = Math.max(existing.lengthMin ?? facts.lengthMin, facts.lengthMin);\n if (facts.lengthMax !== undefined) existing.lengthMax = Math.min(existing.lengthMax ?? facts.lengthMax, facts.lengthMax);\n }\n }\n\n return byTable;\n}\n","import chalk from \"chalk\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport pg from \"pg\";\nimport arg from \"arg\";\nimport * as dotenv from \"dotenv\";\nimport readline from \"readline\";\n\nimport {\n buildTablesMap,\n buildEnumMap,\n generateCollectionFile,\n generateIndexContent,\n mergeIndexContent,\n safeHostFromUrl\n} from \"./introspect-db-logic\";\nimport { detectCollectionBuilder } from \"./introspect-db-project\";\nimport { countRowsUpTo, isUsableFileName, quoteRelation, readSchemaMetadata } from \"./introspect-db-queries\";\nimport { classifyTables, lookupCandidates, LOOKUP_MAX_ROWS } from \"./introspect-db-structure\";\nimport { parseCheckConstraints } from \"./introspect-db-constraints\";\nimport { out, outWarn, outError } from \"../cli-output\";\n\nasync function main() {\n const args = arg(\n {\n \"--output\": String,\n \"--collections\": String,\n \"--force\": Boolean,\n \"--schema\": String,\n \"--data-inference\": Boolean,\n \"--no-data-inference\": Boolean,\n \"-o\": \"--output\",\n \"-c\": \"--collections\",\n \"-f\": \"--force\"\n },\n { permissive: true }\n );\n\n const cwd = process.cwd();\n const isBackendDir = path.basename(cwd) === \"backend\";\n const defaultOutDir = isBackendDir\n ? path.resolve(cwd, \"..\", \"config\", \"collections\")\n : path.resolve(cwd, \"config\", \"collections\");\n\n const outDir = args[\"--output\"] || args[\"--collections\"] || defaultOutDir;\n const force = args[\"--force\"] || false;\n const pgSchema = args[\"--schema\"] || \"public\";\n\n if (!fs.existsSync(outDir)) {\n fs.mkdirSync(outDir, { recursive: true });\n }\n\n // Load env\n const envPaths = [\n process.env.DOTENV_CONFIG_PATH,\n path.resolve(process.cwd(), \".env\"),\n path.resolve(process.cwd(), \"../.env\"),\n path.resolve(process.cwd(), \"../../.env\")\n ].filter(Boolean) as string[];\n\n for (const p of envPaths) {\n if (fs.existsSync(p)) {\n dotenv.config({ path: p, quiet: true });\n break;\n }\n }\n\n const databaseUrl = process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING;\n if (!databaseUrl) {\n outError(chalk.red(\"✗ DATABASE_URL is not set. Make sure your .env file is configured.\"));\n process.exit(1);\n }\n\n const client = new pg.Client({ connectionString: databaseUrl });\n\n try {\n await client.connect();\n } catch (err) {\n outError(chalk.red(`✗ Failed to connect to database: ${err instanceof Error ? err.message : String(err)}`));\n outError(chalk.gray(\" Check your DATABASE_URL and ensure the database is reachable.\"));\n process.exit(1);\n }\n\n // Log the host portion safely — handle URLs without \"@\"\n const hostPart = safeHostFromUrl(databaseUrl);\n out(chalk.gray(`Connected to database: ${hostPart}`));\n out(chalk.gray(`Introspecting schema '${pgSchema}'...`));\n\n try {\n const metadata = await readSchemaMetadata(client, pgSchema);\n const enumMap = buildEnumMap(metadata.enumValues);\n const tablesMap = buildTablesMap(metadata.tables, metadata.columns, metadata.pks, metadata.fks);\n const fks = metadata.fks;\n\n // Only tables that could structurally be a code list are counted, and\n // each count stops at the threshold — see `countRowsUpTo`. Introspection\n // runs against a database it does not own, so \"cheap on a table of any\n // size\" is a requirement, not an optimization.\n for (const table of lookupCandidates(metadata, tablesMap)) {\n try {\n metadata.rowCounts[table] = await countRowsUpTo(client, pgSchema, table, LOOKUP_MAX_ROWS);\n } catch (err) {\n // A table this run cannot read is simply not classified as a code\n // list; everything else about it still generates.\n out(chalk.gray(` (skipped row count for ${table}: ${err instanceof Error ? err.message : String(err)})`));\n }\n }\n\n const classifications = classifyTables(metadata, tablesMap);\n const checkFacts = parseCheckConstraints(metadata.checks);\n\n // Which builder the generated files may import — read from the manifests\n // above `outDir`, because a project without `@rebasepro/cms-types` cannot\n // resolve that import and has no `admin` block to write into either.\n const builder = detectCollectionBuilder(outDir);\n if (builder === \"annotation\") {\n outWarn(chalk.yellow(\n \"⚠ Neither @rebasepro/cms-types nor @rebasepro/common is declared above \" +\n `${outDir}.`\n ));\n outWarn(chalk.gray(\n \" Generating plain type annotations, which widen the property keys to `string`: \" +\n \"`propertiesOrder` and friends will not be checked. Add @rebasepro/common to the \" +\n \"config package to turn that checking on.\"\n ));\n }\n const joinTables = new Set(\n Array.from(classifications.values())\n .filter((c) => c.role === \"junction\")\n .map((c) => c.table)\n );\n\n const roleCount = (role: string) =>\n Array.from(classifications.values()).filter((c) => c.role === role).length;\n\n out(chalk.blue(`Found ${tablesMap.size} tables.`));\n out(chalk.gray(\n ` ${roleCount(\"entity\")} entities, ${joinTables.size} join tables (folded into relations), ` +\n `${roleCount(\"lookup\")} code lists, ${roleCount(\"owned-child\")} owned by another table (hidden from navigation).`\n ));\n\n let runDataInference = false;\n if (args[\"--no-data-inference\"]) {\n runDataInference = false;\n } else if (args[\"--data-inference\"] !== undefined) {\n runDataInference = args[\"--data-inference\"];\n } else if (!process.stdin.isTTY) {\n // No terminal to answer the question below (scaffolding scripts, CI,\n // `rebase init --introspect`) — asking would hang forever.\n out(chalk.gray(\"Skipping data inference (non-interactive run; pass --data-inference to enable).\"));\n } else {\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout\n });\n const answer = await new Promise<string>((resolve) => rl.question(chalk.yellow(\"? Do you want to run comprehensive data inference on sampled rows to auto-detect types, formats, constraints, and UI configurations? (y/N) \"), resolve));\n runDataInference = answer.trim().toLowerCase() === \"y\";\n rl.close();\n }\n\n if (runDataInference) {\n out(chalk.gray(\"Sampling database rows for data inference...\"));\n }\n\n // Generate Collections\n const generatedFiles: string[] = [];\n const skippedFiles: string[] = [];\n\n const tablesToProcess = Array.from(tablesMap.entries()).filter(([tableName]) => !joinTables.has(tableName));\n\n const BATCH_SIZE = 10;\n for (let i = 0; i < tablesToProcess.length; i += BATCH_SIZE) {\n const batch = tablesToProcess.slice(i, i + BATCH_SIZE);\n\n await Promise.all(batch.map(async ([tableName, meta]) => {\n // ── The name has to be usable as a filename ────────────────\n //\n // `tableName` came out of `pg_class` in a database this tool\n // did not create — adopting somebody else's database is the\n // whole point of introspection — and it is about to be joined\n // onto a path. Postgres will happily hold a table called\n // `../../../etc/cron.d/x`, and `path.join` resolves it: the\n // generator would write a TypeScript file wherever that\n // pointed, as whoever ran the command.\n //\n // Skipped rather than sanitised. A file named something other\n // than its table is a collection that does not round-trip, and\n // silently renaming it would be a worse surprise than one line\n // saying which table was left out.\n if (!isUsableFileName(tableName)) {\n outWarn(chalk.yellow(\n `⚠ Skipping table ${JSON.stringify(tableName)}: its name cannot be used as a ` +\n \"filename. Rename the table, or write this collection by hand.\"\n ));\n skippedFiles.push(tableName);\n return;\n }\n\n // ── File overwrite protection ──────────────────────────────\n const filePath = path.join(outDir, `${tableName}.ts`);\n if (fs.existsSync(filePath) && !force) {\n skippedFiles.push(tableName);\n return;\n }\n\n let sampleData: Record<string, unknown>[] | undefined = undefined;\n if (runDataInference) {\n try {\n // Quoted by `quoteRelation`, which doubles an embedded\n // quote. Wrapping a catalogue name in `\"` is not\n // escaping it — see that function.\n const { rows } = await client.query(`SELECT * FROM ${quoteRelation(pgSchema, tableName)} LIMIT 100`);\n sampleData = rows;\n } catch (err) {\n outError(chalk.yellow(`⚠ Failed to sample data for table ${tableName}: ${err instanceof Error ? err.message : String(err)}`));\n }\n }\n\n const fileContent = generateCollectionFile(\n tableName,\n meta,\n fks,\n joinTables,\n tablesMap,\n enumMap,\n sampleData,\n { metadata, classifications, checkFacts, builder }\n );\n\n fs.writeFileSync(filePath, fileContent, \"utf-8\");\n generatedFiles.push(tableName);\n out(chalk.green(` ✓ ${filePath}`));\n }));\n }\n\n // Generate index.ts (sorted alphabetically for deterministic output)\n if (generatedFiles.length > 0) {\n const indexPath = path.join(outDir, \"index.ts\");\n\n if (fs.existsSync(indexPath) && !force) {\n // Merge: read existing index, add new exports that don't already exist\n const existing = fs.readFileSync(indexPath, \"utf-8\");\n const merged = mergeIndexContent(existing, generatedFiles);\n fs.writeFileSync(indexPath, merged, \"utf-8\");\n } else {\n // --force replaces collections derived from the database, but the\n // directory can also hold hand-written ones with no table in the\n // introspected schema (the auth users collection lives in\n // \"rebase\"). The backend discovers the whole directory, so an\n // index listing only introspected tables would silently drop them\n // from the admin UI while the API still served them.\n const siblings = fs.readdirSync(outDir)\n .filter(f => f.endsWith(\".ts\") && f !== \"index.ts\")\n .map(f => f.replace(/\\.ts$/, \"\"));\n const allFiles = [...new Set([...generatedFiles, ...siblings])];\n const indexContent = generateIndexContent(allFiles);\n fs.writeFileSync(indexPath, indexContent, \"utf-8\");\n }\n out(chalk.green(` ✓ ${indexPath}`));\n }\n\n out(\"\");\n if (skippedFiles.length > 0) {\n out(chalk.yellow(`⚠ Skipped ${skippedFiles.length} existing file(s): ${skippedFiles.join(\", \")}`));\n out(chalk.gray(\" Use --force to overwrite existing files.\"));\n out(\"\");\n }\n out(chalk.bold.green(`✓ Introspected ${tablesMap.size} tables — generated ${generatedFiles.length} collection(s).`));\n out(chalk.gray(` Review the generated files in ${outDir} and customize properties as needed.`));\n out(\"\");\n\n } catch (e) {\n outError(chalk.red(`✗ Error introspecting database: ${e instanceof Error ? e.message : String(e)}`));\n process.exit(1);\n } finally {\n await client.end();\n }\n}\n\nmain().catch((err) => {\n outError(String(err));\n process.exit(1);\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"introspect-db.js","names":[],"sources":["../../src/schema/introspect-db-project.ts","../../src/schema/introspect-db-constraints.ts","../../src/schema/introspect-db.ts"],"sourcesContent":["/**\n * What the project being introspected into can actually import.\n *\n * The generator is pure — `introspect-db-logic.ts` reads no files — so the one\n * question it cannot answer for itself lives here: which `defineCollection`, if\n * any, will resolve from the directory the collections are written to.\n */\nimport fs from \"fs\";\nimport path from \"path\";\n\nimport { ADMIN_TYPES_PACKAGE, COMMON_PACKAGE, type CollectionBuilder } from \"./introspect-db-logic\";\n\n/** How far up to look before giving up, if no project root announces itself. */\nconst MAX_LEVELS = 8;\n\n/** Every dependency name a manifest declares, in any of the three fields. */\nfunction declaredDependencies(manifestPath: string): Set<string> {\n const names = new Set<string>();\n let parsed: unknown;\n try {\n parsed = JSON.parse(fs.readFileSync(manifestPath, \"utf-8\"));\n } catch {\n // A malformed or unreadable package.json is not this function's problem\n // to report — it answers \"can this import resolve\", and the answer is no.\n return names;\n }\n if (typeof parsed !== \"object\" || parsed === null) return names;\n const manifest = parsed as Record<string, unknown>;\n for (const field of [\"dependencies\", \"devDependencies\", \"peerDependencies\"]) {\n const block = manifest[field];\n if (typeof block === \"object\" && block !== null) {\n for (const name of Object.keys(block)) names.add(name);\n }\n }\n return names;\n}\n\n/**\n * Which `defineCollection` to generate against, for collections written to `outDir`.\n *\n * **The detection is the package manifests above the output directory**, unioned from\n * `outDir` up to and including the project root — the first ancestor holding a\n * `rebase.json`, or `MAX_LEVELS` up if there is none.\n *\n * That is the rule Node itself applies: a file in `config/collections` resolves a bare\n * specifier through `config/node_modules`, then `<project>/node_modules`, and so on up.\n * Reading the manifests along that same path answers the only question that matters —\n * *will this import resolve in the project I am writing into* — from the state on disk\n * at the moment of generation. The alternatives are all proxies for it: `rebase.json`'s\n * `apps` block says a CMS scaffold declared an admin app, and a `frontend/` directory\n * says one was scaffolded, but neither is what the compiler consults, and either can be\n * true of a project whose `config` package does not depend on `@rebasepro/cms-types`.\n *\n * Ambiguity resolves towards the admin panel: a project that declares both packages has\n * a panel, and `@rebasepro/cms-types` is the flavour that keeps the `admin` block.\n */\nexport function detectCollectionBuilder(outDir: string): CollectionBuilder {\n let dir = path.resolve(outDir);\n const declared = new Set<string>();\n\n for (let level = 0; level < MAX_LEVELS; level++) {\n const manifest = path.join(dir, \"package.json\");\n if (fs.existsSync(manifest)) {\n for (const name of declaredDependencies(manifest)) declared.add(name);\n }\n // The project root, and the last level worth reading: anything above it\n // belongs to whatever the project happens to be checked out inside.\n if (fs.existsSync(path.join(dir, \"rebase.json\"))) break;\n\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n\n if (declared.has(ADMIN_TYPES_PACKAGE)) return \"admin-types\";\n if (declared.has(COMMON_PACKAGE)) return \"common\";\n return \"annotation\";\n}\n","/**\n * Reading validation rules out of CHECK constraints.\n *\n * A CHECK constraint is the schema author stating a rule the database already\n * enforces. Introspection has never read them, so a generated form let the user\n * type a value the database was always going to reject — the rule was written\n * down, in the schema, and the UI asked anyway.\n *\n * Everything here parses the *normalized* text `pg_get_constraintdef` produces,\n * not what the author typed: Postgres re-renders the expression from its parse\n * tree, so `CHECK (price > 0)` on a `numeric` column always comes back as\n * `CHECK ((price > (0)::numeric))`. That normalization is what makes a\n * string-level parser tractable — the input is generated, and the shapes are\n * few.\n *\n * Constraints this cannot read are skipped in full. A partially-understood\n * constraint is worse than an unread one: it would produce validation that\n * *narrows* differently from the database, so a value the UI accepts still\n * fails on write, or — worse — a value the database allows is refused in the\n * form with no way to see why.\n *\n * Pure module: no I/O, no logging.\n */\nimport type { CheckConstraintRow } from \"./introspect-db-logic\";\n\n/** What a table's CHECK constraints say about one column. */\nexport interface ColumnCheckFacts {\n /** Allowed values, from `IN (…)` / `= ANY (ARRAY[…])` / `= 'literal'`. */\n enumValues?: string[];\n /** `x >= n` */\n min?: number;\n /** `x <= n` */\n max?: number;\n /** `x > n` */\n moreThan?: number;\n /** `x < n` */\n lessThan?: number;\n /** `length(x) >= n` */\n lengthMin?: number;\n /** `length(x) <= n` */\n lengthMax?: number;\n}\n\n/** Per-table, per-column facts. */\nexport type CheckFactsByTable = Map<string, Map<string, ColumnCheckFacts>>;\n\n/** The length functions whose argument is the column being constrained. */\nconst LENGTH_FUNCTIONS = new Set([\"length\", \"char_length\", \"character_length\", \"octet_length\"]);\n\n/**\n * Splits on a separator that appears at paren depth 0 and outside string\n * literals.\n *\n * Depth tracking is the whole job: `ARRAY['a', 'b']` contains a comma that must\n * not split the enclosing argument list, and `'it''s'` contains a quote that\n * must not end the literal.\n */\nfunction splitTopLevel(input: string, separator: string): string[] {\n const parts: string[] = [];\n let depth = 0;\n let inString = false;\n let current = \"\";\n\n for (let i = 0; i < input.length; i++) {\n const ch = input[i];\n\n if (inString) {\n current += ch;\n if (ch === \"'\") {\n // '' inside a literal is an escaped quote, not the end of it.\n if (input[i + 1] === \"'\") {\n current += input[++i];\n } else {\n inString = false;\n }\n }\n continue;\n }\n\n if (ch === \"'\") {\n inString = true;\n current += ch;\n continue;\n }\n if (ch === \"(\" || ch === \"[\") depth++;\n if (ch === \")\" || ch === \"]\") depth--;\n\n if (depth === 0 && input.startsWith(separator, i)) {\n // A word separator must not match inside an identifier: \"AND\" is a\n // separator in `a AND b` but not in `brand = 1`.\n const before = input[i - 1];\n const after = input[i + separator.length];\n const wordish = /[A-Za-z]/.test(separator[0]);\n const boundedOk = !wordish || ((before === undefined || !/[A-Za-z0-9_]/.test(before)) &&\n (after === undefined || !/[A-Za-z0-9_]/.test(after)));\n if (boundedOk) {\n parts.push(current);\n current = \"\";\n i += separator.length - 1;\n continue;\n }\n }\n\n current += ch;\n }\n\n parts.push(current);\n return parts.map((p) => p.trim()).filter((p) => p.length > 0);\n}\n\n/** Removes one layer of wrapping parentheses, repeatedly, when balanced. */\nexport function unwrapParens(input: string): string {\n let s = input.trim();\n while (s.startsWith(\"(\") && s.endsWith(\")\")) {\n let depth = 0;\n let balanced = true;\n let inString = false;\n for (let i = 0; i < s.length; i++) {\n const ch = s[i];\n if (inString) {\n if (ch === \"'\") {\n if (s[i + 1] === \"'\") i++;\n else inString = false;\n }\n continue;\n }\n if (ch === \"'\") { inString = true; continue; }\n if (ch === \"(\") depth++;\n else if (ch === \")\") {\n depth--;\n // The opening paren closed before the end, so the outer pair is\n // not a wrapper: `(a) AND (b)`.\n if (depth === 0 && i < s.length - 1) { balanced = false; break; }\n }\n }\n if (!balanced) break;\n s = s.slice(1, -1).trim();\n }\n return s;\n}\n\n/** Drops trailing `::type` casts, including array and quoted forms. */\nexport function stripCasts(input: string): string {\n let s = unwrapParens(input);\n let previous: string;\n do {\n previous = s;\n s = unwrapParens(\n s.replace(/::\\s*\"?[A-Za-z_][A-Za-z0-9_ ]*\"?(\\s*\\(\\s*\\d+\\s*(,\\s*\\d+\\s*)?\\))?(\\s*\\[\\s*\\])*\\s*$/, \"\").trim()\n );\n } while (s !== previous);\n return s;\n}\n\n/** A bare, unquoted column reference — anything else is an expression. */\nfunction asColumnName(input: string): string | null {\n const s = stripCasts(input);\n if (/^[a-z_][a-z0-9_$]*$/i.test(s)) return s;\n // A quoted identifier keeps its case: \"userId\".\n const quoted = s.match(/^\"([^\"]+)\"$/);\n return quoted ? quoted[1] : null;\n}\n\n/** `length(col)` and friends → the column they measure. */\nfunction asLengthOfColumn(input: string): string | null {\n const s = stripCasts(input);\n const match = s.match(/^([a-z_]+)\\s*\\((.*)\\)$/is);\n if (!match || !LENGTH_FUNCTIONS.has(match[1].toLowerCase())) return null;\n return asColumnName(match[2]);\n}\n\n/** A numeric literal, after casts are removed. Rejects anything else. */\nfunction asNumber(input: string): number | null {\n const s = stripCasts(input).replace(/^'(.*)'$/s, \"$1\");\n if (!/^[+-]?(\\d+\\.?\\d*|\\.\\d+)$/.test(s)) return null;\n const n = Number(s);\n return Number.isFinite(n) ? n : null;\n}\n\n/** A single-quoted string literal, with `''` unescaped. */\nfunction asStringLiteral(input: string): string | null {\n const s = stripCasts(input);\n if (!s.startsWith(\"'\") || !s.endsWith(\"'\") || s.length < 2) return null;\n const body = s.slice(1, -1);\n // Any unescaped quote inside means this was not one literal.\n if (/(^|[^'])'($|[^'])/.test(body)) return null;\n return body.replace(/''/g, \"'\");\n}\n\n/** The element list of `ARRAY[…]`, or of a parenthesised `IN (…)` list. */\nfunction asValueList(input: string): string[] | null {\n const s = stripCasts(input);\n const arrayMatch = s.match(/^ARRAY\\s*\\[(.*)\\]$/is);\n const body = arrayMatch ? arrayMatch[1] : (s.startsWith(\"(\") && s.endsWith(\")\") ? s.slice(1, -1) : null);\n if (body === null) return null;\n if (body.trim() === \"\") return null;\n\n const values: string[] = [];\n for (const element of splitTopLevel(body, \",\")) {\n const literal = asStringLiteral(element);\n if (literal !== null) {\n values.push(literal);\n continue;\n }\n const numeric = asNumber(element);\n if (numeric !== null) {\n values.push(String(numeric));\n continue;\n }\n return null; // A non-literal element means this is not an enumeration.\n }\n return values;\n}\n\n/** One understood comparison, already resolved to the column it constrains. */\ninterface CheckTerm {\n column: string;\n apply: (facts: ColumnCheckFacts) => void;\n}\n\n/**\n * Parses one conjunct. Returns null when the shape is not one of the few this\n * understands, which is the signal to abandon the whole constraint.\n */\nfunction parseTerm(rawTerm: string): CheckTerm | null {\n const term = unwrapParens(rawTerm);\n\n // ── Membership: `x = ANY (ARRAY[…])`, `x IN (…)` ──────────────────\n const anyMatch = term.match(/^(.*?)\\s*=\\s*ANY\\s*(\\(.*\\))$/is);\n const inMatch = anyMatch ? null : term.match(/^(.*?)\\s+IN\\s*(\\(.*\\))$/is);\n const membership = anyMatch ?? inMatch;\n if (membership) {\n const column = asColumnName(membership[1]);\n const values = asValueList(membership[2]);\n if (!column || !values || values.length === 0) return null;\n return {\n column,\n apply: (facts) => {\n // Two membership checks on one column mean both must hold.\n facts.enumValues = facts.enumValues\n ? facts.enumValues.filter((v) => values.includes(v))\n : values;\n }\n };\n }\n\n // ── Comparison: `x >= n`, `length(x) <= n`, `x = 'literal'` ───────\n // Longest operators first so `>=` is not read as `>`.\n for (const operator of [\">=\", \"<=\", \"<>\", \"!=\", \"=\", \">\", \"<\"] as const) {\n const parts = splitTopLevel(term, operator);\n if (parts.length !== 2) continue;\n const [rawLeft, rawRight] = parts;\n\n const lengthColumn = asLengthOfColumn(rawLeft);\n if (lengthColumn) {\n const bound = asNumber(rawRight);\n if (bound === null) return null;\n switch (operator) {\n case \">=\": return { column: lengthColumn, apply: (f) => { f.lengthMin = Math.max(f.lengthMin ?? bound, bound); } };\n case \">\": return { column: lengthColumn, apply: (f) => { f.lengthMin = Math.max(f.lengthMin ?? bound + 1, bound + 1); } };\n case \"<=\": return { column: lengthColumn, apply: (f) => { f.lengthMax = Math.min(f.lengthMax ?? bound, bound); } };\n case \"<\": return { column: lengthColumn, apply: (f) => { f.lengthMax = Math.min(f.lengthMax ?? bound - 1, bound - 1); } };\n case \"=\": return { column: lengthColumn, apply: (f) => { f.lengthMin = bound; f.lengthMax = bound; } };\n default: return null;\n }\n }\n\n const column = asColumnName(rawLeft);\n if (!column) continue;\n\n if (operator === \"=\") {\n const literal = asStringLiteral(rawRight);\n if (literal !== null) {\n return {\n column,\n apply: (facts) => {\n facts.enumValues = facts.enumValues\n ? facts.enumValues.filter((v) => v === literal)\n : [literal];\n }\n };\n }\n // `x = 5` pins a number. Expressed as both bounds rather than as a\n // one-value enum, which for a number would render as a dropdown.\n const pinned = asNumber(rawRight);\n if (pinned === null) return null;\n return { column, apply: (f) => { f.min = pinned; f.max = pinned; } };\n }\n\n const bound = asNumber(rawRight);\n if (bound === null) return null;\n switch (operator) {\n case \">=\": return { column, apply: (f) => { f.min = Math.max(f.min ?? bound, bound); } };\n case \">\": return { column, apply: (f) => { f.moreThan = Math.max(f.moreThan ?? bound, bound); } };\n case \"<=\": return { column, apply: (f) => { f.max = Math.min(f.max ?? bound, bound); } };\n case \"<\": return { column, apply: (f) => { f.lessThan = Math.min(f.lessThan ?? bound, bound); } };\n default: return null; // `<>` / `!=` exclude rather than bound.\n }\n }\n\n return null;\n}\n\n/**\n * Reads one constraint definition into per-column facts.\n *\n * Returns an empty map for anything not understood — including a constraint\n * that is understood but spans two columns (`start_date < end_date`), which no\n * per-property validation rule can express.\n */\nexport function parseCheckDefinition(definition: string): Map<string, ColumnCheckFacts> {\n const result = new Map<string, ColumnCheckFacts>();\n\n const body = definition\n .trim()\n .replace(/\\s+NOT\\s+VALID\\s*$/i, \"\")\n .replace(/^CHECK\\s*/i, \"\")\n .trim();\n if (body === definition.trim()) return result; // Not a CHECK definition.\n\n const expression = unwrapParens(body);\n if (!expression) return result;\n\n // Only conjunctions. A disjunction (`a OR b`) does not narrow a column: each\n // branch may allow what the other forbids, so no single bound follows.\n if (splitTopLevel(expression, \" OR \").length > 1) return result;\n\n const terms = splitTopLevel(expression, \" AND \");\n const parsed: CheckTerm[] = [];\n for (const term of terms) {\n const one = parseTerm(term);\n if (!one) return result; // One unreadable conjunct voids the constraint.\n parsed.push(one);\n }\n\n // A CHECK naming two columns constrains their *relationship*; keeping the\n // half that mentions one column would assert something the database does not.\n const columns = new Set(parsed.map((t) => t.column));\n if (columns.size !== 1) return result;\n\n const facts: ColumnCheckFacts = {};\n for (const term of parsed) term.apply(facts);\n if (Object.keys(facts).length === 0) return result;\n\n result.set(parsed[0].column, facts);\n return result;\n}\n\n/** Merges every readable CHECK in the schema into per-table, per-column facts. */\nexport function parseCheckConstraints(rows: CheckConstraintRow[]): CheckFactsByTable {\n const byTable: CheckFactsByTable = new Map();\n\n for (const row of rows) {\n const parsed = parseCheckDefinition(row.definition);\n if (parsed.size === 0) continue;\n\n let table = byTable.get(row.table_name);\n if (!table) {\n table = new Map();\n byTable.set(row.table_name, table);\n }\n\n for (const [column, facts] of parsed) {\n const existing = table.get(column);\n if (!existing) {\n table.set(column, facts);\n continue;\n }\n // Separate constraints on the same column all hold at once.\n if (facts.enumValues) {\n existing.enumValues = existing.enumValues\n ? existing.enumValues.filter((v) => facts.enumValues!.includes(v))\n : facts.enumValues;\n }\n if (facts.min !== undefined) existing.min = Math.max(existing.min ?? facts.min, facts.min);\n if (facts.max !== undefined) existing.max = Math.min(existing.max ?? facts.max, facts.max);\n if (facts.moreThan !== undefined) existing.moreThan = Math.max(existing.moreThan ?? facts.moreThan, facts.moreThan);\n if (facts.lessThan !== undefined) existing.lessThan = Math.min(existing.lessThan ?? facts.lessThan, facts.lessThan);\n if (facts.lengthMin !== undefined) existing.lengthMin = Math.max(existing.lengthMin ?? facts.lengthMin, facts.lengthMin);\n if (facts.lengthMax !== undefined) existing.lengthMax = Math.min(existing.lengthMax ?? facts.lengthMax, facts.lengthMax);\n }\n }\n\n return byTable;\n}\n","import chalk from \"chalk\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport pg from \"pg\";\nimport arg from \"arg\";\nimport * as dotenv from \"dotenv\";\nimport readline from \"readline\";\n\nimport {\n buildTablesMap,\n buildEnumMap,\n generateCollectionFile,\n generateIndexContent,\n mergeIndexContent,\n safeHostFromUrl\n} from \"./introspect-db-logic\";\nimport { detectCollectionBuilder } from \"./introspect-db-project\";\nimport { countRowsUpTo, isUsableFileName, quoteRelation, readSchemaMetadata } from \"./introspect-db-queries\";\nimport { classifyTables, lookupCandidates, LOOKUP_MAX_ROWS } from \"./introspect-db-structure\";\nimport { parseCheckConstraints } from \"./introspect-db-constraints\";\nimport { out, outWarn, outError } from \"../cli-output\";\n\nasync function main() {\n const args = arg(\n {\n \"--output\": String,\n \"--collections\": String,\n \"--force\": Boolean,\n \"--schema\": String,\n \"--data-inference\": Boolean,\n \"--no-data-inference\": Boolean,\n \"-o\": \"--output\",\n \"-c\": \"--collections\",\n \"-f\": \"--force\"\n },\n { permissive: true }\n );\n\n const cwd = process.cwd();\n const isBackendDir = path.basename(cwd) === \"backend\";\n const defaultOutDir = isBackendDir\n ? path.resolve(cwd, \"..\", \"config\", \"collections\")\n : path.resolve(cwd, \"config\", \"collections\");\n\n const outDir = args[\"--output\"] || args[\"--collections\"] || defaultOutDir;\n const force = args[\"--force\"] || false;\n const pgSchema = args[\"--schema\"] || \"public\";\n\n if (!fs.existsSync(outDir)) {\n fs.mkdirSync(outDir, { recursive: true });\n }\n\n // Load env\n const envPaths = [\n process.env.DOTENV_CONFIG_PATH,\n path.resolve(process.cwd(), \".env\"),\n path.resolve(process.cwd(), \"../.env\"),\n path.resolve(process.cwd(), \"../../.env\")\n ].filter(Boolean) as string[];\n\n for (const p of envPaths) {\n if (fs.existsSync(p)) {\n dotenv.config({ path: p, quiet: true });\n break;\n }\n }\n\n const databaseUrl = process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING;\n if (!databaseUrl) {\n outError(chalk.red(\"✗ DATABASE_URL is not set. Make sure your .env file is configured.\"));\n process.exit(1);\n }\n\n const client = new pg.Client({ connectionString: databaseUrl });\n\n try {\n await client.connect();\n } catch (err) {\n outError(chalk.red(`✗ Failed to connect to database: ${err instanceof Error ? err.message : String(err)}`));\n outError(chalk.gray(\" Check your DATABASE_URL and ensure the database is reachable.\"));\n process.exit(1);\n }\n\n // Log the host portion safely — handle URLs without \"@\"\n const hostPart = safeHostFromUrl(databaseUrl);\n out(chalk.gray(`Connected to database: ${hostPart}`));\n out(chalk.gray(`Introspecting schema '${pgSchema}'...`));\n\n try {\n const metadata = await readSchemaMetadata(client, pgSchema);\n const enumMap = buildEnumMap(metadata.enumValues);\n const tablesMap = buildTablesMap(metadata.tables, metadata.columns, metadata.pks, metadata.fks);\n const fks = metadata.fks;\n\n // Only tables that could structurally be a code list are counted, and\n // each count stops at the threshold — see `countRowsUpTo`. Introspection\n // runs against a database it does not own, so \"cheap on a table of any\n // size\" is a requirement, not an optimization.\n for (const table of lookupCandidates(metadata, tablesMap)) {\n try {\n metadata.rowCounts[table] = await countRowsUpTo(client, pgSchema, table, LOOKUP_MAX_ROWS);\n } catch (err) {\n // A table this run cannot read is simply not classified as a code\n // list; everything else about it still generates.\n out(chalk.gray(` (skipped row count for ${table}: ${err instanceof Error ? err.message : String(err)})`));\n }\n }\n\n const classifications = classifyTables(metadata, tablesMap);\n const checkFacts = parseCheckConstraints(metadata.checks);\n\n // Which builder the generated files may import — read from the manifests\n // above `outDir`, because a project without `@rebasepro/cms-types` cannot\n // resolve that import and has no `admin` block to write into either.\n const builder = detectCollectionBuilder(outDir);\n if (builder === \"annotation\") {\n outWarn(chalk.yellow(\n \"⚠ Neither @rebasepro/cms-types nor @rebasepro/common is declared above \" +\n `${outDir}.`\n ));\n outWarn(chalk.gray(\n \" Generating plain type annotations, which widen the property keys to `string`: \" +\n \"`propertiesOrder` and friends will not be checked. Add @rebasepro/common to the \" +\n \"config package to turn that checking on.\"\n ));\n }\n const joinTables = new Set(\n Array.from(classifications.values())\n .filter((c) => c.role === \"junction\")\n .map((c) => c.table)\n );\n\n const roleCount = (role: string) =>\n Array.from(classifications.values()).filter((c) => c.role === role).length;\n\n out(chalk.blue(`Found ${tablesMap.size} tables.`));\n out(chalk.gray(\n ` ${roleCount(\"entity\")} entities, ${joinTables.size} join tables (folded into relations), ` +\n `${roleCount(\"lookup\")} code lists, ${roleCount(\"owned-child\")} owned by another table (hidden from navigation).`\n ));\n\n let runDataInference = false;\n if (args[\"--no-data-inference\"]) {\n runDataInference = false;\n } else if (args[\"--data-inference\"] !== undefined) {\n runDataInference = args[\"--data-inference\"];\n } else if (!process.stdin.isTTY) {\n // No terminal to answer the question below (scaffolding scripts, CI,\n // `rebase init --introspect`) — asking would hang forever.\n out(chalk.gray(\"Skipping data inference (non-interactive run; pass --data-inference to enable).\"));\n } else {\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout\n });\n const answer = await new Promise<string>((resolve) => rl.question(chalk.yellow(\"? Do you want to run comprehensive data inference on sampled rows to auto-detect types, formats, constraints, and UI configurations? (y/N) \"), resolve));\n runDataInference = answer.trim().toLowerCase() === \"y\";\n rl.close();\n }\n\n if (runDataInference) {\n out(chalk.gray(\"Sampling database rows for data inference...\"));\n }\n\n // Generate Collections\n const generatedFiles: string[] = [];\n const skippedFiles: string[] = [];\n\n const tablesToProcess = Array.from(tablesMap.entries()).filter(([tableName]) => !joinTables.has(tableName));\n\n const BATCH_SIZE = 10;\n for (let i = 0; i < tablesToProcess.length; i += BATCH_SIZE) {\n const batch = tablesToProcess.slice(i, i + BATCH_SIZE);\n\n await Promise.all(batch.map(async ([tableName, meta]) => {\n // ── The name has to be usable as a filename ────────────────\n //\n // `tableName` came out of `pg_class` in a database this tool\n // did not create — adopting somebody else's database is the\n // whole point of introspection — and it is about to be joined\n // onto a path. Postgres will happily hold a table called\n // `../../../etc/cron.d/x`, and `path.join` resolves it: the\n // generator would write a TypeScript file wherever that\n // pointed, as whoever ran the command.\n //\n // Skipped rather than sanitised. A file named something other\n // than its table is a collection that does not round-trip, and\n // silently renaming it would be a worse surprise than one line\n // saying which table was left out.\n if (!isUsableFileName(tableName)) {\n outWarn(chalk.yellow(\n `⚠ Skipping table ${JSON.stringify(tableName)}: its name cannot be used as a ` +\n \"filename. Rename the table, or write this collection by hand.\"\n ));\n skippedFiles.push(tableName);\n return;\n }\n\n // ── File overwrite protection ──────────────────────────────\n const filePath = path.join(outDir, `${tableName}.ts`);\n if (fs.existsSync(filePath) && !force) {\n skippedFiles.push(tableName);\n return;\n }\n\n let sampleData: Record<string, unknown>[] | undefined = undefined;\n if (runDataInference) {\n try {\n // Quoted by `quoteRelation`, which doubles an embedded\n // quote. Wrapping a catalogue name in `\"` is not\n // escaping it — see that function.\n const { rows } = await client.query(`SELECT * FROM ${quoteRelation(pgSchema, tableName)} LIMIT 100`);\n sampleData = rows;\n } catch (err) {\n outError(chalk.yellow(`⚠ Failed to sample data for table ${tableName}: ${err instanceof Error ? err.message : String(err)}`));\n }\n }\n\n const fileContent = generateCollectionFile(\n tableName,\n meta,\n fks,\n joinTables,\n tablesMap,\n enumMap,\n sampleData,\n { metadata, classifications, checkFacts, builder }\n );\n\n fs.writeFileSync(filePath, fileContent, \"utf-8\");\n generatedFiles.push(tableName);\n out(chalk.green(` ✓ ${filePath}`));\n }));\n }\n\n // Generate index.ts (sorted alphabetically for deterministic output)\n if (generatedFiles.length > 0) {\n const indexPath = path.join(outDir, \"index.ts\");\n\n if (fs.existsSync(indexPath) && !force) {\n // Merge: read existing index, add new exports that don't already exist\n const existing = fs.readFileSync(indexPath, \"utf-8\");\n const merged = mergeIndexContent(existing, generatedFiles);\n fs.writeFileSync(indexPath, merged, \"utf-8\");\n } else {\n // --force replaces collections derived from the database, but the\n // directory can also hold hand-written ones with no table in the\n // introspected schema (the auth users collection lives in\n // \"rebase\"). The backend discovers the whole directory, so an\n // index listing only introspected tables would silently drop them\n // from the admin UI while the API still served them.\n const siblings = fs.readdirSync(outDir)\n .filter(f => f.endsWith(\".ts\") && f !== \"index.ts\")\n .map(f => f.replace(/\\.ts$/, \"\"));\n const allFiles = [...new Set([...generatedFiles, ...siblings])];\n const indexContent = generateIndexContent(allFiles);\n fs.writeFileSync(indexPath, indexContent, \"utf-8\");\n }\n out(chalk.green(` ✓ ${indexPath}`));\n }\n\n out(\"\");\n if (skippedFiles.length > 0) {\n out(chalk.yellow(`⚠ Skipped ${skippedFiles.length} existing file(s): ${skippedFiles.join(\", \")}`));\n out(chalk.gray(\" Use --force to overwrite existing files.\"));\n out(\"\");\n }\n out(chalk.bold.green(`✓ Introspected ${tablesMap.size} tables — generated ${generatedFiles.length} collection(s).`));\n out(chalk.gray(` Review the generated files in ${outDir} and customize properties as needed.`));\n out(\"\");\n\n } catch (e) {\n outError(chalk.red(`✗ Error introspecting database: ${e instanceof Error ? e.message : String(e)}`));\n process.exit(1);\n } finally {\n await client.end();\n }\n}\n\nmain().catch((err) => {\n outError(String(err));\n process.exit(1);\n});\n"],"mappings":"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"}
|
|
@@ -120,11 +120,12 @@ export interface OrphanedRequiredColumn {
|
|
|
120
120
|
export interface WithheldConstraint {
|
|
121
121
|
/** `schema.table.column`. */
|
|
122
122
|
target: string;
|
|
123
|
-
kind: "not-null";
|
|
123
|
+
kind: "not-null" | "unique";
|
|
124
124
|
/**
|
|
125
125
|
* Why, in a sentence that names the obstacle rather than the rule. The
|
|
126
126
|
* reader is looking at a column that is nullable when they asked for
|
|
127
|
-
* required,
|
|
127
|
+
* required, or accepts duplicates when they asked for unique, and needs to
|
|
128
|
+
* know what to do about it.
|
|
128
129
|
*/
|
|
129
130
|
reason: string;
|
|
130
131
|
/** What would make it applicable. */
|
|
@@ -55,9 +55,16 @@ export declare const defaultBelongsToOnDelete: (required: boolean | undefined) =
|
|
|
55
55
|
* The type a column pointing at this collection's primary key must have — a
|
|
56
56
|
* junction endpoint, a `belongsTo` foreign key, a `reference`.
|
|
57
57
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
58
|
+
* The key's own column type, from the same functions that typed the key: a
|
|
59
|
+
* foreign key only holds between compatible types, so any second reading of
|
|
60
|
+
* the key is a constraint waiting to fail. This used to look at the id
|
|
61
|
+
* strategy alone, so `id: { isId: true, columnType: "uuid" }` was referenced by
|
|
62
|
+
* TEXT columns (42804 — the constraint was never created) and a `bigint` key by
|
|
63
|
+
* INTEGER ones, which overflow at 2^31.
|
|
64
|
+
*
|
|
65
|
+
* One exception: a `serial` key owns a sequence, and a column pointing at it is
|
|
66
|
+
* the plain integer of the same width — `SERIAL` there would give every
|
|
67
|
+
* referencing column a sequence of its own.
|
|
61
68
|
*/
|
|
62
69
|
export declare const primaryKeyPgType: (collection: CollectionConfig) => PgType;
|
|
63
70
|
/**
|
|
@@ -288,7 +288,12 @@ export interface EnumPlan {
|
|
|
288
288
|
schema: string;
|
|
289
289
|
/** Type name, unqualified: `<table>_<column>`. A frozen derived name. */
|
|
290
290
|
name: string;
|
|
291
|
-
/**
|
|
291
|
+
/**
|
|
292
|
+
* `schema.name`, with the name cut to the 63 bytes Postgres keeps — the key
|
|
293
|
+
* `readExistingSchema` returns. `name` itself is emitted in full, and
|
|
294
|
+
* Postgres truncates it the same way; comparing the full name against the
|
|
295
|
+
* catalogue found a long one missing on every boot.
|
|
296
|
+
*/
|
|
292
297
|
qualified: string;
|
|
293
298
|
/**
|
|
294
299
|
* The schema the collection *declared*, before the `public` fallback.
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* `auditor` and `auditoria` to `auditori` — *different lexemes* — so a query
|
|
44
44
|
* typed without accents misses every row that carries them.
|
|
45
45
|
*/
|
|
46
|
-
import { CollectionConfig, SearchConfig, SearchWeight } from "@rebasepro/types";
|
|
46
|
+
import { CollectionConfig, SearchConfig, SearchMode, SearchWeight } from "@rebasepro/types";
|
|
47
47
|
/**
|
|
48
48
|
* Names of the helper functions. Frozen: they are recorded in the stored
|
|
49
49
|
* generation expression of every search column ever created, so renaming one
|
|
@@ -67,6 +67,20 @@ export interface ResolvedSearchField {
|
|
|
67
67
|
sql: string;
|
|
68
68
|
/** The plain-text term this field contributes, for the fuzzy column. */
|
|
69
69
|
textSql: string;
|
|
70
|
+
/**
|
|
71
|
+
* The same text with accents folded **unconditionally**, for the substring
|
|
72
|
+
* half of {@link SearchMode} `"hybrid"`.
|
|
73
|
+
*
|
|
74
|
+
* Separate from {@link textSql} rather than replacing it, and that
|
|
75
|
+
* separation is the whole migration story for `mode`. `textSql` feeds the
|
|
76
|
+
* *stored* generated columns, so changing it changes their generation
|
|
77
|
+
* expression, which changes the fingerprint, which makes the next boot
|
|
78
|
+
* refuse (see `searchStampGuards`). This one is only ever interpolated into
|
|
79
|
+
* a WHERE clause, so a collection can switch to `"hybrid"` — and gain
|
|
80
|
+
* accent folding on the substring half — without rebuilding a column or
|
|
81
|
+
* taking an ACCESS EXCLUSIVE lock.
|
|
82
|
+
*/
|
|
83
|
+
foldedTextSql: string;
|
|
70
84
|
}
|
|
71
85
|
/** Everything the generators need to render one collection's search column. */
|
|
72
86
|
export interface SearchColumnSpec {
|
|
@@ -76,6 +90,12 @@ export interface SearchColumnSpec {
|
|
|
76
90
|
column: string;
|
|
77
91
|
language: string;
|
|
78
92
|
unaccent: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* How the query side matches. Deliberately absent from
|
|
95
|
+
* {@link SearchColumnSpec.expression} and from every fingerprint: it
|
|
96
|
+
* describes the WHERE clause, not the column.
|
|
97
|
+
*/
|
|
98
|
+
mode: SearchMode;
|
|
79
99
|
fields: ResolvedSearchField[];
|
|
80
100
|
/** Body of `GENERATED ALWAYS AS ( … ) STORED` for the tsvector column. */
|
|
81
101
|
expression: string;
|
|
@@ -7,6 +7,7 @@ import { DrizzleClient } from "../interfaces.js";
|
|
|
7
7
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry.js";
|
|
8
8
|
import { type NestedPathHop } from "./nested-path.js";
|
|
9
9
|
import { type WithDeleted } from "./soft-delete.js";
|
|
10
|
+
import { type ReadCallContextProvider } from "./read-scope.js";
|
|
10
11
|
/**
|
|
11
12
|
* Service for handling all row read operations.
|
|
12
13
|
* Handles fetching, searching, counting, and filtering rows.
|
|
@@ -39,8 +40,22 @@ export declare function numericAggregateAliases(aggregates: {
|
|
|
39
40
|
export declare class FetchService {
|
|
40
41
|
private db;
|
|
41
42
|
private registry;
|
|
43
|
+
/**
|
|
44
|
+
* How this service's reads reach the identity their `beforeQuery` hooks
|
|
45
|
+
* run as. Passed by the driver that constructed it; absent only in a
|
|
46
|
+
* test, where a collection declaring the hook is refused rather than
|
|
47
|
+
* read unnarrowed. See `read-scope.ts`.
|
|
48
|
+
*/
|
|
49
|
+
private callContext?;
|
|
42
50
|
private relationService;
|
|
43
|
-
constructor(db: DrizzleClient, registry: PostgresCollectionRegistry
|
|
51
|
+
constructor(db: DrizzleClient, registry: PostgresCollectionRegistry,
|
|
52
|
+
/**
|
|
53
|
+
* How this service's reads reach the identity their `beforeQuery` hooks
|
|
54
|
+
* run as. Passed by the driver that constructed it; absent only in a
|
|
55
|
+
* test, where a collection declaring the hook is refused rather than
|
|
56
|
+
* read unnarrowed. See `read-scope.ts`.
|
|
57
|
+
*/
|
|
58
|
+
callContext?: ReadCallContextProvider | undefined);
|
|
44
59
|
/**
|
|
45
60
|
* Get the relational query builder for a given table name.
|
|
46
61
|
* Safely narrows the DrizzleClient union type to access db.query[tableName].
|
|
@@ -63,6 +78,31 @@ export declare class FetchService {
|
|
|
63
78
|
* compile stays unresolvable and so fails closed.
|
|
64
79
|
*/
|
|
65
80
|
private filterContext;
|
|
81
|
+
/**
|
|
82
|
+
* The conditions this collection's `beforeQuery` hooks ask to add, for a
|
|
83
|
+
* read that is about to be compiled.
|
|
84
|
+
*
|
|
85
|
+
* Resolved here, from the registry this service already holds, rather than
|
|
86
|
+
* handed in by a caller — so a read path that builds a WHERE cannot serve
|
|
87
|
+
* one of these collections without the hook. `[]` when nothing is declared,
|
|
88
|
+
* which leaves the compiled SQL byte-identical.
|
|
89
|
+
*
|
|
90
|
+
* Every caller pushes the result onto the same `allConditions` array its
|
|
91
|
+
* own filter goes into, and every one of those arrays is AND-ed. That is
|
|
92
|
+
* what keeps a hook additive: it is not trusted to narrow, it is only ever
|
|
93
|
+
* given a way to.
|
|
94
|
+
*/
|
|
95
|
+
private narrowRead;
|
|
96
|
+
/** {@link narrowRead}, pre-combined, for the reads that hold one `SQL`. */
|
|
97
|
+
private narrowReadCondition;
|
|
98
|
+
/**
|
|
99
|
+
* The read the hooks are shown, from the options a read path was given.
|
|
100
|
+
*
|
|
101
|
+
* Transport is left out on purpose — a `databaseId`, an `include` tree, a
|
|
102
|
+
* `withDeleted` flag are not part of "which rows". What is in is what a
|
|
103
|
+
* scope might reasonably branch on.
|
|
104
|
+
*/
|
|
105
|
+
private static describeRead;
|
|
66
106
|
/**
|
|
67
107
|
* The table column this collection's rows are keyed by, or `undefined`.
|
|
68
108
|
*
|
|
@@ -101,6 +141,11 @@ export declare class FetchService {
|
|
|
101
141
|
* rows in arbitrary order, and paging over that repeats and skips rows.
|
|
102
142
|
*/
|
|
103
143
|
static readonly SCORE_FIELD = "_score";
|
|
144
|
+
/**
|
|
145
|
+
* The distance a vector search attaches to every row it serves, and to no
|
|
146
|
+
* other. Like `_score` it is computed per query and stored nowhere.
|
|
147
|
+
*/
|
|
148
|
+
static readonly DISTANCE_FIELD = "_distance";
|
|
104
149
|
private resolveOrderTarget;
|
|
105
150
|
/**
|
|
106
151
|
* The aggregate a sort key names, as an expression, or `undefined` if the
|
|
@@ -141,15 +186,17 @@ export declare class FetchService {
|
|
|
141
186
|
*/
|
|
142
187
|
private static nullsLast;
|
|
143
188
|
/**
|
|
144
|
-
* The full `ORDER BY`: the caller's keys, then the
|
|
189
|
+
* The full `ORDER BY`: the caller's keys, then the primary key.
|
|
145
190
|
*
|
|
146
|
-
* The
|
|
147
|
-
* what makes the ordering *total*, and a cursor over
|
|
148
|
-
* repeats and skips rows among the ties. Every keyset
|
|
149
|
-
* {@link buildCursorConditions} ends on the same
|
|
150
|
-
* to agree: they did not, and an ascending sort
|
|
151
|
-
* `ORDER BY … , id DESC`, so rows sharing a
|
|
152
|
-
* every page after the first.
|
|
191
|
+
* The key is always last and always descending, every column of it. It is
|
|
192
|
+
* not decoration — it is what makes the ordering *total*, and a cursor over
|
|
193
|
+
* a non-total order repeats and skips rows among the ties. Every keyset
|
|
194
|
+
* comparison built by {@link buildCursorConditions} ends on the same key
|
|
195
|
+
* `DESC`, and the two have to agree: they did not, and an ascending sort
|
|
196
|
+
* paged with `id >` against an `ORDER BY … , id DESC`, so rows sharing a
|
|
197
|
+
* sort value were dropped from every page after the first. Nor is the
|
|
198
|
+
* first column of a composite key enough: it ties every row that shares
|
|
199
|
+
* it, which is exactly the rows a cursor then skipped.
|
|
153
200
|
*
|
|
154
201
|
* Where the NULLs go is written out rather than inherited. Postgres already
|
|
155
202
|
* defaults to `NULLS LAST` ascending and `NULLS FIRST` descending, so this
|
|
@@ -162,22 +209,6 @@ export declare class FetchService {
|
|
|
162
209
|
*/
|
|
163
210
|
private buildOrderExpressions;
|
|
164
211
|
private resolveOrderByField;
|
|
165
|
-
/**
|
|
166
|
-
* Build the `with` config for Drizzle's relational query API.
|
|
167
|
-
* Converts collection relations to a Drizzle-compatible `with` object.
|
|
168
|
-
*
|
|
169
|
-
* When `include` is provided, only those relations are loaded.
|
|
170
|
-
* When `include` is absent, ALL relations are loaded (the admin path).
|
|
171
|
-
*
|
|
172
|
-
* Automatically detects many-to-many junction tables and nests
|
|
173
|
-
* the target relation so actual row data is returned.
|
|
174
|
-
*/
|
|
175
|
-
private buildWithConfig;
|
|
176
|
-
/**
|
|
177
|
-
* Get the Drizzle relation name on the junction table that points to the actual target row.
|
|
178
|
-
* For example, for posts_tags junction, this returns "tag_id" (the relation pointing to tags).
|
|
179
|
-
*/
|
|
180
|
-
private getJunctionTargetRelationName;
|
|
181
212
|
/**
|
|
182
213
|
* The relations one level of an include tree names, resolved.
|
|
183
214
|
*
|
|
@@ -263,11 +294,21 @@ export declare class FetchService {
|
|
|
263
294
|
*/
|
|
264
295
|
cursorFor(collectionPath: string, row: Record<string, unknown>, orderBy?: OrderByTuple[]): string | undefined;
|
|
265
296
|
/**
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
297
|
+
* Every relation of one row, as the refs the admin renders, attached in
|
|
298
|
+
* place.
|
|
299
|
+
*
|
|
300
|
+
* Loaded by the batched loaders an `include` takes, so a target reached from
|
|
301
|
+
* this row is read as the caller may see it: its `beforeQuery` scope and its
|
|
302
|
+
* soft delete applied, its unreadable fields stripped. Populated through
|
|
303
|
+
* drizzle's relational `with`, the single get knew neither, and a
|
|
304
|
+
* tenant-scoped user fetching an owner was handed another tenant's docs —
|
|
305
|
+
* and the trashed ones — under a relation `?include=` answered correctly.
|
|
306
|
+
*
|
|
307
|
+
* A to-many relation that reaches nothing is `[]`; a to-one is left off. A
|
|
308
|
+
* relation that fails for a reason other than the database is logged and
|
|
309
|
+
* left off, as on the include path, rather than failing the whole row.
|
|
269
310
|
*/
|
|
270
|
-
private
|
|
311
|
+
private loadRelationRefs;
|
|
271
312
|
/**
|
|
272
313
|
* Extract cursor pagination conditions from startAfter options.
|
|
273
314
|
*
|
|
@@ -277,6 +318,46 @@ export declare class FetchService {
|
|
|
277
318
|
* several it nests, each key's tie handing the decision to the next.
|
|
278
319
|
*/
|
|
279
320
|
private buildCursorConditions;
|
|
321
|
+
/** The table's primary key columns, in key order. */
|
|
322
|
+
private static keyColumns;
|
|
323
|
+
/**
|
|
324
|
+
* The cursor row's key, one value per key column — or `undefined` for a
|
|
325
|
+
* cursor that names no row.
|
|
326
|
+
*
|
|
327
|
+
* A cursor carries the row's address, which for a composite key is every
|
|
328
|
+
* part joined (see {@link cursorFor}). One that carries fewer parts than
|
|
329
|
+
* the key has columns — the first column's value alone, say — cannot say
|
|
330
|
+
* which row it stopped at, and is refused rather than read as a guess.
|
|
331
|
+
*/
|
|
332
|
+
private static cursorKey;
|
|
333
|
+
/**
|
|
334
|
+
* "Sorts after the cursor row" on the key alone: the key descending, as the
|
|
335
|
+
* `ORDER BY` ends.
|
|
336
|
+
*
|
|
337
|
+
* Every column runs the same direction, so for a composite key the
|
|
338
|
+
* row-value comparison is exactly the lexicographic order the `ORDER BY`
|
|
339
|
+
* produces.
|
|
340
|
+
*/
|
|
341
|
+
private static keyAfter;
|
|
342
|
+
/**
|
|
343
|
+
* The cursor row's value for a timestamp key, to the microsecond.
|
|
344
|
+
*
|
|
345
|
+
* A cursor is built from the row as served, and a timestamp is served to
|
|
346
|
+
* the millisecond while Postgres stores it to the microsecond. Seeking past
|
|
347
|
+
* the served value compares the column against something up to 999µs short
|
|
348
|
+
* of the cursor row's own, so every row in that sliver lands on the wrong
|
|
349
|
+
* side of it: paging `createdAt desc` over rows written in one transaction
|
|
350
|
+
* stopped after the first page, and ascending repeated the cursor row
|
|
351
|
+
* forever.
|
|
352
|
+
*
|
|
353
|
+
* So the exact value is read back from the cursor row by its key — but only
|
|
354
|
+
* while that row still holds the millisecond the cursor carries. A row
|
|
355
|
+
* deleted or re-stamped since the page was served falls back to the stored
|
|
356
|
+
* value, which is where the listing was when the cursor was issued.
|
|
357
|
+
*
|
|
358
|
+
* Any other key, and a NULL, is returned as it came.
|
|
359
|
+
*/
|
|
360
|
+
private preciseCursorValue;
|
|
280
361
|
/**
|
|
281
362
|
* "Sorts strictly after the cursor row", over `keys` and then the id.
|
|
282
363
|
*
|
|
@@ -386,6 +467,8 @@ export declare class FetchService {
|
|
|
386
467
|
fields?: string[];
|
|
387
468
|
/** `SELECT DISTINCT` over the projection. */
|
|
388
469
|
distinct?: boolean;
|
|
470
|
+
/** See `FetchCollectionProps.withDeleted`. */
|
|
471
|
+
withDeleted?: WithDeleted;
|
|
389
472
|
}): Promise<Record<string, unknown>[]>;
|
|
390
473
|
/**
|
|
391
474
|
* Search rows by text
|
|
@@ -453,11 +536,45 @@ export declare class FetchService {
|
|
|
453
536
|
logical?: LogicalCondition;
|
|
454
537
|
searchString?: string;
|
|
455
538
|
limit?: number;
|
|
539
|
+
/** Groups to skip. Applied only to a grouped aggregate, like `limit`. */
|
|
540
|
+
offset?: number;
|
|
541
|
+
/**
|
|
542
|
+
* Sort for the groups: by a `groupBy` field or an aggregate's alias.
|
|
543
|
+
* Applied only to a grouped aggregate; see {@link aggregateOrdering}.
|
|
544
|
+
*/
|
|
545
|
+
orderBy?: OrderByTuple[];
|
|
456
546
|
/** See `FetchCollectionProps.withDeleted`. */
|
|
457
547
|
withDeleted?: WithDeleted;
|
|
458
548
|
}): Promise<Record<string, unknown>[]>;
|
|
459
549
|
/**
|
|
460
|
-
*
|
|
550
|
+
* The `ORDER BY` of a grouped aggregate: the caller's keys, then every
|
|
551
|
+
* group key they did not name.
|
|
552
|
+
*
|
|
553
|
+
* A grouped aggregate is paged like a listing — `limit` bounds it, so more
|
|
554
|
+
* groups than one page holds are reached with `offset` — and a page is only
|
|
555
|
+
* a page over a total order. With no `ORDER BY` at all, Postgres returns the
|
|
556
|
+
* groups in whatever order its plan produces them, so "the next fifty" was
|
|
557
|
+
* not a thing that existed. The group keys identify a group, so ending on
|
|
558
|
+
* them makes the order total and puts every page boundary in the same place
|
|
559
|
+
* on every request.
|
|
560
|
+
*
|
|
561
|
+
* A key may name a `groupBy` field or an aggregate by its alias
|
|
562
|
+
* (`count`, `sum_total`), sorted on the aggregate's own expression — a
|
|
563
|
+
* drizzle selection is not aliased in the SQL, so there is no output name
|
|
564
|
+
* to refer to. Anything else is a 400: Postgres would refuse it too, but as
|
|
565
|
+
* a `GROUP BY` error from inside the statement.
|
|
566
|
+
*/
|
|
567
|
+
private aggregateOrdering;
|
|
568
|
+
/**
|
|
569
|
+
* Check if a field value is unique.
|
|
570
|
+
*
|
|
571
|
+
* Deliberately **not** narrowed by `beforeQuery`, and it is the only read
|
|
572
|
+
* here that is not. The question is whether the value exists anywhere in
|
|
573
|
+
* the table, not whether it exists among the rows this caller can see:
|
|
574
|
+
* narrowed, it would answer "unique" for a value a hidden row already
|
|
575
|
+
* holds, and the insert would then be rejected by the unique constraint
|
|
576
|
+
* with a message about a row the caller has no way to find. Uniqueness is a
|
|
577
|
+
* property of the table.
|
|
461
578
|
*/
|
|
462
579
|
checkUniqueField(collectionPath: string, fieldName: string, value: unknown, excludeEntityId?: string, _databaseId?: string): Promise<boolean>;
|
|
463
580
|
/**
|