@rebasepro/server 0.13.0 → 0.13.1-canary.g249daa1
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/{admin_block-CcSGdH7M.js → admin_block-CLeUMG8G.js} +3 -1
- package/dist/admin_block-CLeUMG8G.js.map +1 -0
- package/dist/api/errors.d.ts +0 -6
- package/dist/api/openapi-generator.d.ts +14 -0
- package/dist/api/rest/idempotency.d.ts +41 -4
- package/dist/api/rest/write-validation.d.ts +12 -5
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/jwt.d.ts +12 -0
- package/dist/{auth-CuC9M2x6.js → auth-BANhRhN9.js} +194 -84
- package/dist/auth-BANhRhN9.js.map +1 -0
- package/dist/{backup-CVggVhR2.js → backup-BJiY_mM-.js} +2 -2
- package/dist/{backup-CVggVhR2.js.map → backup-BJiY_mM-.js.map} +1 -1
- package/dist/boot/boot.d.ts +19 -0
- package/dist/boot/ddl-bootstrap.d.ts +70 -0
- package/dist/{contract-routes-Dj8i5AiM.js → contract-routes-FSt0Nyfk.js} +2 -2
- package/dist/{contract-routes-Dj8i5AiM.js.map → contract-routes-FSt0Nyfk.js.map} +1 -1
- package/dist/cron/cron-scheduler.d.ts +8 -3
- package/dist/cron/define-cron.d.ts +5 -3
- package/dist/{cron-routes-CrQ0tK-_.js → cron-routes-D5a9v1HY.js} +2 -2
- package/dist/{cron-routes-CrQ0tK-_.js.map → cron-routes-D5a9v1HY.js.map} +1 -1
- package/dist/{cron-scheduler-B3RFt0HS.js → cron-scheduler-CsQtYLMq.js} +28 -3
- package/dist/cron-scheduler-CsQtYLMq.js.map +1 -0
- package/dist/{cron-store-BywZsyfZ.js → cron-store-ZbjfuvMb.js} +65 -46
- package/dist/cron-store-ZbjfuvMb.js.map +1 -0
- package/dist/ddl-bootstrap-Bp3mr6Td.js +183 -0
- package/dist/ddl-bootstrap-Bp3mr6Td.js.map +1 -0
- package/dist/{errors-CgkCzoj7.js → errors-BpudWAVU.js} +2 -10
- package/dist/errors-BpudWAVU.js.map +1 -0
- package/dist/functions/define-function.d.ts +1 -1
- package/dist/history/history-routes.d.ts +6 -0
- package/dist/index.es.js +835 -276
- package/dist/index.es.js.map +1 -1
- package/dist/{jwt-DD6EtpGj.js → jwt-CzeviDcB.js} +17 -2
- package/dist/{jwt-DD6EtpGj.js.map → jwt-CzeviDcB.js.map} +1 -1
- package/dist/{openapi-generator-BEwyiaAr.js → openapi-generator-VB0MUH8h.js} +340 -128
- package/dist/openapi-generator-VB0MUH8h.js.map +1 -0
- package/dist/{schema-editor-routes-CbF20Mf1.js → schema-editor-routes-BqjzvvWU.js} +4 -4
- package/dist/{schema-editor-routes-CbF20Mf1.js.map → schema-editor-routes-BqjzvvWU.js.map} +1 -1
- package/dist/{src-_qQ3RNCK.js → src-Ca6NhxKs.js} +79 -2
- package/dist/src-Ca6NhxKs.js.map +1 -0
- package/dist/{src-Cum9kox5.js → src-DVb5a0bp.js} +153 -39
- package/dist/src-DVb5a0bp.js.map +1 -0
- package/dist/storage/keys.d.ts +65 -0
- package/dist/utils/logging.d.ts +0 -4
- package/dist/utils/sql.d.ts +11 -6
- package/package.json +6 -6
- package/dist/admin_block-CcSGdH7M.js.map +0 -1
- package/dist/auth-CuC9M2x6.js.map +0 -1
- package/dist/backend-CIxN4FVm.js +0 -15
- package/dist/backend-CIxN4FVm.js.map +0 -1
- package/dist/cron-scheduler-B3RFt0HS.js.map +0 -1
- package/dist/cron-store-BywZsyfZ.js.map +0 -1
- package/dist/errors-CgkCzoj7.js.map +0 -1
- package/dist/openapi-generator-BEwyiaAr.js.map +0 -1
- package/dist/src-Cum9kox5.js.map +0 -1
- package/dist/src-_qQ3RNCK.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi-generator-VB0MUH8h.js","names":[],"sources":["../../types/src/types/relations.ts","../src/api/openapi-generator.ts"],"sourcesContent":["import type { AnyCollectionConfig } from \"./collections\";\n\n/**\n * @group Models\n */\nexport type OnAction = \"cascade\" | \"restrict\" | \"no action\" | \"set null\" | \"set default\";\n\n/**\n * What kind of link a relation is.\n *\n * The discriminant. Every other field a relation carries belongs to exactly one\n * of these, which is the point: a relation used to be a single open interface\n * where `cardinality`, `direction`, `localKey`, `foreignKeyOnTarget`, `through`\n * and `joinPath` were all optional and any combination typechecked. Which link\n * you meant then had to be *inferred* from which fields you happened to set,\n * and the inference was ~200 lines that guessed, fell back on naming\n * conventions, and swallowed its own failures.\n *\n * Most of that guessing produced bugs rather than convenience. A `many`\n * relation carrying a `localKey` — a combination the old type permitted — made\n * the write path stamp the parent's own foreign key onto the child row. Under\n * these kinds that state cannot be written down.\n *\n * @group Models\n */\nexport type RelationKind = \"belongsTo\" | \"hasOne\" | \"hasMany\" | \"manyToMany\" | \"via\";\n\n/** Fields every relation carries, whatever its kind. @group Models */\nexport interface RelationBase {\n /**\n * The name this link is addressed by: the key in `include`, the tab in the\n * admin panel, and the path segment of a nested URL.\n *\n * Defaults to the declaring property's key, or to the target's slug for an\n * entry in `relations`.\n */\n relationName?: string;\n\n /** The collection on the other end. */\n target: () => AnyCollectionConfig;\n\n onUpdate?: OnAction;\n onDelete?: OnAction;\n\n /** Presentation overrides applied when this relation is rendered as a tab. */\n overrides?: Partial<AnyCollectionConfig>;\n\n validation?: {\n required?: boolean;\n };\n}\n\n/**\n * This collection holds the foreign key. One target row per source row.\n *\n * ```ts\n * author: { kind: \"belongsTo\", target: () => authors, localKey: \"author_id\" }\n * ```\n * @group Models\n */\nexport interface BelongsToRelation extends RelationBase {\n kind: \"belongsTo\";\n /**\n * Column on **this** collection's table holding the target's key.\n * Defaults to `<relationName>_id`.\n */\n localKey?: string;\n}\n\n/**\n * The target holds the foreign key, and at most one target row points back.\n *\n * ```ts\n * profile: { kind: \"hasOne\", target: () => profiles, foreignKeyOnTarget: \"user_id\" }\n * ```\n * @group Models\n */\nexport interface HasOneRelation extends RelationBase {\n kind: \"hasOne\";\n /**\n * Column on the **target's** table holding this collection's key.\n * Defaults to `<thisCollection>_id`.\n */\n foreignKeyOnTarget?: string;\n /**\n * Column on **this** collection's table whose value `foreignKeyOnTarget`\n * holds. Defaults to this collection's primary key.\n *\n * Set it when the two sides are joined on a natural key rather than on the\n * row id — an external identity id, a SKU, a tenant slug. See\n * {@link HasManyRelation.sourceKey}, which this mirrors.\n */\n sourceKey?: string;\n}\n\n/**\n * The target holds the foreign key, and many target rows point back. The\n * children belong to this parent alone — deleting one deletes a row.\n *\n * ```ts\n * posts: { kind: \"hasMany\", target: () => posts, foreignKeyOnTarget: \"author_id\" }\n * ```\n * @group Models\n */\nexport interface HasManyRelation extends RelationBase {\n kind: \"hasMany\";\n /**\n * Column on the **target's** table holding this collection's key.\n * Defaults to `<thisCollection>_id`.\n */\n foreignKeyOnTarget?: string;\n /**\n * Column on **this** collection's table whose value `foreignKeyOnTarget`\n * holds. Defaults to this collection's primary key.\n *\n * The mirror of `localKey` on {@link BelongsToRelation}: that one names the\n * column this side reads from, this one names the column the other side\n * points at. Without it the pair can only be joined on the row id, which\n * makes a natural-key link — `auth_user_id ↔ auth_user_id`, a SKU, a tenant\n * slug — inexpressible as `hasMany`, and it has to drop to the read-only\n * `via`.\n *\n * The column must be unique: the link addresses one source row per value,\n * and Postgres will not accept a foreign key against a non-unique column.\n *\n * ```ts\n * applications: {\n * kind: \"hasMany\",\n * target: () => talentApplications,\n * sourceKey: \"auth_user_id\",\n * foreignKeyOnTarget: \"auth_user_id\"\n * }\n * ```\n */\n sourceKey?: string;\n}\n\n/**\n * Both sides hold many, through a junction table. The target rows are shared,\n * so this collection owns the *link* and not the row: removing one removes a\n * junction row and leaves the target alone.\n *\n * Declared the same way from either side — there is no owning and inverse\n * version. Swap `sourceColumn` and `targetColumn` to describe the other\n * direction.\n *\n * ```ts\n * tags: { kind: \"manyToMany\", target: () => tags }\n * ```\n * @group Models\n */\nexport interface ManyToManyRelation extends RelationBase {\n kind: \"manyToMany\";\n /**\n * The junction table and its two key columns. Every part defaults: the\n * table to both table names sorted and joined, the columns to\n * `<collection>_id` and `<relationName>_id`.\n */\n through?: {\n table?: string;\n /** Junction column holding **this** collection's key. */\n sourceColumn?: string;\n /** Junction column holding the **target's** key. */\n targetColumn?: string;\n };\n}\n\n/**\n * An explicit chain of joins, for links the four shapes above cannot express:\n * multi-hop paths, composite keys, or a join whose condition is not a plain\n * foreign key.\n *\n * Read-only. Rebase will not infer how to write through an arbitrary join\n * chain, and guessing is what this type exists to stop.\n *\n * ```ts\n * permissions: {\n * kind: \"via\",\n * target: () => permissions,\n * cardinality: \"many\",\n * joinPath: [\n * { table: \"user_roles\", on: { from: \"id\", to: \"user_id\" } },\n * { table: \"role_permissions\", on: { from: \"role_id\", to: \"role_id\" } },\n * { table: \"permissions\", on: { from: \"permission_id\", to: \"id\" } }\n * ]\n * }\n * ```\n * @group Models\n */\nexport interface ViaRelation extends RelationBase {\n kind: \"via\";\n /** Whether the chain yields one row or many. Cannot be derived from a join chain. */\n cardinality: \"one\" | \"many\";\n joinPath: JoinStep[];\n}\n\n/**\n * A link from one collection to another, as authored.\n *\n * A closed union: pick the kind that describes the link and the type offers\n * exactly the fields that kind needs. See {@link ResolvedRelation} for the form\n * the runtime works with, which has every default filled in.\n *\n * @group Models\n */\nexport type Relation =\n | BelongsToRelation\n | HasOneRelation\n | HasManyRelation\n | ManyToManyRelation\n | ViaRelation;\n\n/**\n * A relation with every default filled in — the form the runtime works with.\n *\n * The authored {@link Relation} and this are deliberately different types.\n * They used to be one, which meant no reader could tell which fields had been\n * supplied and which had been guessed, and so every consumer re-derived what it\n * needed with its own chain of `if (through) … else if (localKey) …` fallbacks.\n * Those chains disagreed with each other; that disagreement is what produced\n * silently wrong reads and corrupt writes.\n *\n * Here each variant carries exactly its own fields, all required. A consumer\n * switches on `kind` and gets what it needs without a fallback, and the cases\n * it forgot are a compile error rather than a wrong answer at runtime.\n *\n * @group Models\n */\nexport type ResolvedRelation =\n | ResolvedBelongsTo\n | ResolvedHasOne\n | ResolvedHasMany\n | ResolvedManyToMany\n | ResolvedVia;\n\n/** Fields present on every resolved relation. @group Models */\nexport interface ResolvedRelationBase {\n /** Always set: defaulted during resolution if the author omitted it. */\n relationName: string;\n target: () => AnyCollectionConfig;\n /** The target's slug, resolved once so consumers need not call `target()`. */\n targetSlug: string;\n onUpdate?: OnAction;\n onDelete?: OnAction;\n overrides?: Partial<AnyCollectionConfig>;\n validation?: { required?: boolean };\n /**\n * Whether one row or many come back. Derived from `kind` — kept because it\n * is what most consumers actually branch on, and because `via` is the one\n * kind where it is authored rather than implied.\n */\n cardinality: \"one\" | \"many\";\n /**\n * Whether Rebase knows how to write through this link. False only for\n * {@link ResolvedVia}, whose join chain it will not invent a write for.\n */\n writable: boolean;\n /**\n * Whether the target rows are shared with other parents. True for\n * many-to-many and for multi-hop `via`: what the parent owns is the link,\n * so removing one must not delete the row.\n */\n shared: boolean;\n}\n\n/** @group Models */\nexport interface ResolvedBelongsTo extends ResolvedRelationBase {\n kind: \"belongsTo\";\n cardinality: \"one\";\n writable: true;\n shared: false;\n /** Column on this collection's table. */\n localKey: string;\n}\n\n/** @group Models */\nexport interface ResolvedHasOne extends ResolvedRelationBase {\n kind: \"hasOne\";\n cardinality: \"one\";\n writable: true;\n shared: false;\n /** Column on the target's table. */\n foreignKeyOnTarget: string;\n /** @see ResolvedHasMany.sourceKey */\n sourceKey?: string;\n}\n\n/** @group Models */\nexport interface ResolvedHasMany extends ResolvedRelationBase {\n kind: \"hasMany\";\n cardinality: \"many\";\n writable: true;\n shared: false;\n /** Column on the target's table. */\n foreignKeyOnTarget: string;\n /**\n * Column on the source's table that `foreignKeyOnTarget` points at, or\n * `undefined` for the source's primary key.\n *\n * The one optional field on a resolved relation, and deliberately so. Every\n * other default is filled in here because it can be: a table name and a\n * column name are derivable from the relation and its two endpoints alone.\n * The primary key is not — this driver resolves it from `isId`, then the\n * Drizzle schema, then a column named `id`, and the middle tier does not\n * exist at resolution time.\n *\n * So `undefined` is a sentinel with exactly one meaning, not a field a\n * consumer is invited to guess at. Read it through `sourceKeyField()`,\n * which is the only place that turns it into a column name.\n */\n sourceKey?: string;\n}\n\n/** @group Models */\nexport interface ResolvedManyToMany extends ResolvedRelationBase {\n kind: \"manyToMany\";\n cardinality: \"many\";\n writable: true;\n shared: true;\n through: {\n table: string;\n sourceColumn: string;\n targetColumn: string;\n };\n}\n\n/** @group Models */\nexport interface ResolvedVia extends ResolvedRelationBase {\n kind: \"via\";\n writable: false;\n joinPath: JoinStep[];\n}\n\n// ── Narrowing helpers ────────────────────────────────────────────────\n//\n// Consumers that only care about one axis — \"does this list many rows\",\n// \"is there a column on the target\" — should ask that question rather\n// than enumerate kinds, so adding a kind later does not silently skip them.\n\n/** Relations whose target row carries this collection's key. @group Models */\nexport type ResolvedForeignKeyOnTarget = ResolvedHasOne | ResolvedHasMany;\n\n/** @group Models */\nexport function hasForeignKeyOnTarget(relation: ResolvedRelation): relation is ResolvedForeignKeyOnTarget {\n return relation.kind === \"hasOne\" || relation.kind === \"hasMany\";\n}\n\n/** @group Models */\nexport function isManyToMany(relation: ResolvedRelation): relation is ResolvedManyToMany {\n return relation.kind === \"manyToMany\";\n}\n\n/** @group Models */\nexport function isToMany(relation: ResolvedRelation): boolean {\n return relation.cardinality === \"many\";\n}\n\n/**\n * Defines a single, explicit step in a multi-join path.\n *\n * Each step represents one JOIN operation in the sequence. The `from` columns\n * refer to the previous table in the chain (or the source table for the first step),\n * and the `to` columns refer to the current table being joined.\n *\n * @example Single column join:\n * ```typescript\n * {\n * table: \"authors\",\n * on: {\n * from: \"author_id\", // Column from previous table (e.g., posts.author_id)\n * to: \"id\" // Column from current table (authors.id)\n * }\n * }\n * ```\n *\n * @example Multi-column composite key join:\n * ```typescript\n * {\n * table: \"order_items\",\n * on: {\n * from: [\"order_id\", \"store_id\"], // Multiple columns from previous table\n * to: [\"order_id\", \"store_id\"] // Corresponding columns in current table\n * }\n * }\n * ```\n */\nexport interface JoinStep {\n /**\n * The database table name to join TO in this step.\n * This is the table you're joining into, not the table you're joining from.\n *\n * @example \"authors\", \"user_roles\", \"product_categories\"\n */\n table: string;\n\n /**\n * The join condition for this step. Defines how the previous table\n * connects to the current table.\n *\n * - `from`: Column name(s) on the PREVIOUS table in the join chain\n * - `to`: Column name(s) on the CURRENT table (specified in `table`)\n *\n * For the first step, `from` refers to the source collection's table.\n * For subsequent steps, `from` refers to the table from the previous step.\n *\n * Both `from` and `to` support:\n * - Single column: `\"user_id\"`\n * - Multiple columns: `[\"company_id\", \"region_id\"]` for composite keys\n *\n * When using arrays, both `from` and `to` must have the same length,\n * and columns are matched by position (index 0 with index 0, etc.).\n */\n on: {\n from: string | string[];\n to: string | string[];\n };\n}\n","import { CollectionConfig, Property, StringProperty, NumberProperty, ArrayProperty, MapProperty, isToMany, VectorProperty, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT } from \"@rebasepro/types\";\nimport { resolveCollectionRelations } from \"@rebasepro/common\";\n\n/**\n * OpenAPI 3.0.3 specification generator.\n *\n * Produces a spec that exactly mirrors the REST API consumed by the\n * Rebase SDK client (`@rebasepro/client`).\n *\n * Routes are mounted at `{basePath}/data/{slug}` by `initializeRebaseBackend`.\n */\n\nexport interface OpenApiGeneratorOptions {\n /** Base path for the API (e.g. \"/api\"). Defaults to \"/api\". */\n basePath?: string;\n /** Whether auth is enabled on data routes. Defaults to true. */\n requireAuth?: boolean;\n /**\n * The list-pagination bounds the REST layer applies, so the spec states the\n * ones a request will actually meet.\n *\n * These were hardcoded as `default: 20, maximum: 100` — neither of which\n * the server has ever used. The spec drives the API Explorer and is what a\n * generated client is built from, so an understated ceiling is a request\n * the client refuses to make, and an overstated one is a 400 nobody\n * predicted.\n */\n listLimits?: { defaultLimit?: number; maxLimit?: number };\n}\n\nexport function generateOpenApiSpec(\n collections: CollectionConfig[],\n options: OpenApiGeneratorOptions = {}\n): Record<string, unknown> {\n const basePath = options.basePath ?? \"/api\";\n const requireAuth = options.requireAuth ?? true;\n const defaultLimit = options.listLimits?.defaultLimit ?? DEFAULT_LIST_LIMIT;\n const maxLimit = options.listLimits?.maxLimit ?? MAX_LIST_LIMIT;\n\n /**\n * The query parameters every list endpoint honours.\n *\n * Written once because it was written twice: the root listing named eight\n * and the subcollection listing named four, though both go through the same\n * `parseQueryOptions` and the same fetch. Four capabilities were therefore\n * unreachable from a generated client on nested routes, and `or`/`and` were\n * undocumented on both.\n */\n const listQueryParameters = () => [\n { name: \"limit\", in: \"query\", schema: { type: \"integer\", default: defaultLimit, maximum: maxLimit },\n description: \"Maximum number of records to return\" },\n { name: \"offset\", in: \"query\", schema: { type: \"integer\", default: 0 },\n description: \"Number of records to skip\" },\n { name: \"page\", in: \"query\", schema: { type: \"integer\", minimum: 1 },\n description: \"Page number (alternative to offset). Calculates offset as (page-1)*limit\" },\n {\n name: \"orderBy\",\n in: \"query\",\n schema: { type: \"string\" },\n description: \"Sort field and direction. Accepts `field:asc` or `field:desc`, or a JSON array `[{\\\"field\\\":\\\"name\\\",\\\"direction\\\":\\\"asc\\\"}]`\",\n example: \"created_at:desc\"\n },\n {\n name: \"where\",\n in: \"query\",\n schema: { type: \"string\" },\n description: \"JSON object filter, mapping each field to a `[operator, value]` tuple. \"\n + \"Combines with the per-field `?field=op.value` parameters below; on the same field, the per-field parameter wins.\",\n example: \"{\\\"status\\\":[\\\"==\\\",\\\"active\\\"]}\"\n },\n {\n name: \"or\",\n in: \"query\",\n schema: { type: \"string\" },\n description: \"Disjunction of conditions, AND-ed with `where` and `searchString`.\",\n example: \"(status.eq.draft,status.eq.review)\"\n },\n {\n name: \"and\",\n in: \"query\",\n schema: { type: \"string\" },\n description: \"Conjunction of conditions, AND-ed with `where` and `searchString`. Ignored when `or` is also present.\",\n example: \"(views.gte.10,status.eq.draft)\"\n },\n {\n name: \"include\",\n in: \"query\",\n schema: { type: \"string\" },\n description: \"Comma-separated list of relations to include (eager-load). Use `*` for all relations.\",\n example: \"author,tags\"\n },\n {\n name: \"fields\",\n in: \"query\",\n schema: { type: \"string\" },\n description: \"Comma-separated list of fields to return (field selection)\",\n example: \"id,name,created_at\"\n },\n {\n name: \"searchString\",\n in: \"query\",\n schema: { type: \"string\" },\n description:\n \"Text search. By default a case-insensitive substring match OR-ed across the \" +\n \"collection's top-level string properties. A collection declaring a `search` block \" +\n \"gets ranked full-text matching over the fields it names, and rows carry a `_score`.\"\n },\n // Vector search has been served here since vectors landed and was\n // documented nowhere, so the only way to find it was to read the query\n // parser. All four are needed together; `vector_search` and `vector`\n // are ignored unless both are present.\n {\n name: \"vector_search\",\n in: \"query\",\n schema: { type: \"string\" },\n description: \"Name of the `vector` property to run a nearest-neighbour search against. Requires `vector`.\",\n example: \"embedding\"\n },\n {\n name: \"vector\",\n in: \"query\",\n schema: { type: \"string\" },\n description: \"The query embedding, as a JSON array of numbers. Its length must match the property's declared `dimensions`.\",\n example: \"[0.12,-0.04,0.98]\"\n },\n {\n name: \"vector_distance\",\n in: \"query\",\n schema: { type: \"string\", enum: [\"cosine\", \"l2\", \"inner_product\"], default: \"cosine\" },\n description: \"Distance function used for ordering.\"\n },\n {\n name: \"vector_threshold\",\n in: \"query\",\n schema: { type: \"number\" },\n description: \"Drop rows farther than this distance. Rows are returned closest-first with a `_distance` field.\"\n }\n ];\n\n const spec: Record<string, unknown> = {\n openapi: \"3.0.3\",\n info: {\n title: \"Rebase API\",\n version: \"1.0.0\",\n description:\n \"Auto-generated REST API from Rebase collection definitions. \" +\n \"This is the same API consumed by the `@rebasepro/client` SDK.\"\n },\n servers: [\n {\n url: basePath,\n description: \"API Server\"\n }\n ],\n paths: {} as Record<string, unknown>,\n components: {\n schemas: {\n ErrorResponse: {\n type: \"object\",\n properties: {\n error: {\n type: \"object\",\n required: [\"message\", \"code\"],\n properties: {\n message: { type: \"string\" },\n code: { type: \"string\" },\n details: {}\n }\n }\n }\n },\n PaginationMeta: {\n type: \"object\",\n properties: {\n total: { type: \"integer\",\ndescription: \"Total number of matching records\" },\n limit: { type: \"integer\",\ndescription: \"Page size used for this query\" },\n offset: { type: \"integer\",\ndescription: \"Number of records skipped\" },\n hasMore: { type: \"boolean\",\ndescription: \"Whether more records exist beyond this page\" }\n }\n }\n } as Record<string, unknown>,\n securitySchemes: {} as Record<string, unknown>\n },\n tags: [] as Array<{ name: string; description?: string }>\n };\n\n // ── Security Schemes ─────────────────────────────────────────────────\n if (requireAuth) {\n (spec.components as Record<string, unknown>).securitySchemes = {\n bearerAuth: {\n type: \"http\",\n scheme: \"bearer\",\n bearerFormat: \"JWT\",\n description:\n \"JWT access token obtained from `POST /auth/login` or `POST /auth/register`. \" +\n \"Can also be a static service key for server-to-server authentication.\"\n },\n queryToken: {\n type: \"apiKey\",\n in: \"query\",\n name: \"token\",\n description: \"Alternative: pass the JWT or service key as a `token` query parameter.\"\n }\n };\n (spec as Record<string, unknown>).security = [\n { bearerAuth: [] },\n { queryToken: [] }\n ];\n }\n\n const paths = spec.paths as Record<string, unknown>;\n const schemas = (spec.components as Record<string, unknown>).schemas as Record<string, unknown>;\n const tags = spec.tags as Array<{ name: string; description?: string }>;\n\n // ── Collection routes ────────────────────────────────────────────────\n for (const collection of (collections || [])) {\n const schemaName = toPascalCase(collection.singularName || collection.name);\n const slug = collection.slug;\n\n tags.push({\n name: collection.name,\n description: collection.description || `CRUD operations for ${collection.name}`\n });\n\n // Build component schema for this collection\n schemas[schemaName] = buildCollectionSchema(collection);\n\n // Build an \"input\" schema (no read-only/auto fields like autoValue dates)\n schemas[`${schemaName}Input`] = buildCollectionInputSchema(collection);\n\n // The update body — same columns, no `required`. PATCH and PUT both\n // merge, so a field left out means \"unchanged\", not \"omitted by mistake\".\n schemas[`${schemaName}Update`] = buildCollectionUpdateSchema(collection);\n\n const dataPath = `/data/${slug}`;\n\n // ── GET /data/{slug} — List entities ──────────────────────────\n paths[dataPath] = {\n get: {\n tags: [collection.name],\n summary: `List ${collection.name}`,\n operationId: `list${schemaName}`,\n parameters: [\n ...listQueryParameters(),\n ...buildFilterParameters(collection)\n ],\n responses: {\n 200: {\n description: \"Paginated list of entities\",\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n properties: {\n data: {\n type: \"array\",\n items: { $ref: `#/components/schemas/${schemaName}` }\n },\n meta: { $ref: \"#/components/schemas/PaginationMeta\" }\n }\n }\n }\n }\n },\n ...errorResponses(requireAuth)\n }\n },\n post: {\n tags: [collection.name],\n summary: `Create ${collection.singularName || collection.name}`,\n operationId: `create${schemaName}`,\n requestBody: {\n required: true,\n content: {\n \"application/json\": {\n schema: { $ref: `#/components/schemas/${schemaName}Input` }\n }\n }\n },\n responses: {\n 201: {\n description: \"Created entity\",\n content: {\n \"application/json\": {\n schema: { $ref: `#/components/schemas/${schemaName}` }\n }\n }\n },\n ...errorResponses(requireAuth)\n }\n }\n };\n\n // ── Bulk: one transaction, all-or-nothing ─────────────────────\n //\n // These went undocumented while they existed, which is the same defect\n // the update verb had: an endpoint the server serves and the spec does\n // not mention cannot be reached by a generated client at all.\n const idempotencyHeader = {\n name: \"Idempotency-Key\",\n in: \"header\",\n required: false,\n schema: { type: \"string\" },\n description:\n \"Names this write so a retry is recognised instead of repeated. Without it a \" +\n \"client that lost the response cannot distinguish a replay from a second \" +\n \"genuine batch, and the whole batch is written twice.\"\n };\n\n const bulkErrors = {\n 400: {\n description: \"Malformed body, an unknown field, or more rows than the per-batch limit\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n 409: {\n description: \"A request with the same Idempotency-Key is still in flight\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n ...errorResponses(requireAuth)\n };\n\n paths[`/data/${slug}/bulk`] = {\n post: {\n tags: [collection.name],\n summary: `Create many ${collection.name} in one transaction`,\n description:\n \"All-or-nothing: if any row is rejected none of them land, and the error names \" +\n \"the offending index. Every row still runs callbacks, relations and row-level \" +\n \"security. Capped server-side because one batch holds its locks for its whole \" +\n \"duration.\",\n operationId: `createMany${schemaName}`,\n parameters: [idempotencyHeader],\n requestBody: {\n required: true,\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n required: [\"rows\"],\n properties: {\n rows: { type: \"array\", items: { $ref: `#/components/schemas/${schemaName}Input` } },\n upsert: {\n type: \"boolean\",\n description: \"Write each row as INSERT ... ON CONFLICT DO UPDATE on the primary key.\"\n }\n }\n }\n }\n }\n },\n responses: {\n 200: {\n description: \"The written rows, in the order given\",\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n properties: {\n data: { type: \"array\", items: { $ref: `#/components/schemas/${schemaName}` } },\n meta: { type: \"object\", properties: { written: { type: \"integer\" } } }\n }\n }\n }\n }\n },\n ...bulkErrors\n }\n },\n patch: {\n tags: [collection.name],\n summary: `Update many ${collection.name} in one transaction`,\n description:\n \"Each entry names its row and the fields to change. `{ id, data }` rather than \" +\n \"flat rows carrying their own key, because on a table keyed on something other \" +\n \"than `id` a flat row cannot say whether a column is the address or a value to \" +\n \"write. An id matching no row fails the batch.\",\n operationId: `updateMany${schemaName}`,\n parameters: [idempotencyHeader],\n requestBody: {\n required: true,\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n required: [\"updates\"],\n properties: {\n updates: {\n type: \"array\",\n items: {\n type: \"object\",\n required: [\"id\", \"data\"],\n properties: {\n id: { type: \"string\", description: \"The row to update\" },\n data: { $ref: `#/components/schemas/${schemaName}Update` }\n }\n }\n }\n }\n }\n }\n }\n },\n responses: {\n 200: {\n description: \"The updated rows, in the order given\",\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n properties: {\n data: { type: \"array\", items: { $ref: `#/components/schemas/${schemaName}` } },\n meta: { type: \"object\", properties: { written: { type: \"integer\" } } }\n }\n }\n }\n }\n },\n 404: {\n description: \"One of the ids matches no row; nothing was written\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n ...bulkErrors\n }\n }\n };\n\n paths[`/data/${slug}/bulk/delete`] = {\n post: {\n tags: [collection.name],\n summary: `Delete many ${collection.name} in one transaction`,\n description:\n \"A POST, not `DELETE /bulk` with a body. Bodies on DELETE are permitted but \" +\n \"widely dropped by proxies and CDNs, and several generators ignore \" +\n \"`requestBody` on a DELETE operation — a generated client would send the \" +\n \"request with no ids at all. Takes ids rather than a filter: a mistyped \" +\n \"condition that empties a table cannot be reviewed at the call site the way \" +\n \"an explicit list can. `beforeDelete`/`afterDelete` fire per row.\",\n operationId: `deleteMany${schemaName}`,\n parameters: [idempotencyHeader],\n requestBody: {\n required: true,\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n required: [\"ids\"],\n properties: {\n ids: {\n type: \"array\",\n items: { oneOf: [{ type: \"string\" }, { type: \"integer\" }] }\n }\n }\n }\n }\n }\n },\n responses: {\n 200: {\n description: \"How many rows were deleted\",\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n properties: {\n meta: { type: \"object\", properties: { deleted: { type: \"integer\" } } }\n }\n }\n }\n }\n },\n 404: {\n description: \"One of the ids matches no row; nothing was deleted\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n ...bulkErrors\n }\n }\n };\n\n // ── GET/PUT/DELETE /data/{slug}/{id} ──────────────────────────\n const entityPath = `/data/${slug}/{id}`;\n paths[entityPath] = {\n get: {\n tags: [collection.name],\n summary: `Get ${collection.singularName || collection.name} by ID`,\n operationId: `get${schemaName}ById`,\n parameters: [\n { name: \"id\",\nin: \"path\",\nrequired: true,\nschema: { type: \"string\" },\ndescription: \"Entity ID\" },\n {\n name: \"include\",\n in: \"query\",\n schema: { type: \"string\" },\n description: \"Comma-separated list of relations to include\",\n example: \"author,tags\"\n }\n ],\n responses: {\n 200: {\n description: \"Entity found\",\n content: {\n \"application/json\": {\n schema: { $ref: `#/components/schemas/${schemaName}` }\n }\n }\n },\n 404: { description: \"Entity not found\",\ncontent: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } } },\n ...errorResponses(requireAuth)\n }\n },\n patch: updateOperation(collection, schemaName, requireAuth),\n // Same operation under the verb every published SDK already sends.\n // Kept so existing clients keep validating; `patch` is the one to\n // generate against.\n put: {\n ...updateOperation(collection, schemaName, requireAuth),\n operationId: `update${schemaName}ViaPut`,\n deprecated: true\n },\n delete: {\n tags: [collection.name],\n summary: `Delete ${collection.singularName || collection.name}`,\n operationId: `delete${schemaName}`,\n parameters: [\n { name: \"id\",\nin: \"path\",\nrequired: true,\nschema: { type: \"string\" },\ndescription: \"Entity ID\" }\n ],\n responses: {\n 204: { description: \"Deleted successfully\" },\n 404: { description: \"Entity not found\",\ncontent: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } } },\n ...errorResponses(requireAuth)\n }\n }\n };\n\n }\n\n // ── Subcollection routes ─────────────────────────────────────────────\n //\n // A second pass, after every collection's component schema exists. These\n // routes `$ref` the *target's* schema, and the first pass builds schemas in\n // array order — so doing this inline meant a subcollection whose target\n // appeared later in the list silently degraded to an untyped `object`.\n //\n // The names come from the *resolved* relations, not from the authored\n // `relations` array. `relationName` is optional at the authoring surface —\n // it defaults to the property key, or to the target's slug — so reading the\n // raw field skipped every relation that relied on the default, and missed\n // relations declared inline on a property entirely, since those are not in\n // the array. These are the same resolved names the nested-path router\n // matches, so the spec and the routes cannot drift apart.\n //\n // A to-one relation is left out. `posts/1/author` resolves, but it\n // addresses a single row, and documenting it as a paginated list would\n // describe a response shape the client never gets.\n for (const collection of (collections || [])) {\n const slug = collection.slug;\n const schemaName = toPascalCase(collection.singularName || collection.name);\n const relations = Object.values(resolveCollectionRelations(collection))\n .filter(isToMany);\n for (const relation of relations) {\n const relationName = relation.relationName;\n const targetCollection = relation.target();\n const targetName = targetCollection.singularName || targetCollection.name;\n const targetSchema = toPascalCase(targetName);\n\n const subPath = `/data/${slug}/{parentId}/${relationName}`;\n\n // Only add if the schema exists (target collection is also registered)\n paths[subPath] = {\n get: {\n tags: [collection.name],\n summary: `List ${relationName} for ${withIndefiniteArticle(collection.singularName || collection.name)}`,\n operationId: `list${schemaName}${toPascalCase(relationName)}`,\n parameters: [\n { name: \"parentId\",\nin: \"path\",\nrequired: true,\nschema: { type: \"string\" },\ndescription: `${collection.singularName || collection.name} ID` },\n // The nested list handler goes through the same\n // `parseQueryOptions` and the same fetch as the root\n // one, so it honours the same parameters. It documented\n // four of them.\n ...listQueryParameters()\n ],\n responses: {\n 200: {\n description: `List of related ${relationName}`,\n content: {\n \"application/json\": {\n schema: {\n type: \"object\",\n properties: {\n data: {\n type: \"array\",\n items: schemas[targetSchema]\n ? { $ref: `#/components/schemas/${targetSchema}` }\n : { type: \"object\" }\n },\n meta: { $ref: \"#/components/schemas/PaginationMeta\" }\n }\n }\n }\n }\n },\n ...errorResponses(requireAuth)\n }\n }\n };\n }\n }\n\n return spec;\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────\n\n/**\n * Build the component schema for a collection (output / read shape).\n * All fields are included (including relation foreign keys).\n */\nfunction buildCollectionSchema(collection: CollectionConfig): Record<string, unknown> {\n const properties: Record<string, unknown> = {\n id: { type: \"string\",\ndescription: \"Unique identifier\" }\n };\n const required: string[] = [\"id\"];\n\n for (const [key, property] of Object.entries(collection.properties)) {\n // Skip relation properties — they are virtual and not part of the REST payload\n if (property.type === \"relation\") continue;\n\n properties[key] = convertPropertyToSchema(property);\n\n if (property.validation?.required) {\n required.push(key);\n }\n }\n\n return {\n type: \"object\",\n required: required.length > 0 ? required : undefined,\n properties\n };\n}\n\n/**\n * The PATCH/PUT operation for `/data/{slug}/{id}`.\n *\n * Split out because both verbs serve it and they must not drift: the update\n * body is a **partial**, and describing it with the create schema was the bug\n * this replaces. `<Name>Input` marks every `validation.required` property as\n * required — correct for POST, wrong for an update, where omitting a field\n * means \"leave it alone\" rather than \"I forgot it\". A client generated from\n * that spec demanded fields the server does not, and a spec-validating gateway\n * would have rejected partial updates the server accepts.\n */\nfunction updateOperation(\n collection: CollectionConfig,\n schemaName: string,\n requireAuth: boolean\n): Record<string, unknown> {\n return {\n tags: [collection.name],\n summary: `Update ${collection.singularName || collection.name}`,\n description: \"Partial update: only the properties present in the body are written; the rest are left unchanged.\",\n operationId: `update${schemaName}`,\n parameters: [\n { name: \"id\", in: \"path\", required: true, schema: { type: \"string\" }, description: \"Entity ID\" }\n ],\n requestBody: {\n required: true,\n content: {\n \"application/json\": {\n schema: { $ref: `#/components/schemas/${schemaName}Update` }\n }\n }\n },\n responses: {\n 200: {\n description: \"Updated entity\",\n content: {\n \"application/json\": {\n schema: { $ref: `#/components/schemas/${schemaName}` }\n }\n }\n },\n 404: {\n description: \"Entity not found\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n ...errorResponses(requireAuth)\n }\n };\n}\n\n/**\n * The update body: the create schema with `required` dropped.\n *\n * Derived rather than rebuilt so the two cannot describe different columns —\n * the only difference between creating and updating is which fields you must\n * supply, and that is exactly the one thing removed here.\n */\nfunction buildCollectionUpdateSchema(collection: CollectionConfig): Record<string, unknown> {\n const { required: _required, ...rest } = buildCollectionInputSchema(collection);\n return rest;\n}\n\n/**\n * Build an input schema (for POST/PUT) — excludes auto-generated fields.\n */\nfunction buildCollectionInputSchema(collection: CollectionConfig): Record<string, unknown> {\n const properties: Record<string, unknown> = {};\n const required: string[] = [];\n\n for (const [key, property] of Object.entries(collection.properties)) {\n if (property.type === \"relation\") continue;\n\n // Skip auto-value date fields from the input schema\n if (property.type === \"date\" && property.autoValue) continue;\n\n // Skip auto-generated ID fields\n if (\"isId\" in property && property.isId && property.isId !== \"manual\" && property.isId !== true) continue;\n\n properties[key] = convertPropertyToSchema(property);\n\n if (property.validation?.required) {\n required.push(key);\n }\n }\n\n // Allow explicit ID for create (optional)\n properties[\"id\"] = {\n type: \"string\",\n description: \"Optional: client-assigned ID. If omitted, the server generates one.\"\n };\n\n return {\n type: \"object\",\n required: required.length > 0 ? required : undefined,\n properties\n };\n}\n\n/**\n * Convert a Rebase Property to an OpenAPI 3.0 schema object.\n */\nfunction convertPropertyToSchema(property: Property): Record<string, unknown> {\n const base: Record<string, unknown> = {};\n\n if (property.name) {\n base.description = property.name;\n }\n\n switch (property.type) {\n case \"string\": {\n const sp = property as StringProperty;\n base.type = \"string\";\n\n if (sp.enum) {\n const enumValues = resolveEnumValues(sp.enum);\n if (enumValues.length > 0) {\n base.enum = enumValues;\n }\n }\n\n if (sp.validation) {\n if (sp.validation.min !== undefined) base.minLength = sp.validation.min;\n if (sp.validation.max !== undefined) base.maxLength = sp.validation.max;\n if (sp.validation.length !== undefined) {\n base.minLength = sp.validation.length;\n base.maxLength = sp.validation.length;\n }\n if (sp.validation.matches !== undefined) {\n base.pattern = String(sp.validation.matches);\n }\n }\n\n if (sp.email) base.format = \"email\";\n if (sp.url) base.format = \"uri\";\n if (sp.storage) base.format = \"uri\";\n\n return base;\n }\n\n case \"number\": {\n const np = property as NumberProperty;\n const isInteger = np.validation?.integer || np.columnType === \"integer\" || np.columnType === \"serial\" || np.columnType === \"bigserial\" || np.columnType === \"bigint\";\n base.type = isInteger ? \"integer\" : \"number\";\n\n if (np.enum) {\n const enumValues = resolveEnumValues(np.enum);\n if (enumValues.length > 0) {\n base.enum = enumValues;\n }\n }\n\n if (np.validation) {\n if (np.validation.min !== undefined) base.minimum = np.validation.min;\n if (np.validation.max !== undefined) base.maximum = np.validation.max;\n if (np.validation.moreThan !== undefined) {\n base.minimum = np.validation.moreThan;\n base.exclusiveMinimum = true;\n }\n if (np.validation.lessThan !== undefined) {\n base.maximum = np.validation.lessThan;\n base.exclusiveMaximum = true;\n }\n }\n\n return base;\n }\n\n case \"boolean\":\n base.type = \"boolean\";\n return base;\n\n case \"date\": {\n base.type = \"string\";\n if (property.mode === \"date\") {\n base.format = \"date\";\n } else {\n base.format = \"date-time\";\n }\n if (property.autoValue) {\n base.readOnly = true;\n base.description = (base.description || \"\") +\n (property.autoValue === \"on_create\" ? \" (Auto-set on creation)\" : \" (Auto-updated)\");\n }\n return base;\n }\n\n case \"geopoint\":\n base.type = \"object\";\n base.properties = {\n latitude: { type: \"number\" },\n longitude: { type: \"number\" }\n };\n base.required = [\"latitude\", \"longitude\"];\n return base;\n\n case \"reference\":\n base.type = \"string\";\n base.description = (base.description || \"\") + \" (Reference ID)\";\n return base;\n\n case \"array\": {\n const ap = property as ArrayProperty;\n base.type = \"array\";\n\n if (ap.oneOf) {\n // Discriminated union (e.g., content blocks)\n const typeField = ap.oneOf.typeField || \"type\";\n const valueField = ap.oneOf.valueField || \"value\";\n const variants: Record<string, unknown>[] = [];\n\n for (const [variantKey, variantProp] of Object.entries(ap.oneOf.properties)) {\n variants.push({\n type: \"object\",\n properties: {\n [typeField]: { type: \"string\",\nenum: [variantKey] },\n [valueField]: convertPropertyToSchema(variantProp)\n },\n required: [typeField, valueField]\n });\n }\n\n base.items = { oneOf: variants };\n } else if (ap.of) {\n if (Array.isArray(ap.of)) {\n base.items = { oneOf: ap.of.map(p => convertPropertyToSchema(p)) };\n } else {\n base.items = convertPropertyToSchema(ap.of);\n }\n } else {\n base.items = {};\n }\n\n if (ap.validation) {\n if (ap.validation.min !== undefined) base.minItems = ap.validation.min;\n if (ap.validation.max !== undefined) base.maxItems = ap.validation.max;\n }\n\n return base;\n }\n\n case \"map\": {\n const mp = property as MapProperty;\n base.type = \"object\";\n\n if (mp.properties) {\n const props: Record<string, unknown> = {};\n const req: string[] = [];\n\n for (const [key, subProp] of Object.entries(mp.properties)) {\n props[key] = convertPropertyToSchema(subProp);\n if (subProp.validation?.required) {\n req.push(key);\n }\n }\n\n base.properties = props;\n if (req.length > 0) base.required = req;\n } else if (mp.keyValue) {\n base.additionalProperties = true;\n }\n\n return base;\n }\n\n case \"vector\": {\n const vp = property as VectorProperty;\n base.type = \"array\";\n base.items = { type: \"number\" };\n base.description = (base.description || \"\") + ` (Vector(${vp.dimensions}))`;\n return base;\n }\n case \"binary\": {\n base.type = \"string\";\n base.description = (base.description || \"\") + \" (Binary/Base64)\";\n return base;\n }\n default:\n base.type = \"string\";\n return base;\n }\n}\n\n/**\n * Resolve EnumValues (array or record) into a flat array of enum values.\n */\nfunction resolveEnumValues(enumDef: Record<string | number, unknown> | Array<{ id: string | number }>): Array<string | number> {\n if (Array.isArray(enumDef)) {\n return enumDef.map(e => (typeof e === \"object\" && e !== null && \"id\" in e) ? e.id : e as string | number);\n }\n return Object.keys(enumDef).map(k => {\n // Preserve numeric keys as numbers\n const num = Number(k);\n return isNaN(num) ? k : num;\n });\n}\n\n/**\n * Build PostgREST-style filter parameters for a collection.\n * These are additional query parameters like `?status=eq.active&price=gte.100`.\n */\nfunction buildFilterParameters(collection: CollectionConfig): Array<Record<string, unknown>> {\n const params: Array<Record<string, unknown>> = [];\n\n for (const [key, property] of Object.entries(collection.properties)) {\n if (property.type === \"relation\" || property.type === \"map\" || property.type === \"array\" || property.type === \"geopoint\") {\n continue;\n }\n\n params.push({\n name: key,\n in: \"query\",\n required: false,\n schema: { type: \"string\" },\n description:\n `Filter by \\`${key}\\`. Supports PostgREST operators: ` +\n \"`eq.value`, `neq.value`, `gt.value`, `gte.value`, `lt.value`, `lte.value`, \" +\n \"`in.(a,b,c)`, `nin.(a,b,c)`, `cs.value` (array-contains), `csa.(a,b)` (array-contains-any). \" +\n \"Plain values imply equality.\",\n example: property.type === \"string\" ? \"eq.active\" : property.type === \"number\" ? \"gte.100\" : undefined\n });\n }\n\n return params;\n}\n\n/**\n * Standard error responses included on every endpoint.\n */\nfunction errorResponses(requireAuth: boolean): Record<string, unknown> {\n const responses: Record<string, unknown> = {\n 400: {\n description: \"Bad request\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n },\n 500: {\n description: \"Internal server error\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n }\n };\n\n if (requireAuth) {\n responses[401] = {\n description: \"Authentication required or invalid token\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n };\n responses[403] = {\n description: \"Insufficient permissions\",\n content: { \"application/json\": { schema: { $ref: \"#/components/schemas/ErrorResponse\" } } }\n };\n }\n\n return responses;\n}\n\n/**\n * Prefix a noun with \"a\" or \"an\" based on its leading sound.\n */\nfunction withIndefiniteArticle(noun: string): string {\n return `${/^[aeiou]/i.test(noun) ? \"an\" : \"a\"} ${noun}`;\n}\n\n/**\n * Convert a string to PascalCase for schema names.\n */\nfunction toPascalCase(str: string): string {\n return str\n .replace(/[^a-zA-Z0-9]+/g, \" \")\n .split(\" \")\n .filter(Boolean)\n .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(\"\");\n}\n"],"mappings":"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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
import "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
|
-
import "./src-
|
|
5
|
-
import { t as ADMIN_COLLECTION_KEYS } from "./admin_block-
|
|
6
|
-
import { n as errorHandler } from "./errors-
|
|
4
|
+
import "./src-Ca6NhxKs.js";
|
|
5
|
+
import { t as ADMIN_COLLECTION_KEYS } from "./admin_block-CLeUMG8G.js";
|
|
6
|
+
import { n as errorHandler } from "./errors-BpudWAVU.js";
|
|
7
7
|
import * as fs$1 from "fs";
|
|
8
8
|
import * as path$1 from "path";
|
|
9
9
|
import { Hono } from "hono";
|
|
@@ -245,4 +245,4 @@ function createSchemaEditorRoutes(collectionsDir) {
|
|
|
245
245
|
//#endregion
|
|
246
246
|
export { createSchemaEditorRoutes };
|
|
247
247
|
|
|
248
|
-
//# sourceMappingURL=schema-editor-routes-
|
|
248
|
+
//# sourceMappingURL=schema-editor-routes-BqjzvvWU.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-editor-routes-CbF20Mf1.js","names":[],"sources":["../src/api/ast-schema-editor.ts","../src/api/schema-editor-routes.ts"],"sourcesContent":["import { Project, SyntaxKind, ObjectLiteralExpression, ObjectLiteralElementLike, PropertyAssignment, VariableDeclaration, IndentationText } from \"ts-morph\";\nimport { ADMIN_COLLECTION_KEYS } from \"@rebasepro/types\";\nimport * as path from \"path\";\nimport * as fs from \"fs\";\n\n/**\n * Move presentation keys into the `admin` block.\n *\n * `ADMIN_COLLECTION_KEYS` comes from `@rebasepro/types` rather than being spelled\n * out here, so adding a field to `AdminCollectionOptions` cannot leave this writer\n * behind. Any such key already inside `admin` wins over a top-level copy: the\n * nested one is what the file said, and a flat duplicate is the view model's\n * flattening leaking back.\n */\nexport function nestAdminKeys(collectionData: Record<string, unknown>): Record<string, unknown> {\n const adminKeys = new Set<string>(ADMIN_COLLECTION_KEYS as readonly string[]);\n const existingBlock = (collectionData.admin ?? {}) as Record<string, unknown>;\n\n const top: Record<string, unknown> = {};\n const block: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(collectionData)) {\n if (key === \"admin\") continue;\n if (adminKeys.has(key)) block[key] = value;\n else top[key] = value;\n }\n\n const merged = { ...block, ...existingBlock };\n if (Object.keys(merged).length > 0) top.admin = merged;\n return top;\n}\n\nexport class AstSchemaEditor {\n private project: Project;\n private collectionsDir: string;\n\n constructor(collectionsDir: string) {\n this.project = new Project({\n manipulationSettings: {\n indentationText: IndentationText.FourSpaces\n }\n });\n if (fs.existsSync(collectionsDir)) {\n this.project.addSourceFilesAtPaths(`${collectionsDir}/**/*.ts`);\n }\n this.collectionsDir = path.resolve(collectionsDir);\n }\n\n /**\n * Sanitize collectionId to prevent path traversal attacks.\n * Only allows alphanumeric characters, underscores, and hyphens.\n */\n private sanitizeCollectionId(collectionId: string): string {\n const sanitized = collectionId.replace(/[^a-zA-Z0-9_-]/g, \"\");\n if (!sanitized || sanitized !== collectionId) {\n throw new Error(`Invalid collection ID: \"${collectionId}\". Only alphanumeric characters, underscores, and hyphens are allowed.`);\n }\n return sanitized;\n }\n\n /**\n * Resolve a file path and ensure it falls within the collectionsDir.\n */\n private safePath(filename: string): string {\n const resolved = path.resolve(this.collectionsDir, filename);\n if (!resolved.startsWith(this.collectionsDir + path.sep) && resolved !== this.collectionsDir) {\n throw new Error(\"Path traversal detected: resolved path is outside the collections directory.\");\n }\n return resolved;\n }\n\n private getCollectionFile(collectionId: string) {\n const safeId = this.sanitizeCollectionId(collectionId);\n const filePath = this.safePath(`${safeId}.ts`);\n let file = this.project.getSourceFile(filePath);\n if (!file && fs.existsSync(filePath)) {\n this.project.addSourceFilesAtPaths(`${this.collectionsDir}/**/*.ts`);\n file = this.project.getSourceFile(filePath);\n }\n return file;\n }\n\n private getCollectionObject(collectionId: string): ObjectLiteralExpression | null {\n const file = this.getCollectionFile(collectionId);\n if (!file) return null;\n\n const defaultExport = file.getDefaultExportSymbol();\n if (defaultExport) {\n const declaration = defaultExport.getDeclarations()[0];\n if (declaration && declaration.getKind() === SyntaxKind.ExportAssignment) {\n const expr = declaration.asKind(SyntaxKind.ExportAssignment)?.getExpression();\n if (expr && expr.getKind() === SyntaxKind.Identifier) {\n const varName = expr.getText();\n const varDecl = file.getVariableDeclaration(varName);\n return varDecl?.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression) || null;\n }\n }\n }\n // Fallback: Just get the first exported VariableDeclaration with an ObjectLiteral\n const varDecls = file.getVariableDeclarations();\n for (const varDecl of varDecls) {\n const init = varDecl.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);\n if (init) return init;\n }\n return null;\n }\n\n private convertJsonToAstString(obj: unknown, indentLevel = 0, oldAstNode?: ObjectLiteralExpression): string {\n // Base TS-morph parses arrays as 2 levels deep from the property key:\n // PropertiesObject = level 1, PropertyConfig = level 2.\n // We calibrate the spacing multiples to keep the items flush with standard TS format.\n const indentStr = \" \";\n const indent = indentStr.repeat(indentLevel);\n const innerIndent = indentStr.repeat(indentLevel + 1);\n\n if (obj === null || obj === undefined) {\n return \"undefined\";\n }\n if (typeof obj === \"string\") {\n return JSON.stringify(obj);\n }\n if (typeof obj === \"number\" || typeof obj === \"boolean\") {\n return String(obj);\n }\n if (Array.isArray(obj)) {\n if (obj.length === 0) return \"[]\";\n const items = obj.map(item => this.convertJsonToAstString(item, indentLevel + 1));\n return `[\\n${innerIndent}${items.join(`,\\n${innerIndent}`)}\\n${indent}]`;\n }\n if (typeof obj === \"object\") {\n const record = obj as Record<string, unknown>;\n const keys = Object.keys(record);\n\n // Collect preserved AST properties\n const preservedProps: string[] = [];\n if (oldAstNode) {\n const oldProps = oldAstNode.getProperties();\n for (const oldProp of oldProps) {\n if (oldProp.isKind(SyntaxKind.PropertyAssignment)) {\n const nameNode = oldProp.getNameNode();\n let name = nameNode.getText();\n if (name.startsWith('\"') && name.endsWith('\"')) name = name.slice(1, -1);\n if (name.startsWith(\"'\") && name.endsWith(\"'\")) name = name.slice(1, -1);\n\n // If the JSON object doesn't have this key, check if we should preserve it\n if (!(name in record)) {\n const init = oldProp.getInitializer();\n if (init) {\n const kind = init.getKind();\n const isCode = kind === SyntaxKind.ArrowFunction ||\n kind === SyntaxKind.FunctionExpression ||\n kind === SyntaxKind.Identifier ||\n kind === SyntaxKind.CallExpression ||\n kind === SyntaxKind.JsxElement;\n\n if (isCode || name === \"target\" || name === \"callbacks\" || name === \"permissions\" || name === \"securityRules\") {\n // Preserve this property exactly as it was\n const keyStr = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);\n preservedProps.push(`${keyStr}: ${init.getText()}`);\n }\n }\n }\n }\n }\n }\n\n if (keys.length === 0 && preservedProps.length === 0) return \"{}\";\n\n const props = keys.map(key => {\n const keyStr = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);\n\n // If the value is an object, pass the old AST node to recurse\n let childAstNode: ObjectLiteralExpression | undefined;\n if (oldAstNode && typeof record[key] === \"object\" && record[key] !== null && !Array.isArray(record[key])) {\n const oldProp = oldAstNode.getProperty(\n (p: ObjectLiteralElementLike) => \"getName\" in p && typeof (p as PropertyAssignment).getName === \"function\" && ((p as PropertyAssignment).getName() === key || (p as PropertyAssignment).getName() === `\"${key}\"` || (p as PropertyAssignment).getName() === `'${key}'`)\n );\n if (oldProp && oldProp.isKind(SyntaxKind.PropertyAssignment)) {\n childAstNode = oldProp.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);\n }\n }\n\n return `${keyStr}: ${this.convertJsonToAstString(record[key], indentLevel + 1, childAstNode)}`;\n });\n\n const allProps = [...props, ...preservedProps];\n return `{\\n${innerIndent}${allProps.join(`,\\n${innerIndent}`)}\\n${indent}}`;\n }\n return \"undefined\";\n }\n\n public async saveProperty(collectionId: string, propertyKey: string, propertyConfig: Record<string, unknown>) {\n const collectionObj = this.getCollectionObject(collectionId);\n if (!collectionObj) throw new Error(`Collection ${collectionId} not found in ATS workspace.`);\n\n let propertiesProp = collectionObj.getProperty(\"properties\") as PropertyAssignment;\n if (!propertiesProp) {\n propertiesProp = collectionObj.addPropertyAssignment({\n name: \"properties\",\n initializer: \"{}\"\n });\n }\n\n const propsObj = propertiesProp.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);\n if (propsObj) {\n const existingProp = propsObj.getProperty(\n (p: ObjectLiteralElementLike) => \"getName\" in p && typeof (p as PropertyAssignment).getName === \"function\" && ((p as PropertyAssignment).getName() === propertyKey || (p as PropertyAssignment).getName() === `\"${propertyKey}\"`)\n );\n\n let oldPropAstNode: ObjectLiteralExpression | undefined;\n if (existingProp && existingProp.isKind(SyntaxKind.PropertyAssignment)) {\n oldPropAstNode = existingProp.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);\n }\n\n const newInitializer = this.convertJsonToAstString(propertyConfig, 2, oldPropAstNode);\n\n if (existingProp) {\n if (existingProp.isKind(SyntaxKind.PropertyAssignment)) {\n existingProp.setInitializer(newInitializer);\n }\n } else {\n propsObj.addPropertyAssignment({\n name: /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(propertyKey) ? propertyKey : JSON.stringify(propertyKey),\n initializer: newInitializer\n });\n }\n\n const file = this.getCollectionFile(collectionId);\n if (file) {\n file.formatText();\n }\n await this.project.save();\n }\n }\n\n public async deleteProperty(collectionId: string, propertyKey: string) {\n const collectionObj = this.getCollectionObject(collectionId);\n if (!collectionObj) return;\n\n const propertiesProp = collectionObj.getProperty(\"properties\") as PropertyAssignment;\n if (propertiesProp) {\n const propsObj = propertiesProp.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);\n if (propsObj) {\n const existingProp = propsObj.getProperty(\n (p: ObjectLiteralElementLike) => \"getName\" in p && typeof (p as PropertyAssignment).getName === \"function\" && ((p as PropertyAssignment).getName() === propertyKey || (p as PropertyAssignment).getName() === `\"${propertyKey}\"`)\n );\n if (existingProp) {\n existingProp.remove();\n const file = this.getCollectionFile(collectionId);\n if (file) {\n file.formatText();\n }\n await this.project.save();\n }\n }\n }\n }\n\n public async saveCollection(collectionId: string, collectionData: Record<string, unknown>) {\n let file = this.getCollectionFile(collectionId);\n const collectionObj = this.getCollectionObject(collectionId);\n\n if (!file || !collectionObj) {\n // Create a new file\n const safeId = this.sanitizeCollectionId(collectionId);\n const newFilePath = this.safePath(`${safeId}.ts`);\n file = this.project.createSourceFile(newFilePath, `import { CollectionConfig } from \"@rebasepro/types\";\\n\\nconst ${safeId}Collection: CollectionConfig = ${this.convertJsonToAstString(nestAdminKeys(collectionData))};\\n\\nexport default ${safeId}Collection;\\n`, { overwrite: true });\n } else {\n // Update root level properties gracefully\n\n // Force delete securityRules if empty or undefined to handle Formex / serialization stripping\n if (!(\"securityRules\" in collectionData) || collectionData.securityRules === undefined || (Array.isArray(collectionData.securityRules) && collectionData.securityRules.length === 0)) {\n const srProp = collectionObj.getProperty(\"securityRules\");\n if (srProp) {\n srProp.remove();\n }\n\n // If it was in collectionData as an empty array, delete it so the loop below doesn't add it back as \"[]\"\n // Actually, if it's \"[]\", omitting it entirely from the TS file achieves the same logical effect (no RLS rules)\n // and correctly triggers \"unmapped policies\" if the DB still has them.\n delete collectionData[\"securityRules\"];\n }\n\n // The panel works with a flat view model — presentation merged onto the\n // collection — so what arrives here has `icon` and `listProperties` at\n // the top level. On disk they belong inside `admin`. Writing them flat\n // would produce a file the backend loads and ignores and the panel\n // never reads back, which looks exactly like the edit not saving.\n collectionData = nestAdminKeys(collectionData);\n\n for (const key of Object.keys(collectionData)) {\n if (key === \"relations\") continue; // Kept via other AST functions or handled separately.\n\n const prop = collectionObj.getProperty(key) as PropertyAssignment;\n\n let oldAstNode: ObjectLiteralExpression | undefined;\n if (prop && prop.isKind(SyntaxKind.PropertyAssignment)) {\n oldAstNode = prop.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);\n }\n\n const newInit = this.convertJsonToAstString(collectionData[key], 1, oldAstNode);\n if (prop) {\n prop.setInitializer(newInit);\n } else {\n collectionObj.addPropertyAssignment({\n name: key,\n initializer: newInit\n });\n }\n }\n }\n if (file) {\n file.formatText();\n }\n await this.project.save();\n }\n\n public async deleteCollection(collectionId: string) {\n const file = this.getCollectionFile(collectionId);\n if (file) {\n file.deleteImmediatelySync();\n }\n }\n}\n","import { Hono } from \"hono\";\nimport { AstSchemaEditor } from \"./ast-schema-editor\";\nimport { errorHandler } from \"./errors\";\nimport { HonoEnv } from \"./types\";\n\nexport function createSchemaEditorRoutes(collectionsDir: string): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n router.onError(errorHandler);\n const editor = new AstSchemaEditor(collectionsDir);\n\n router.post(\"/property/save\", async (c) => {\n const body = await c.req.json();\n const { collectionId, propertyKey, propertyConfig } = body;\n await editor.saveProperty(collectionId, propertyKey, propertyConfig);\n return c.json({ success: true });\n });\n\n router.post(\"/property/delete\", async (c) => {\n const body = await c.req.json();\n const { collectionId, propertyKey } = body;\n await editor.deleteProperty(collectionId, propertyKey);\n return c.json({ success: true });\n });\n\n router.post(\"/collection/save\", async (c) => {\n const body = await c.req.json();\n const { collectionId, collectionData } = body;\n await editor.saveCollection(collectionId, collectionData);\n return c.json({ success: true });\n });\n\n router.post(\"/collection/delete\", async (c) => {\n const body = await c.req.json();\n const { collectionId } = body;\n await editor.deleteCollection(collectionId);\n return c.json({ success: true });\n });\n\n return router;\n}\n\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"schema-editor-routes-BqjzvvWU.js","names":[],"sources":["../src/api/ast-schema-editor.ts","../src/api/schema-editor-routes.ts"],"sourcesContent":["import { Project, SyntaxKind, ObjectLiteralExpression, ObjectLiteralElementLike, PropertyAssignment, VariableDeclaration, IndentationText } from \"ts-morph\";\nimport { ADMIN_COLLECTION_KEYS } from \"@rebasepro/types\";\nimport * as path from \"path\";\nimport * as fs from \"fs\";\n\n/**\n * Move presentation keys into the `admin` block.\n *\n * `ADMIN_COLLECTION_KEYS` comes from `@rebasepro/types` rather than being spelled\n * out here, so adding a field to `AdminCollectionOptions` cannot leave this writer\n * behind. Any such key already inside `admin` wins over a top-level copy: the\n * nested one is what the file said, and a flat duplicate is the view model's\n * flattening leaking back.\n */\nexport function nestAdminKeys(collectionData: Record<string, unknown>): Record<string, unknown> {\n const adminKeys = new Set<string>(ADMIN_COLLECTION_KEYS as readonly string[]);\n const existingBlock = (collectionData.admin ?? {}) as Record<string, unknown>;\n\n const top: Record<string, unknown> = {};\n const block: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(collectionData)) {\n if (key === \"admin\") continue;\n if (adminKeys.has(key)) block[key] = value;\n else top[key] = value;\n }\n\n const merged = { ...block, ...existingBlock };\n if (Object.keys(merged).length > 0) top.admin = merged;\n return top;\n}\n\nexport class AstSchemaEditor {\n private project: Project;\n private collectionsDir: string;\n\n constructor(collectionsDir: string) {\n this.project = new Project({\n manipulationSettings: {\n indentationText: IndentationText.FourSpaces\n }\n });\n if (fs.existsSync(collectionsDir)) {\n this.project.addSourceFilesAtPaths(`${collectionsDir}/**/*.ts`);\n }\n this.collectionsDir = path.resolve(collectionsDir);\n }\n\n /**\n * Sanitize collectionId to prevent path traversal attacks.\n * Only allows alphanumeric characters, underscores, and hyphens.\n */\n private sanitizeCollectionId(collectionId: string): string {\n const sanitized = collectionId.replace(/[^a-zA-Z0-9_-]/g, \"\");\n if (!sanitized || sanitized !== collectionId) {\n throw new Error(`Invalid collection ID: \"${collectionId}\". Only alphanumeric characters, underscores, and hyphens are allowed.`);\n }\n return sanitized;\n }\n\n /**\n * Resolve a file path and ensure it falls within the collectionsDir.\n */\n private safePath(filename: string): string {\n const resolved = path.resolve(this.collectionsDir, filename);\n if (!resolved.startsWith(this.collectionsDir + path.sep) && resolved !== this.collectionsDir) {\n throw new Error(\"Path traversal detected: resolved path is outside the collections directory.\");\n }\n return resolved;\n }\n\n private getCollectionFile(collectionId: string) {\n const safeId = this.sanitizeCollectionId(collectionId);\n const filePath = this.safePath(`${safeId}.ts`);\n let file = this.project.getSourceFile(filePath);\n if (!file && fs.existsSync(filePath)) {\n this.project.addSourceFilesAtPaths(`${this.collectionsDir}/**/*.ts`);\n file = this.project.getSourceFile(filePath);\n }\n return file;\n }\n\n private getCollectionObject(collectionId: string): ObjectLiteralExpression | null {\n const file = this.getCollectionFile(collectionId);\n if (!file) return null;\n\n const defaultExport = file.getDefaultExportSymbol();\n if (defaultExport) {\n const declaration = defaultExport.getDeclarations()[0];\n if (declaration && declaration.getKind() === SyntaxKind.ExportAssignment) {\n const expr = declaration.asKind(SyntaxKind.ExportAssignment)?.getExpression();\n if (expr && expr.getKind() === SyntaxKind.Identifier) {\n const varName = expr.getText();\n const varDecl = file.getVariableDeclaration(varName);\n return varDecl?.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression) || null;\n }\n }\n }\n // Fallback: Just get the first exported VariableDeclaration with an ObjectLiteral\n const varDecls = file.getVariableDeclarations();\n for (const varDecl of varDecls) {\n const init = varDecl.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);\n if (init) return init;\n }\n return null;\n }\n\n private convertJsonToAstString(obj: unknown, indentLevel = 0, oldAstNode?: ObjectLiteralExpression): string {\n // Base TS-morph parses arrays as 2 levels deep from the property key:\n // PropertiesObject = level 1, PropertyConfig = level 2.\n // We calibrate the spacing multiples to keep the items flush with standard TS format.\n const indentStr = \" \";\n const indent = indentStr.repeat(indentLevel);\n const innerIndent = indentStr.repeat(indentLevel + 1);\n\n if (obj === null || obj === undefined) {\n return \"undefined\";\n }\n if (typeof obj === \"string\") {\n return JSON.stringify(obj);\n }\n if (typeof obj === \"number\" || typeof obj === \"boolean\") {\n return String(obj);\n }\n if (Array.isArray(obj)) {\n if (obj.length === 0) return \"[]\";\n const items = obj.map(item => this.convertJsonToAstString(item, indentLevel + 1));\n return `[\\n${innerIndent}${items.join(`,\\n${innerIndent}`)}\\n${indent}]`;\n }\n if (typeof obj === \"object\") {\n const record = obj as Record<string, unknown>;\n const keys = Object.keys(record);\n\n // Collect preserved AST properties\n const preservedProps: string[] = [];\n if (oldAstNode) {\n const oldProps = oldAstNode.getProperties();\n for (const oldProp of oldProps) {\n if (oldProp.isKind(SyntaxKind.PropertyAssignment)) {\n const nameNode = oldProp.getNameNode();\n let name = nameNode.getText();\n if (name.startsWith('\"') && name.endsWith('\"')) name = name.slice(1, -1);\n if (name.startsWith(\"'\") && name.endsWith(\"'\")) name = name.slice(1, -1);\n\n // If the JSON object doesn't have this key, check if we should preserve it\n if (!(name in record)) {\n const init = oldProp.getInitializer();\n if (init) {\n const kind = init.getKind();\n const isCode = kind === SyntaxKind.ArrowFunction ||\n kind === SyntaxKind.FunctionExpression ||\n kind === SyntaxKind.Identifier ||\n kind === SyntaxKind.CallExpression ||\n kind === SyntaxKind.JsxElement;\n\n if (isCode || name === \"target\" || name === \"callbacks\" || name === \"permissions\" || name === \"securityRules\") {\n // Preserve this property exactly as it was\n const keyStr = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);\n preservedProps.push(`${keyStr}: ${init.getText()}`);\n }\n }\n }\n }\n }\n }\n\n if (keys.length === 0 && preservedProps.length === 0) return \"{}\";\n\n const props = keys.map(key => {\n const keyStr = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);\n\n // If the value is an object, pass the old AST node to recurse\n let childAstNode: ObjectLiteralExpression | undefined;\n if (oldAstNode && typeof record[key] === \"object\" && record[key] !== null && !Array.isArray(record[key])) {\n const oldProp = oldAstNode.getProperty(\n (p: ObjectLiteralElementLike) => \"getName\" in p && typeof (p as PropertyAssignment).getName === \"function\" && ((p as PropertyAssignment).getName() === key || (p as PropertyAssignment).getName() === `\"${key}\"` || (p as PropertyAssignment).getName() === `'${key}'`)\n );\n if (oldProp && oldProp.isKind(SyntaxKind.PropertyAssignment)) {\n childAstNode = oldProp.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);\n }\n }\n\n return `${keyStr}: ${this.convertJsonToAstString(record[key], indentLevel + 1, childAstNode)}`;\n });\n\n const allProps = [...props, ...preservedProps];\n return `{\\n${innerIndent}${allProps.join(`,\\n${innerIndent}`)}\\n${indent}}`;\n }\n return \"undefined\";\n }\n\n public async saveProperty(collectionId: string, propertyKey: string, propertyConfig: Record<string, unknown>) {\n const collectionObj = this.getCollectionObject(collectionId);\n if (!collectionObj) throw new Error(`Collection ${collectionId} not found in ATS workspace.`);\n\n let propertiesProp = collectionObj.getProperty(\"properties\") as PropertyAssignment;\n if (!propertiesProp) {\n propertiesProp = collectionObj.addPropertyAssignment({\n name: \"properties\",\n initializer: \"{}\"\n });\n }\n\n const propsObj = propertiesProp.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);\n if (propsObj) {\n const existingProp = propsObj.getProperty(\n (p: ObjectLiteralElementLike) => \"getName\" in p && typeof (p as PropertyAssignment).getName === \"function\" && ((p as PropertyAssignment).getName() === propertyKey || (p as PropertyAssignment).getName() === `\"${propertyKey}\"`)\n );\n\n let oldPropAstNode: ObjectLiteralExpression | undefined;\n if (existingProp && existingProp.isKind(SyntaxKind.PropertyAssignment)) {\n oldPropAstNode = existingProp.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);\n }\n\n const newInitializer = this.convertJsonToAstString(propertyConfig, 2, oldPropAstNode);\n\n if (existingProp) {\n if (existingProp.isKind(SyntaxKind.PropertyAssignment)) {\n existingProp.setInitializer(newInitializer);\n }\n } else {\n propsObj.addPropertyAssignment({\n name: /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(propertyKey) ? propertyKey : JSON.stringify(propertyKey),\n initializer: newInitializer\n });\n }\n\n const file = this.getCollectionFile(collectionId);\n if (file) {\n file.formatText();\n }\n await this.project.save();\n }\n }\n\n public async deleteProperty(collectionId: string, propertyKey: string) {\n const collectionObj = this.getCollectionObject(collectionId);\n if (!collectionObj) return;\n\n const propertiesProp = collectionObj.getProperty(\"properties\") as PropertyAssignment;\n if (propertiesProp) {\n const propsObj = propertiesProp.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);\n if (propsObj) {\n const existingProp = propsObj.getProperty(\n (p: ObjectLiteralElementLike) => \"getName\" in p && typeof (p as PropertyAssignment).getName === \"function\" && ((p as PropertyAssignment).getName() === propertyKey || (p as PropertyAssignment).getName() === `\"${propertyKey}\"`)\n );\n if (existingProp) {\n existingProp.remove();\n const file = this.getCollectionFile(collectionId);\n if (file) {\n file.formatText();\n }\n await this.project.save();\n }\n }\n }\n }\n\n public async saveCollection(collectionId: string, collectionData: Record<string, unknown>) {\n let file = this.getCollectionFile(collectionId);\n const collectionObj = this.getCollectionObject(collectionId);\n\n if (!file || !collectionObj) {\n // Create a new file\n const safeId = this.sanitizeCollectionId(collectionId);\n const newFilePath = this.safePath(`${safeId}.ts`);\n file = this.project.createSourceFile(newFilePath, `import { CollectionConfig } from \"@rebasepro/types\";\\n\\nconst ${safeId}Collection: CollectionConfig = ${this.convertJsonToAstString(nestAdminKeys(collectionData))};\\n\\nexport default ${safeId}Collection;\\n`, { overwrite: true });\n } else {\n // Update root level properties gracefully\n\n // Force delete securityRules if empty or undefined to handle Formex / serialization stripping\n if (!(\"securityRules\" in collectionData) || collectionData.securityRules === undefined || (Array.isArray(collectionData.securityRules) && collectionData.securityRules.length === 0)) {\n const srProp = collectionObj.getProperty(\"securityRules\");\n if (srProp) {\n srProp.remove();\n }\n\n // If it was in collectionData as an empty array, delete it so the loop below doesn't add it back as \"[]\"\n // Actually, if it's \"[]\", omitting it entirely from the TS file achieves the same logical effect (no RLS rules)\n // and correctly triggers \"unmapped policies\" if the DB still has them.\n delete collectionData[\"securityRules\"];\n }\n\n // The panel works with a flat view model — presentation merged onto the\n // collection — so what arrives here has `icon` and `listProperties` at\n // the top level. On disk they belong inside `admin`. Writing them flat\n // would produce a file the backend loads and ignores and the panel\n // never reads back, which looks exactly like the edit not saving.\n collectionData = nestAdminKeys(collectionData);\n\n for (const key of Object.keys(collectionData)) {\n if (key === \"relations\") continue; // Kept via other AST functions or handled separately.\n\n const prop = collectionObj.getProperty(key) as PropertyAssignment;\n\n let oldAstNode: ObjectLiteralExpression | undefined;\n if (prop && prop.isKind(SyntaxKind.PropertyAssignment)) {\n oldAstNode = prop.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);\n }\n\n const newInit = this.convertJsonToAstString(collectionData[key], 1, oldAstNode);\n if (prop) {\n prop.setInitializer(newInit);\n } else {\n collectionObj.addPropertyAssignment({\n name: key,\n initializer: newInit\n });\n }\n }\n }\n if (file) {\n file.formatText();\n }\n await this.project.save();\n }\n\n public async deleteCollection(collectionId: string) {\n const file = this.getCollectionFile(collectionId);\n if (file) {\n file.deleteImmediatelySync();\n }\n }\n}\n","import { Hono } from \"hono\";\nimport { AstSchemaEditor } from \"./ast-schema-editor\";\nimport { errorHandler } from \"./errors\";\nimport { HonoEnv } from \"./types\";\n\nexport function createSchemaEditorRoutes(collectionsDir: string): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n router.onError(errorHandler);\n const editor = new AstSchemaEditor(collectionsDir);\n\n router.post(\"/property/save\", async (c) => {\n const body = await c.req.json();\n const { collectionId, propertyKey, propertyConfig } = body;\n await editor.saveProperty(collectionId, propertyKey, propertyConfig);\n return c.json({ success: true });\n });\n\n router.post(\"/property/delete\", async (c) => {\n const body = await c.req.json();\n const { collectionId, propertyKey } = body;\n await editor.deleteProperty(collectionId, propertyKey);\n return c.json({ success: true });\n });\n\n router.post(\"/collection/save\", async (c) => {\n const body = await c.req.json();\n const { collectionId, collectionData } = body;\n await editor.saveCollection(collectionId, collectionData);\n return c.json({ success: true });\n });\n\n router.post(\"/collection/delete\", async (c) => {\n const body = await c.req.json();\n const { collectionId } = body;\n await editor.deleteCollection(collectionId);\n return c.json({ success: true });\n });\n\n return router;\n}\n\n"],"mappings":"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"}
|
|
@@ -274,6 +274,83 @@ function getDeclaredSubcollections(collection) {
|
|
|
274
274
|
return collection.subcollections;
|
|
275
275
|
}
|
|
276
276
|
//#endregion
|
|
277
|
-
|
|
277
|
+
//#region ../types/src/types/rls-functions.ts
|
|
278
|
+
/**
|
|
279
|
+
* The SQL helper functions RLS policies call, and the schema they live in.
|
|
280
|
+
*
|
|
281
|
+
* ## One schema, and it is ours
|
|
282
|
+
*
|
|
283
|
+
* Rebase creates exactly one schema in a project's database: `rebase`. These
|
|
284
|
+
* three functions live in it alongside the framework's own tables, and that is
|
|
285
|
+
* the whole contract — a reader can look at a database and know precisely which
|
|
286
|
+
* namespace belongs to the framework and that nothing else was touched.
|
|
287
|
+
*
|
|
288
|
+
* It used to be two. `uid()`, `jwt()` and `roles()` sat in a schema called
|
|
289
|
+
* `auth`, which is Supabase's name, chosen so that a developer who had written
|
|
290
|
+
* Supabase RLS would recognise `auth.uid()`. The familiarity was real but the
|
|
291
|
+
* name was not Rebase's to take, and taking it had a concrete cost: pointing
|
|
292
|
+
* Rebase at a database that already had a Supabase `auth` schema meant
|
|
293
|
+
* `CREATE OR REPLACE FUNCTION auth.uid() RETURNS text` against Supabase's
|
|
294
|
+
* `RETURNS uuid`, which Postgres rejects outright —
|
|
295
|
+
*
|
|
296
|
+
* ERROR: cannot change return type of existing function
|
|
297
|
+
* HINT: Use DROP FUNCTION auth.uid() first.
|
|
298
|
+
*
|
|
299
|
+
* — and the failure landed inside a catch-all that logged a warning and carried
|
|
300
|
+
* on, leaving a database with auth tables, no helper functions, and policies
|
|
301
|
+
* calling functions that did not exist. Under `rebase db migrate` the same
|
|
302
|
+
* statements aborted the migration instead.
|
|
303
|
+
*
|
|
304
|
+
* `rebase.uid()` collides with nobody. A Supabase database keeps its `auth`
|
|
305
|
+
* schema untouched and gains a `rebase` one, which is what a gradual migration
|
|
306
|
+
* needs.
|
|
307
|
+
*
|
|
308
|
+
* ## Why functions at all, rather than inlining `current_setting`
|
|
309
|
+
*
|
|
310
|
+
* Because the indirection has already been spent once. `uid()` resolves
|
|
311
|
+
* `app.uid` and falls back to the pre-rename `app.user_id`, so that during a
|
|
312
|
+
* rolling deploy — old and new pods serving one database — both eras resolve
|
|
313
|
+
* the principal. That was a single `CREATE OR REPLACE`. Inlined into policy
|
|
314
|
+
* bodies it would have been a rewrite of every policy on every table.
|
|
315
|
+
*
|
|
316
|
+
* ## Why the name is not configurable
|
|
317
|
+
*
|
|
318
|
+
* A policy body is stored SQL: Postgres parses `USING (…)` once and keeps it, so
|
|
319
|
+
* these strings are written into every policy in every database Rebase has
|
|
320
|
+
* provisioned. Everything that reads policies back — the SQL-to-policy parser
|
|
321
|
+
* behind the admin UI, the drift checker, `rls-check` — would have to know the
|
|
322
|
+
* configured value to recognise its own output. One frozen name is the feature.
|
|
323
|
+
*/
|
|
324
|
+
/** The schema Rebase owns. The only schema Rebase creates. */
|
|
325
|
+
var REBASE_SCHEMA = "rebase";
|
|
326
|
+
/**
|
|
327
|
+
* The principal of the current request, as text, or NULL in the server context.
|
|
328
|
+
*
|
|
329
|
+
* Never NULL for a user request — an anonymous one carries
|
|
330
|
+
* {@link ANONYMOUS_USER_ID} — which is what makes `IS NULL` a reliable test for
|
|
331
|
+
* the trusted server plane and `IS NOT NULL` a tautology.
|
|
332
|
+
*/
|
|
333
|
+
var RLS_UID_SQL = `${REBASE_SCHEMA}.uid()`;
|
|
334
|
+
/** The request's roles as a comma-separated string, for `string_to_array`. */
|
|
335
|
+
var RLS_ROLES_SQL = `${REBASE_SCHEMA}.roles()`;
|
|
336
|
+
/** The request's JWT claims as `jsonb`, or `{}`. */
|
|
337
|
+
var RLS_JWT_SQL = `${REBASE_SCHEMA}.jwt()`;
|
|
338
|
+
/**
|
|
339
|
+
* The pre-1.0 spellings, for recognising policies and hand-written SQL that
|
|
340
|
+
* predate the move.
|
|
341
|
+
*
|
|
342
|
+
* Kept because policies outlive the server that wrote them: a database migrated
|
|
343
|
+
* by an older release still holds `auth.uid()` in its policy bodies until the
|
|
344
|
+
* next push or boot recompiles them, and anything that reads policies back has
|
|
345
|
+
* to recognise both eras or report the framework's own output as foreign drift.
|
|
346
|
+
* Also used to give a project whose `securityRules` contain raw `auth.uid()` a
|
|
347
|
+
* message naming the replacement, instead of a parse failure.
|
|
348
|
+
*/
|
|
349
|
+
var LEGACY_RLS_SCHEMA = "auth";
|
|
350
|
+
`${LEGACY_RLS_SCHEMA}`;
|
|
351
|
+
`${LEGACY_RLS_SCHEMA}`;
|
|
352
|
+
`${LEGACY_RLS_SCHEMA}`;
|
|
353
|
+
//#endregion
|
|
354
|
+
export { getDeclaredSubcollections as a, DEFAULT_DATA_SOURCE_KEY as c, NULL_OPS as d, REST_TO_CANONICAL as f, getCollectionDataPath as i, getDataSourceCapabilities as l, RLS_ROLES_SQL as n, isPostgresCollectionConfig as o, toCanonicalOp as p, RLS_UID_SQL as r, isRelationalCollectionConfig as s, RLS_JWT_SQL as t, CANONICAL_TO_REST as u };
|
|
278
355
|
|
|
279
|
-
//# sourceMappingURL=src-
|
|
356
|
+
//# sourceMappingURL=src-Ca6NhxKs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src-Ca6NhxKs.js","names":[],"sources":["../../types/src/types/filter-operators.ts","../../types/src/types/data_source.ts","../../types/src/types/collections.ts","../../types/src/types/rls-functions.ts"],"sourcesContent":["/**\n * Canonical filter operators and REST wire-format mappings.\n *\n * `WhereFilterOp` is THE operator type used at every layer — from React\n * components through the SDK, server, and down to the database driver.\n *\n * PostgREST short-codes (`eq`, `gt`, `cs`, …) exist **only** at the\n * HTTP wire boundary, handled by `serializeFilter` / `deserializeFilter`\n * in `@rebasepro/common`.\n *\n * ┌──────────────────────┬───────────────┬──────────────────────────────┐\n * │ Canonical │ REST short │ Meaning │\n * ├──────────────────────┼───────────────┼──────────────────────────────┤\n * │ \"==\" │ \"eq\" │ Equal │\n * │ \"!=\" │ \"neq\" │ Not equal │\n * │ \">\" │ \"gt\" │ Greater than │\n * │ \">=\" │ \"gte\" │ Greater than or equal │\n * │ \"<\" │ \"lt\" │ Less than │\n * │ \"<=\" │ \"lte\" │ Less than or equal │\n * │ \"in\" │ \"in\" │ Value in list │\n * │ \"not-in\" │ \"nin\" │ Value not in list │\n * │ \"array-contains\" │ \"cs\" │ Array contains element │\n * │ \"array-contains-any\" │ \"csa\" │ Array contains any of │\n * │ \"like\" │ \"like\" │ SQL LIKE (case-sensitive) │\n * │ \"ilike\" │ \"ilike\" │ SQL ILIKE (case-insensitive) │\n * │ \"not-like\" │ \"nlike\" │ NOT LIKE (case-sensitive) │\n * │ \"not-ilike\" │ \"nilike\" │ NOT ILIKE (case-insensitive) │\n * │ \"is-null\" │ \"isnull\" │ Field IS NULL │\n * │ \"is-not-null\" │ \"notnull\" │ Field IS NOT NULL │\n * └──────────────────────┴───────────────┴──────────────────────────────┘\n *\n * Pattern matching (`like`/`ilike`) uses SQL wildcard syntax: `%` matches any\n * sequence of characters, `_` matches a single character. On MongoDB these are\n * translated to anchored regular expressions; Firestore has no native pattern\n * matching and rejects these operators (use `searchString` instead).\n *\n * @module\n */\n\n/**\n * Canonical sort representation: `[fieldName, direction]`.\n *\n * Used in `FindParams.orderBy`, `collection.sort`, and `FilterPreset.sort`.\n * The colon-string form (`\"field:direction\"`) exists only at the HTTP wire\n * boundary, handled by `serializeOrderBy` / `deserializeOrderBy` in\n * `@rebasepro/common`.\n *\n * Design note: the natural extension for multi-column sort is\n * `OrderByTuple[]` — not implemented yet (server consumes only the first).\n *\n * @group Models\n */\nexport type OrderByTuple<Key extends string = string> = [Key, \"asc\" | \"desc\"];\n\n/**\n * Canonical filter operators supported across all database backends.\n * Each DB driver translates these to its native query format.\n *\n * @group Models\n */\nexport type WhereFilterOp =\n | \"<\"\n | \"<=\"\n | \"==\"\n | \"!=\"\n | \">=\"\n | \">\"\n | \"array-contains\"\n | \"in\"\n | \"not-in\"\n | \"array-contains-any\"\n | \"like\"\n | \"ilike\"\n | \"not-like\"\n | \"not-ilike\"\n | \"is-null\"\n | \"is-not-null\";\n\n/**\n * Used to define filters applied in collections.\n *\n * A single condition is a tuple `[operator, value]`.\n * Multiple conditions on the same field use an array of tuples.\n *\n * @example\n * // Single condition per field\n * { status: [\"==\", \"active\"], price: [\">=\", 9.99] }\n *\n * // Multiple conditions on one field\n * { age: [[\">=\", 18], [\"<\", 65]] }\n *\n * // Array operators\n * { role: [\"in\", [\"admin\", \"editor\"]] }\n * { tags: [\"array-contains\", \"featured\"] }\n *\n * // Pattern matching (SQL wildcards: % and _)\n * { name: [\"ilike\", \"%john%\"] }\n * { slug: [\"like\", \"post-%\"] }\n *\n * // Null checks (the value is ignored; `null` is conventional)\n * { deleted_at: [\"is-null\", null] }\n * { published_at: [\"is-not-null\", null] }\n *\n * @group Models\n */\nexport type FilterValues<Key extends string> =\n Partial<Record<Key, [WhereFilterOp, unknown] | [WhereFilterOp, unknown][]>>;\n\n/**\n * The field names a query may address on a row type: every column, plus a\n * dotted path reaching inside one.\n *\n * Only the **root** of a dotted path is checked. `\"meta.tag\"` requires a `meta`\n * column and says nothing about what is under it, because what is under it is a\n * `map`/jsonb value whose shape the row type does not describe — and rejecting\n * paths we cannot verify would make jsonb columns unqueryable.\n *\n * When `M` is left at its default `Record<string, unknown>`, `keyof M` is\n * `string` and a template literal over `string` is itself assignable to\n * `string`, so this collapses to `string` and every query stays permissive.\n * That is what keeps an untyped `createRebaseClient()` behaving exactly as it\n * did before the row type was threaded through.\n *\n * @group Models\n */\nexport type FieldPath<M extends Record<string, unknown> = Record<string, unknown>> =\n | Extract<keyof M, string>\n | `${Extract<keyof M, string>}.${string}`;\n\n/**\n * Relaxed filter type that also accepts pre-serialized PostgREST strings.\n * **Internal only** — used at the wire-format boundary\n * (`serializeFilter` / `deserializeFilter` in `@rebasepro/common`).\n *\n * Application code, UI components, and SDK consumers should use\n * {@link FilterValues} instead.\n *\n * @internal\n */\nexport type WireFilterValues<Key extends string> =\n Partial<Record<Key, [WhereFilterOp, unknown] | [WhereFilterOp, unknown][] | string>>;\n\n/**\n * A pre-defined filter preset for quick access in the collection toolbar.\n * Users can select a preset to instantly apply a set of filters and\n * optionally a sort order.\n *\n * @group Models\n */\nexport interface FilterPreset<Key extends string = string> {\n /**\n * Display label shown in the preset menu.\n * If omitted, a summary is auto-generated from the filter keys.\n */\n label?: string;\n\n /**\n * The filter values to apply when this preset is selected.\n */\n filterValues: FilterValues<Key>;\n\n /**\n * Optional sort override to apply alongside the filter values.\n */\n sort?: OrderByTuple<Key>;\n}\n\n/**\n * PostgREST short-code operators. Wire format only — these never appear\n * in application code. Used by `serializeFilter`/`deserializeFilter`\n * in `@rebasepro/common`.\n */\nexport type RestFilterOp =\n | \"eq\" | \"neq\"\n | \"gt\" | \"gte\"\n | \"lt\" | \"lte\"\n | \"in\" | \"nin\"\n | \"cs\" | \"csa\"\n | \"like\" | \"ilike\"\n | \"nlike\" | \"nilike\"\n | \"isnull\" | \"notnull\";\n\n/** Maps canonical operators to their REST short-code equivalents. */\nexport const CANONICAL_TO_REST: Readonly<Record<WhereFilterOp, RestFilterOp>> = {\n \"==\": \"eq\",\n \"!=\": \"neq\",\n \">\": \"gt\",\n \">=\": \"gte\",\n \"<\": \"lt\",\n \"<=\": \"lte\",\n \"in\": \"in\",\n \"not-in\": \"nin\",\n \"array-contains\": \"cs\",\n \"array-contains-any\": \"csa\",\n \"like\": \"like\",\n \"ilike\": \"ilike\",\n \"not-like\": \"nlike\",\n \"not-ilike\": \"nilike\",\n \"is-null\": \"isnull\",\n \"is-not-null\": \"notnull\"\n};\n\n/** Maps REST short-code operators to their canonical equivalents. */\nexport const REST_TO_CANONICAL: Readonly<Record<RestFilterOp, WhereFilterOp>> = {\n \"eq\": \"==\",\n \"neq\": \"!=\",\n \"gt\": \">\",\n \"gte\": \">=\",\n \"lt\": \"<\",\n \"lte\": \"<=\",\n \"in\": \"in\",\n \"nin\": \"not-in\",\n \"cs\": \"array-contains\",\n \"csa\": \"array-contains-any\",\n \"like\": \"like\",\n \"ilike\": \"ilike\",\n \"nlike\": \"not-like\",\n \"nilike\": \"not-ilike\",\n \"isnull\": \"is-null\",\n \"notnull\": \"is-not-null\"\n};\n\n/**\n * Operators that test for null/not-null and therefore ignore their value.\n * Codecs normalize the value of these conditions to `null`.\n */\nexport const NULL_OPS: ReadonlySet<WhereFilterOp> = new Set<WhereFilterOp>([\n \"is-null\", \"is-not-null\"\n]);\n\n/**\n * Every canonical operator, in a stable order. Useful for engine capability\n * declarations ({@link DataSourceCapabilities.filterOperators}) and for\n * building operator subsets.\n * @group Models\n */\nexport const ALL_WHERE_FILTER_OPS: readonly WhereFilterOp[] = [\n \"<\", \"<=\", \"==\", \"!=\", \">=\", \">\",\n \"in\", \"not-in\",\n \"array-contains\", \"array-contains-any\",\n \"like\", \"ilike\", \"not-like\", \"not-ilike\",\n \"is-null\", \"is-not-null\"\n];\n\n/** All canonical operator strings for runtime validation. */\nconst CANONICAL_OPS: ReadonlySet<string> = new Set<WhereFilterOp>(ALL_WHERE_FILTER_OPS);\n\n/**\n * Resolve any operator string (canonical or REST short-code) to its\n * canonical `WhereFilterOp` form. Returns `undefined` for unknown operators.\n *\n * @example\n * toCanonicalOp(\"==\") // \"==\"\n * toCanonicalOp(\"eq\") // \"==\"\n * toCanonicalOp(\"cs\") // \"array-contains\"\n * toCanonicalOp(\"xyz\") // undefined\n */\nexport function toCanonicalOp(op: string): WhereFilterOp | undefined {\n if (CANONICAL_OPS.has(op)) return op as WhereFilterOp;\n return (REST_TO_CANONICAL as Record<string, WhereFilterOp | undefined>)[op];\n}\n","import { ALL_WHERE_FILTER_OPS, WhereFilterOp } from \"./filter-operators\";\n\n/**\n * Describes the capabilities and features supported by a data source (driver).\n *\n * Each driver (Postgres, Firebase, MongoDB, etc.) declares which features it\n * supports. The admin uses this descriptor to:\n * - Show/hide editor tabs (e.g. Relations for SQL, Subcollections for Firebase)\n * - Filter the property type picker (e.g. `relation` for SQL, `reference` for Firebase)\n * - Toggle driver-specific form controls (e.g. `columnType` for SQL)\n *\n * @group Models\n */\nexport interface DataSourceCapabilities {\n /** Unique driver key (e.g. \"postgres\", \"firestore\", \"mongodb\") */\n key: string;\n\n /** Human-readable label for the UI (e.g. \"PostgreSQL\", \"Firebase / Firestore\") */\n label: string;\n\n // ── Feature flags ─────────────────────────────────────────────────\n /** Does this source support SQL-style relations (JOINs)? */\n supportsRelations: boolean;\n\n /** Does this source support nested subcollections? */\n supportsSubcollections: boolean;\n\n /** Does this source support Row Level Security policies? */\n supportsRLS: boolean;\n\n /** Does this source support document references (Firebase-style)? */\n supportsReferences: boolean;\n\n /** Does this source support SQL column type annotations? */\n supportsColumnTypes: boolean;\n\n /** Does this source support real-time listeners? */\n supportsRealtime: boolean;\n\n /**\n * Does this source store vectors natively?\n *\n * `VectorProperty` carries a `dimensions` and is pgvector-shaped. It was\n * the one driver-specific property kind with no flag to gate it, so unlike\n * every other field in this descriptor there was not even a runtime answer\n * to appeal to — a Firestore collection could declare an embedding column\n * and no driver would do anything with it.\n */\n supportsVectors: boolean;\n\n /**\n * Canonical filter operators this engine can execute.\n *\n * The admin UI intersects this set with the property-type defaults and\n * any per-property narrowing (`property.ui.filterOperators`) to decide\n * which operators to offer in filter fields — so an engine that cannot\n * run `ilike` (e.g. Firestore) never shows a \"Contains\" filter that\n * would throw at query time.\n */\n filterOperators: readonly WhereFilterOp[];\n\n /**\n * Relation kinds this engine's driver can compile into a filter.\n *\n * Only `belongsTo` puts a column on the row being filtered; the others are\n * answered with a correlated subquery over the junction or the target\n * table, which not every driver can build. An engine with no relations at\n * all declares none.\n *\n * The admin uses this to decide whether a relation column offers a filter\n * control. Offering one an engine cannot answer is not cosmetic: a driver\n * that drops the key it cannot resolve *widens* the read to every row, and\n * one that fails closed answers a control the admin itself put on screen\n * with a 400.\n *\n * Optional, so a third-party driver registered before this existed still\n * compiles. Omitted means {@link DEFAULT_FILTERABLE_RELATION_KINDS} — the\n * one kind that is a plain column comparison, which every relational\n * driver can do. The subquery kinds are a real capability and have to be\n * claimed rather than assumed: assuming them wrongly is the widening.\n */\n filterableRelationKinds?: readonly string[];\n\n // ── Admin capability flags ───────────────────────────────────────\n /** Does this source support SQL admin operations (SQL editor, EXPLAIN, etc.)? */\n supportsSQLAdmin: boolean;\n\n /** Does this source support document admin operations (aggregation, stats)? */\n supportsDocumentAdmin: boolean;\n\n /** Does this source support schema admin (unmapped tables, table metadata)? */\n supportsSchemaAdmin: boolean;\n}\n\n/**\n * Subset of DataSourceCapabilities containing only feature flags.\n * Useful when you only need to check capabilities without UI metadata.\n * @group Models\n */\nexport type DataSourceFeatures = Omit<DataSourceCapabilities, \"key\" | \"label\">;\n\n/**\n * The default data-source key, used when a collection does not name a\n * `dataSource`. Shared by the frontend router and the backend driver\n * registry so both agree on \"the default database\".\n * @group Models\n */\nexport const DEFAULT_DATA_SOURCE_KEY = \"(default)\";\n\n/**\n * How the *frontend* reaches a data source.\n *\n * - `\"server\"` — through the Rebase backend (the `RebaseClient`). The backend\n * holds the actual database adapter and routes by data-source key. This is\n * the default and covers Postgres, MongoDB, and any other server-mediated\n * engine.\n * - `\"direct\"` — straight from the client to the external backend via its own\n * SDK driver (e.g. Firestore). The Rebase backend is not in the data path.\n * - `\"custom\"` — a developer-supplied {@link DataDriver}, transport unspecified.\n *\n * @group Models\n */\nexport type DataSourceTransport = \"server\" | \"direct\" | \"custom\";\n\n/**\n * Declarative definition of a data source — a named place data lives.\n *\n * Declared once and shared front and back: the frontend uses it to decide\n * transport (client vs direct driver), the backend uses the same `key` to\n * resolve a database adapter, and the editor derives capabilities from\n * `engine`. Collections reference a definition by its `key` via\n * `collection.dataSource`.\n *\n * @group Models\n */\nexport interface DataSourceDefinition {\n /**\n * Unique identifier for this data source. Collections point at it via\n * `dataSource`. Defaults to {@link DEFAULT_DATA_SOURCE_KEY}.\n */\n key: string;\n\n /**\n * The engine backing this data source (e.g. `\"postgres\"`, `\"mongodb\"`,\n * `\"firestore\"`, or a custom id). Determines the\n * {@link DataSourceCapabilities} surfaced in the editor.\n */\n engine: string;\n\n /**\n * How the frontend reaches this source. Optional — when omitted it is\n * inferred: `\"direct\"` if the definition carries a client-side driver,\n * `\"server\"` otherwise.\n */\n transport?: DataSourceTransport;\n\n /**\n * The physical database/schema/Firestore-database within the engine.\n * Threaded to drivers/adapters as the existing `databaseId` runtime\n * parameter. Defaults to the engine's own default.\n */\n databaseId?: string;\n\n /** Human-readable label for the UI. */\n label?: string;\n}\n\n/**\n * The resolved data source for a collection: the single source of truth that\n * the frontend router, backend registry, and editor all derive from.\n * Produced by `resolveDataSource(collection, registry)`.\n *\n * @group Models\n */\nexport interface ResolvedDataSource {\n /** Data-source key (routing key, shared front + back). */\n key: string;\n /** Engine backing the source (drives capabilities). */\n engine: string;\n /** Frontend transport. */\n transport: DataSourceTransport;\n /** Within-engine instance, if any (the `databaseId` runtime param). */\n databaseId?: string;\n /** Capabilities derived from {@link engine}. */\n capabilities: DataSourceCapabilities;\n}\n\n/**\n * Relation kinds assumed filterable when a driver does not say.\n *\n * `belongsTo` alone: its filter is a comparison on a column of the row being\n * filtered, the one shape that needs no query construction a driver might not\n * have. Everything else is a correlated subquery over another table.\n *\n * @group Models\n */\nexport const DEFAULT_FILTERABLE_RELATION_KINDS: readonly string[] = [\"belongsTo\"];\n\n// ── Built-in driver capabilities ─────────────────────────────────────\n\n/** @group Models */\nexport const POSTGRES_CAPABILITIES: DataSourceCapabilities = {\n key: \"postgres\",\n label: \"PostgreSQL\",\n supportsRelations: true,\n supportsSubcollections: false,\n supportsRLS: true,\n supportsReferences: false,\n supportsColumnTypes: true,\n supportsRealtime: true,\n supportsVectors: true,\n filterOperators: ALL_WHERE_FILTER_OPS,\n // `via` is absent: its join path is authored source → target with no\n // stated inverse, so the driver has nothing to reverse into a filter.\n filterableRelationKinds: [\"belongsTo\", \"manyToMany\", \"hasMany\", \"hasOne\"],\n supportsSQLAdmin: true,\n supportsDocumentAdmin: false,\n supportsSchemaAdmin: true\n};\n\n/** @group Models */\nexport const FIREBASE_CAPABILITIES: DataSourceCapabilities = {\n key: \"firestore\",\n label: \"Firebase / Firestore\",\n supportsRelations: false,\n supportsSubcollections: true,\n supportsRLS: false,\n supportsReferences: true,\n supportsColumnTypes: false,\n supportsRealtime: true,\n supportsVectors: false,\n // Firestore has no SQL pattern matching — the driver throws on the LIKE\n // family, so the UI must never offer it.\n filterOperators: ALL_WHERE_FILTER_OPS.filter(op =>\n op !== \"like\" && op !== \"ilike\" && op !== \"not-like\" && op !== \"not-ilike\"),\n // No relations at all — a document store links by reference.\n filterableRelationKinds: [],\n supportsSQLAdmin: false,\n supportsDocumentAdmin: false,\n supportsSchemaAdmin: false\n};\n\n/** @group Models */\nexport const MONGODB_CAPABILITIES: DataSourceCapabilities = {\n key: \"mongodb\",\n label: \"MongoDB\",\n supportsRelations: false,\n supportsSubcollections: true,\n supportsRLS: false,\n supportsReferences: true,\n supportsColumnTypes: false,\n supportsRealtime: false,\n supportsVectors: false,\n filterOperators: ALL_WHERE_FILTER_OPS,\n filterableRelationKinds: [],\n supportsSQLAdmin: false,\n supportsDocumentAdmin: true,\n supportsSchemaAdmin: true\n};\n\n/**\n * Fallback capabilities when the driver is unknown.\n * Enables everything so nothing is hidden unexpectedly.\n * @group Models\n */\nexport const DEFAULT_CAPABILITIES: DataSourceCapabilities = {\n key: \"(default)\",\n label: \"Default\",\n supportsRelations: true,\n supportsSubcollections: true,\n supportsRLS: true,\n supportsReferences: true,\n supportsColumnTypes: true,\n supportsRealtime: true,\n supportsVectors: true,\n filterOperators: ALL_WHERE_FILTER_OPS,\n // The exception to this descriptor's \"enable everything\" rule. The other\n // flags hide a tab or a picker when they are wrong; this one decides\n // whether a query is sent that an unknown driver may answer by dropping\n // the condition — which returns every row rather than none.\n filterableRelationKinds: DEFAULT_FILTERABLE_RELATION_KINDS,\n supportsSQLAdmin: true,\n supportsDocumentAdmin: true,\n supportsSchemaAdmin: true\n};\n\nconst CAPABILITIES_REGISTRY: Record<string, DataSourceCapabilities> = {\n postgres: POSTGRES_CAPABILITIES,\n firestore: FIREBASE_CAPABILITIES,\n mongodb: MONGODB_CAPABILITIES,\n \"(default)\": DEFAULT_CAPABILITIES\n};\n\n/**\n * Look up capabilities for a given engine key.\n * If `engine` is undefined or not found, returns `DEFAULT_CAPABILITIES`.\n * @group Models\n */\nexport function getDataSourceCapabilities(engine?: string): DataSourceCapabilities {\n if (!engine) return POSTGRES_CAPABILITIES; // postgres is the default engine\n return CAPABILITIES_REGISTRY[engine] ?? DEFAULT_CAPABILITIES;\n}\n\n/**\n * Register custom capabilities for a third-party driver.\n * @group Models\n */\nexport function registerDataSourceCapabilities(capabilities: DataSourceCapabilities): void {\n CAPABILITIES_REGISTRY[capabilities.key] = capabilities;\n}\n","import type { CollectionCallbacks } from \"./entity_callbacks\";\n\nimport type { EnumValues, Properties, PostgresProperties, FirebaseProperties, MongoProperties } from \"./properties\";\n\nimport type { User } from \"../users\";\nimport type { Relation } from \"./relations\";\nimport type { SecurityRule } from \"./security_rules\";\nimport { getDataSourceCapabilities } from \"./data_source\";\nimport type { WhereFilterOp, FilterValues, FilterPreset } from \"./filter-operators\";\nimport type { SearchConfig } from \"./search\";\n\n/**\n * Base interface containing all driver-agnostic collection properties.\n * Use {@link PostgresCollectionConfig} or {@link FirebaseCollectionConfig} for\n * driver-specific type safety, or {@link CollectionConfig} when you\n * need to handle any collection regardless of backend.\n *\n * @group Models\n */\nexport interface BaseCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User> {\n\n /**\n * The collection's identity. Required, and the value nearly everything else\n * keys on:\n *\n * - the REST path — `/api/data/<slug>`\n * - the SDK accessor — `client.data.<slug>` / `client.data.collection(\"<slug>\")`\n * - the admin panel's URL\n * - the target of a `reference` or `relation` property\n *\n * Conventionally kebab-case and plural (`blog-posts`). It is independent of\n * {@link table}: the slug is what callers say, the table is where the rows\n * live, and renaming one does not rename the other.\n *\n * Treat it as frozen once anything has shipped against it — changing a slug\n * changes every URL and every generated accessor at once.\n *\n * @example\n * defineCollection({\n * slug: \"blog-posts\", // /api/data/blog-posts, client.data.blogPosts\n * table: \"posts\",\n * properties: { … }\n * })\n */\n slug: string;\n\n /**\n * Name of the collection, typically plural.\n * E.g. `Products`, `Blog`\n */\n name: string;\n\n /**\n * Singular name of an entry in this collection\n * E.g. `Product`, `Blog entry`\n */\n singularName?: string;\n\n /**\n * Optional description of this view. You can use Markdown.\n */\n description?: string;\n\n /**\n * Child collections nested under entities of this collection.\n * Populated automatically during normalization from driver-specific fields\n * (e.g. Firebase `subcollections`, Postgres `relations` with many-cardinality).\n *\n * Custom drivers can set this directly to expose child collections to the UI.\n */\n childCollections?: () => CollectionConfig<Record<string, unknown>>[];\n\n\n /**\n * The data source this collection belongs to — the routing key shared by\n * the frontend router and the backend driver registry. It points at a\n * {@link DataSourceDefinition} registered on `<Rebase dataSources>` (front)\n * and `initializeRebaseBackend({ dataSources })` (back).\n *\n * If not specified, the default data source `\"(default)\"` is used, which\n * for a standard Rebase app is the server-mediated Postgres backend.\n *\n * @example\n * // Default data source (server-mediated Postgres)\n * { slug: \"products\" }\n *\n * // A direct-transport Firestore data source registered as \"analytics\"\n * { slug: \"events\", dataSource: \"analytics\" }\n */\n dataSource?: string;\n\n /**\n * The database engine backing this collection (`\"postgres\"`, `\"firestore\"`,\n * `\"mongodb\"`, or a custom id).\n *\n * On concrete collection types ({@link PostgresCollectionConfig},\n * {@link FirebaseCollectionConfig}, {@link MongoDBCollectionConfig}) this is a literal\n * discriminant. On the base type it is optional and gets stamped\n * automatically during collection normalization from the registered\n * {@link DataSourceDefinition}.\n *\n * Prefer setting {@link dataSource} and letting the engine be resolved.\n */\n engine?: string;\n\n /**\n * Which database within the engine.\n * - For Firestore: The Firestore database ID (e.g., for multi-database projects)\n * - For PostgreSQL: Schema or database name\n * - For MongoDB: Database name\n *\n * If not specified, the default database of the engine is used. Resolved\n * from the collection's {@link DataSourceDefinition} when omitted here.\n */\n databaseId?: string;\n\n /**\n * Set of properties that compose a entity\n */\n properties: Properties;\n\n\n\n\n\n\n\n\n\n\n\n\n /**\n * Mark this collection as an authentication collection.\n * When true, this collection is used for user management, login, password hashing, and invitation flows.\n */\n auth?: boolean | AuthCollectionConfig;\n\n\n\n\n\n\n\n /**\n * Row-level authorization rules for this collection.\n *\n * Driver-agnostic on purpose, unlike `disableDefaultPolicies`, `table` and\n * `relations`, which are declared on {@link PostgresCollectionConfig} only.\n * The rules are a *contract* — who may read or write which rows — and each\n * engine enforces it its own way:\n *\n * - **Postgres** compiles them to real `CREATE POLICY` statements and lets\n * the database enforce them (see {@link PostgresCollectionConfig.securityRules},\n * which narrows this with the raw-SQL details).\n * - **MongoDB** translates them into a query filter it AND-s into every\n * read and write, honouring `access`, `ownerField`, `roles`, `mode` and\n * the `operation`/`operations` selectors, and making a best effort at raw\n * `using`/`withCheck` SQL.\n * - **Firestore** does not implement them at all; its own rules language is\n * evaluated by Google, not from here. `supportsRLS` on\n * {@link DataSourceCapabilities} reports which engines generate policies,\n * which is not the same question as whether an engine honours a rule.\n */\n securityRules?: readonly SecurityRule[];\n\n /**\n * This interface defines all the callbacks that can be used when a entity\n * is being created, updated or deleted.\n * Useful for adding your own logic or blocking the execution of the operation.\n */\n readonly callbacks?: CollectionCallbacks<M, USER>;\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n /**\n * User id of the owner of this collection. This is used only by plugins, or if you\n * are writing custom code\n */\n ownerId?: string;\n\n /**\n * Arbitrary key-value metadata for external consumers.\n * Not interpreted by Rebase — passed through serialization unchanged.\n * Used by domain apps to store custom per-collection config.\n */\n metadata?: Record<string, unknown>;\n\n\n\n\n /**\n * If set to true, changes to the entity will be saved in a subcollection.\n * This prop has no effect if the history plugin is not enabled\n */\n history?: boolean;\n\n /**\n * Whether a write naming a field this collection does not declare is\n * rejected with a 400. Defaults to `true`.\n *\n * Set to `false` to let unknown keys through to the database, which is what\n * happened before this existed: a typo reached the INSERT and came back as\n * a Postgres error about a column, or — where a column really does exist\n * that the config never declared, populated by a trigger or a default —\n * quietly worked. The second case is the reason for the escape hatch.\n */\n strictWrites?: boolean;\n\n\n\n\n\n\n\n\n}\n\n// ── Driver-specific collection types ──────────────────────────────────\n\n/**\n * A collection backed by PostgreSQL (or any SQL database).\n * Adds support for SQL-style relations (JOINs) and Row Level Security.\n *\n * Use this type instead of {@link CollectionConfig} when you want\n * compile-time safety that only SQL-relevant fields appear.\n *\n * @group Models\n */\nexport interface PostgresCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>\n extends BaseCollectionConfig<M, USER> {\n properties: PostgresProperties;\n\n /**\n * The database engine for this collection. For Postgres collections this\n * can be omitted (Postgres is the default) or set to `\"postgres\"`.\n */\n engine?: \"postgres\" | undefined;\n\n /**\n * The PostgreSQL table name for this collection.\n */\n table: string;\n\n /**\n * The PostgreSQL schema name for this table.\n * E.g. \"public\", \"rebase\", \"auth\".\n * If not specified, \"public\" is used (or the default search path).\n */\n schema?: string;\n\n /**\n * For SQL databases, you can define the relations between collections here.\n * Relations describe JOINs, foreign keys, and junction tables.\n */\n relations?: Relation[];\n\n /**\n * Security rules for this collection (PostgreSQL Row Level Security).\n * When defined, the schema generator will enable RLS on the table and\n * create the corresponding PostgreSQL policies.\n *\n * Supports three levels of expressiveness:\n * 1. **Convenience shortcuts** — `ownerField`, `access`, `roles`\n * 2. **Raw SQL** — `using` and `withCheck` for full PostgreSQL power\n * 3. **Combined** — mix shortcuts with `roles` for common patterns\n *\n * The authenticated user context is available in raw SQL via:\n * - `auth.uid()` — the current user's ID\n * - `auth.roles()` — comma-separated app role IDs\n * - `auth.jwt()` — full JWT claims as JSONB\n */\n securityRules?: readonly SecurityRule[];\n\n /**\n * Opt out of the framework's default Row Level Security policies.\n *\n * The schema generator automatically injects, for every collection, a\n * baseline SELECT policy granting the trusted server context and the\n * `admin` role read access (reads run under a restricted role, so RLS\n * default-denies without it). For auth collections it additionally injects\n * a self-read policy (`id = auth.uid()`) and an admin-only write gate\n * (INSERT/UPDATE/DELETE require the `admin` role or the trusted server\n * context), making privileged columns such as `roles` safe by default.\n *\n * Author-defined `securityRules` are permissive and broaden access on top\n * of these defaults. Set this flag to `true` to remove the defaults\n * entirely and take full responsibility for the collection's RLS.\n *\n * @default false\n */\n disableDefaultPolicies?: boolean;\n\n /**\n * Opt in to Postgres full-text search for this collection.\n *\n * Omit it and `.search()` keeps its existing behaviour exactly — an\n * `ILIKE '%term%'` across top-level string properties. Declare it and the\n * collection gains one generated `tsvector` column and a GIN index, and\n * `.search()` compiles to a ranked `@@ websearch_to_tsquery` against them.\n *\n * Postgres-only, like {@link VectorProperty}: the block is rejected at boot\n * on other engines rather than silently ignored.\n *\n * @see SearchConfig\n */\n search?: SearchConfig;\n}\n\n/**\n * A collection backed by Firebase / Firestore.\n * Adds support for subcollections (nested document collections).\n *\n * Use this type instead of {@link CollectionConfig} when you want\n * compile-time safety that only Firestore-relevant fields appear.\n *\n * @group Models\n */\nexport interface FirebaseCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>\n extends BaseCollectionConfig<M, USER> {\n /**\n * The database engine for this collection. Must be set to `\"firestore\"`.\n */\n engine: \"firestore\";\n\n /**\n * Set of properties that compose a entity.\n * Firestore collections support `reference` properties but not `relation`.\n */\n properties: FirebaseProperties;\n\n /**\n * The Firestore collection path to query. Defaults to `slug` if not set.\n * Use this when the Firestore path differs from the slug\n * (e.g., when a PostgreSQL collection already uses the same slug).\n *\n * @example\n * ```typescript\n * const fsCustomer: FirebaseCollectionConfig = {\n * slug: \"fs_customer\", // URL: /c/fs_customer\n * path: \"customer\", // Firestore path: customer\n * name: \"Customers (Firestore)\",\n * engine: \"firestore\",\n * properties: { ... }\n * };\n * ```\n */\n path?: string;\n\n /**\n * You can add subcollections to your entity in the same way you define the root\n * collections. The collections added here will be displayed when opening\n * the side dialog of a entity.\n */\n subcollections?: () => CollectionConfig<Record<string, unknown>>[];\n}\n\n/**\n * A collection backed by MongoDB.\n *\n * Use this type instead of {@link CollectionConfig} when you want\n * compile-time safety that only MongoDB-relevant fields appear.\n *\n * @group Models\n */\nexport interface MongoDBCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>\n extends BaseCollectionConfig<M, USER> {\n\n /**\n * The database engine for this collection. Must be set to `\"mongodb\"`.\n */\n engine: \"mongodb\";\n\n /**\n * Set of properties that compose a entity.\n * MongoDB collections support `reference` properties but not `relation`.\n */\n properties: MongoProperties;\n\n /**\n * The MongoDB collection name to use. Defaults to `slug` if not set.\n * Use this when the MongoDB collection name differs from the slug\n * (e.g., when a PostgreSQL collection already uses the same slug).\n *\n * @example\n * ```typescript\n * const mongoCustomer: MongoDBCollectionConfig = {\n * slug: \"mongo_customer\", // URL: /c/mongo_customer\n * path: \"customer\", // MongoDB collection: customer\n * name: \"Customers (MongoDB)\",\n * engine: \"mongodb\",\n * properties: { ... }\n * };\n * ```\n */\n path?: string;\n}\n\n/**\n * A collection backed by any data source.\n * This is a discriminated union — use {@link PostgresCollectionConfig},\n * {@link FirebaseCollectionConfig}, or {@link MongoDBCollectionConfig} for\n * driver-specific type safety.\n *\n * @group Models\n */\nexport type CollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User> =\n | PostgresCollectionConfig<M, USER>\n | FirebaseCollectionConfig<M, USER>\n | MongoDBCollectionConfig<M, USER>;\n\n/**\n * A collection of *any* row type.\n *\n * `CollectionConfig` is **invariant** in `M`: `callbacks` both consumes `M`\n * (`AfterReadProps<M>`) and produces it, so neither direction of assignment\n * holds. `CollectionConfig<SomeRow>` is therefore not assignable to a bare\n * `CollectionConfig`, whose `M` defaults to `Record<string, unknown>`.\n *\n * That matters wherever a collection is merely *referred to* rather than read\n * from. `defineCollection` returns a config whose `M` is inferred from the\n * properties — the whole point of it — so a field typed `() => CollectionConfig`\n * rejects every collection the builder produces, and `target: () => otherCollection`\n * (the documented way to point a relation at its other end) does not compile in\n * any project that uses the builder.\n *\n * `any` is deliberate and is what it is for here: these positions never read the\n * target's rows, they only identify which collection is meant, so there is no\n * type safety to preserve and invariance is pure obstruction.\n *\n * @group Models\n */\nexport type AnyCollectionConfig = CollectionConfig<any, any>;\n\n/**\n * Type guard for PostgreSQL collections.\n * Returns true if the collection uses the Postgres engine (or the default engine).\n *\n * Generic over the *input* type, and narrows by intersection rather than\n * replacement. Narrowing to a bare `PostgresCollectionConfig` discarded whatever\n * the caller actually had — most visibly the admin panel's view model, whose\n * flattened presentation fields vanished the moment a collection passed through\n * one of these guards.\n *\n * @group Models\n */\nexport function isPostgresCollectionConfig<C extends CollectionConfig<any, any>>(\n collection: C\n): collection is C & PostgresCollectionConfig<any, any> {\n return !collection.engine || collection.engine === \"postgres\";\n}\n\n/**\n * Narrows to the SQL collection fields — `table`, `relations`,\n * `disableDefaultPolicies` — by asking the engine's declared capabilities\n * rather than by naming Postgres.\n *\n * The two halves of this already existed and were never joined. The engine\n * split (`PostgresCollectionConfig` / `FirebaseCollectionConfig` /\n * `MongoDBCollectionConfig`) said which fields belong to which engine at the\n * type level; {@link DataSourceCapabilities} said the same thing at runtime,\n * down to a `supportsRelations` flag. So call sites guarded on the capability\n * and then read a field the base type had to declare for them — which is why\n * those fields were on the base, and why a MongoDB collection could be written\n * with a `table`.\n *\n * Prefer this over {@link isPostgresCollectionConfig} wherever the question is\n * \"does this collection live in a SQL table\", so a custom SQL engine\n * registered through `registerDataSourceCapabilities` is included.\n *\n * @group Models\n */\nexport function isRelationalCollectionConfig<C extends CollectionConfig<any, any>>(\n collection: C\n): collection is C & PostgresCollectionConfig<any, any> {\n return getDataSourceCapabilities(collection.engine).supportsRelations;\n}\n\n/**\n * Type guard for Firebase / Firestore collections.\n * @group Models\n */\nexport function isFirebaseCollectionConfig<C extends CollectionConfig<any, any>>(\n collection: C\n): collection is C & FirebaseCollectionConfig<any, any> {\n return collection.engine === \"firestore\";\n}\n\n/**\n * Type guard for MongoDB collections.\n * @group Models\n */\nexport function isMongoDBCollectionConfig<C extends CollectionConfig<any, any>>(\n collection: C\n): collection is C & MongoDBCollectionConfig<any, any> {\n return collection.engine === \"mongodb\";\n}\n\n/**\n * Returns the data path for a collection.\n * For Firestore or MongoDB collections with a `path`, returns that value;\n * otherwise falls back to `slug`.\n */\nexport function getCollectionDataPath<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>(\n collection: CollectionConfig<M, USER>\n): string {\n if (isFirebaseCollectionConfig(collection) && collection.path) {\n return collection.path;\n }\n if (isMongoDBCollectionConfig(collection) && collection.path) {\n return collection.path;\n }\n return collection.slug;\n}\n\n/**\n * Reads a collection's driver-declared subcollections thunk (the `subcollections`\n * field) independent of engine identity, so engine-agnostic code doesn't have to\n * type-guard against a specific driver. Returns `undefined` when the collection\n * declares none.\n *\n * Pair with `getDataSourceCapabilities(engine).supportsSubcollections` to decide\n * whether the engine honours subcollections at all before reading them.\n * @group Models\n */\nexport function getDeclaredSubcollections<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User>(\n collection: CollectionConfig<M, USER>\n): (() => CollectionConfig<Record<string, unknown>>[]) | undefined {\n return (collection as FirebaseCollectionConfig<M, USER>).subcollections;\n}\n\n/**\n * Where the rows in an {@link EntityChildView} come from.\n *\n * The two are not the same thing, and conflating them is what made a Postgres\n * relation borrow Firestore's addressing:\n *\n * - `subcollection` is **containment**. The rows live under the parent; the\n * path is their identity, and they cannot exist without it. This is what\n * Firestore has natively.\n * - `relation` is a **link**. The rows are an ordinary collection, narrowed to\n * those the parent reaches. `owned` means the child carries the parent's\n * foreign key and belongs to it alone; `linked` means the row is shared\n * through a junction, so what the parent controls is the link, not the row.\n *\n * @group Models\n */\nexport type ChildViewSource =\n | { kind: \"subcollection\" }\n | {\n kind: \"relation\";\n relationKey: string;\n mode: \"owned\" | \"linked\";\n /**\n * Slug of the collection the rows actually live in.\n *\n * Distinct from the view's `key`, which is the relation. A `linked` view\n * needs both: the key addresses the parent's set, and this addresses the\n * whole collection to pick an existing row out of.\n */\n targetSlug: string;\n };\n\n/**\n * A list of rows rendered inside an entity view — the tab under a record.\n *\n * This is a *presentation* descriptor, which is the whole point: rendering a\n * related list as a tab used to require minting a child `CollectionConfig` with\n * its own slug, which dragged a URL grammar, a path resolver and a second\n * read/write pipeline along with it. A tab needs a key, a collection to list,\n * and to know where its rows come from.\n *\n * @group Models\n */\nexport interface EntityChildView<M extends Record<string, unknown> = Record<string, unknown>> {\n /**\n * Stable identifier for this view: the tab id and the path segment.\n *\n * For a relation this is the **relation key** — the name the backend\n * resolves a nested path segment by — not the target collection's slug.\n * Those differ whenever a relation is named, which is every inline relation\n * property, and the mismatch is why such a tab used to open onto an error.\n */\n key: string;\n\n /** The collection whose rows this view lists, with any overrides applied. */\n collection: CollectionConfig<M>;\n\n source: ChildViewSource;\n}\n\n\nexport type { WhereFilterOp, FilterValues, WireFilterValues, FilterPreset } from \"./filter-operators\";\n\n\nexport type InferCollectionConfigType<S extends CollectionConfig> = S extends CollectionConfig<infer M> ? M : never;\n\n/**\n * Configuration for authentication collections.\n *\n * Controls what happens when admins create users, reset passwords,\n * and which entity actions are auto-injected.\n *\n * Use `auth: true` as sugar for `{ enabled: true }` with all defaults.\n *\n * @example Override user creation\n * ```ts\n * auth: {\n * enabled: true,\n * onCreateUser: async (values, ctx) => {\n * const hash = await ctx.hashPassword(\"welcome123\");\n * return {\n * values: { ...values, passwordHash: hash, emailVerified: true },\n * temporaryPassword: \"welcome123\",\n * };\n * },\n * }\n * ```\n *\n * @example Disable the reset-password entity action\n * ```ts\n * auth: {\n * enabled: true,\n * actions: { resetPassword: false },\n * }\n * ```\n *\n * @group Models\n */\nexport interface AuthCollectionConfig {\n /** Set to true to mark this collection as the authentication collection. */\n enabled: boolean;\n\n /**\n * Called when an admin creates a user via the collection REST API.\n *\n * Default: generate password → hash → normalize email → save →\n * send invitation email (or return temp password if no email configured).\n *\n * Override to implement custom invitation flows, LDAP sync, etc.\n */\n onCreateUser?: (\n values: Record<string, unknown>,\n ctx: AuthCollectionContext\n ) => Promise<AuthCollectionCreateResult>;\n\n /**\n * Called when an admin resets a user's password via the admin panel.\n *\n * Default: generate reset token → send email (or generate + return temp password).\n * Override for custom reset flows.\n */\n onResetPassword?: (\n uid: string,\n ctx: AuthCollectionContext\n ) => Promise<AuthCollectionResetResult>;\n\n /**\n * Control which auth-specific entity actions are auto-injected.\n *\n * Default: `{ resetPassword: true }` — the framework auto-injects\n * the built-in `resetPasswordAction` into the collection's entity actions.\n *\n * Set to `false` to disable, or pass a custom `EntityAction` to replace the UI.\n *\n * The object form is an `EntityAction` from `@rebasepro/admin-types`, typed\n * here as `object` because it is a React component with admin controllers in\n * its props and nothing on the server reads it — only whether the built-in\n * action is injected, which is the boolean.\n */\n actions?: {\n resetPassword?: boolean | object;\n };\n}\n\n/**\n * Context provided to collection-level auth hooks.\n *\n * This is a simplified facade over the server internals —\n * it exposes only what's needed for custom auth flows without\n * coupling collection config to internal interfaces.\n *\n * @group Models\n */\nexport interface AuthCollectionContext {\n /** Hash a password using the configured algorithm (scrypt by default). */\n hashPassword: (password: string) => Promise<string>;\n /** Send an email. Only available when email service is configured. */\n sendEmail?: (options: { to: string; subject: string; html: string; text?: string }) => Promise<void>;\n /** Whether the email service is configured and available. */\n emailConfigured: boolean;\n /** The app name from email config (for templates). */\n appName: string;\n /** The base URL for password reset links. */\n resetPasswordUrl: string;\n}\n\n/**\n * Result of a collection-level `onCreateUser` hook.\n * @group Models\n */\nexport interface AuthCollectionCreateResult {\n /** Processed values to persist (must include passwordHash, NOT raw password). */\n values: Record<string, unknown>;\n /** If set, shown to the admin in the creation result dialog. */\n temporaryPassword?: string;\n /** Whether an invitation email was sent. */\n invitationSent?: boolean;\n}\n\n/**\n * Result of a collection-level `onResetPassword` hook.\n * @group Models\n */\nexport interface AuthCollectionResetResult {\n /** If set, shown to the admin. */\n temporaryPassword?: string;\n /** Whether a reset email was sent. */\n invitationSent?: boolean;\n}\n","/**\n * The SQL helper functions RLS policies call, and the schema they live in.\n *\n * ## One schema, and it is ours\n *\n * Rebase creates exactly one schema in a project's database: `rebase`. These\n * three functions live in it alongside the framework's own tables, and that is\n * the whole contract — a reader can look at a database and know precisely which\n * namespace belongs to the framework and that nothing else was touched.\n *\n * It used to be two. `uid()`, `jwt()` and `roles()` sat in a schema called\n * `auth`, which is Supabase's name, chosen so that a developer who had written\n * Supabase RLS would recognise `auth.uid()`. The familiarity was real but the\n * name was not Rebase's to take, and taking it had a concrete cost: pointing\n * Rebase at a database that already had a Supabase `auth` schema meant\n * `CREATE OR REPLACE FUNCTION auth.uid() RETURNS text` against Supabase's\n * `RETURNS uuid`, which Postgres rejects outright —\n *\n * ERROR: cannot change return type of existing function\n * HINT: Use DROP FUNCTION auth.uid() first.\n *\n * — and the failure landed inside a catch-all that logged a warning and carried\n * on, leaving a database with auth tables, no helper functions, and policies\n * calling functions that did not exist. Under `rebase db migrate` the same\n * statements aborted the migration instead.\n *\n * `rebase.uid()` collides with nobody. A Supabase database keeps its `auth`\n * schema untouched and gains a `rebase` one, which is what a gradual migration\n * needs.\n *\n * ## Why functions at all, rather than inlining `current_setting`\n *\n * Because the indirection has already been spent once. `uid()` resolves\n * `app.uid` and falls back to the pre-rename `app.user_id`, so that during a\n * rolling deploy — old and new pods serving one database — both eras resolve\n * the principal. That was a single `CREATE OR REPLACE`. Inlined into policy\n * bodies it would have been a rewrite of every policy on every table.\n *\n * ## Why the name is not configurable\n *\n * A policy body is stored SQL: Postgres parses `USING (…)` once and keeps it, so\n * these strings are written into every policy in every database Rebase has\n * provisioned. Everything that reads policies back — the SQL-to-policy parser\n * behind the admin UI, the drift checker, `rls-check` — would have to know the\n * configured value to recognise its own output. One frozen name is the feature.\n */\n\n/** The schema Rebase owns. The only schema Rebase creates. */\nexport const REBASE_SCHEMA = \"rebase\";\n\n/**\n * The principal of the current request, as text, or NULL in the server context.\n *\n * Never NULL for a user request — an anonymous one carries\n * {@link ANONYMOUS_USER_ID} — which is what makes `IS NULL` a reliable test for\n * the trusted server plane and `IS NOT NULL` a tautology.\n */\nexport const RLS_UID_SQL = `${REBASE_SCHEMA}.uid()`;\n\n/** The request's roles as a comma-separated string, for `string_to_array`. */\nexport const RLS_ROLES_SQL = `${REBASE_SCHEMA}.roles()`;\n\n/** The request's JWT claims as `jsonb`, or `{}`. */\nexport const RLS_JWT_SQL = `${REBASE_SCHEMA}.jwt()`;\n\n/**\n * The pre-1.0 spellings, for recognising policies and hand-written SQL that\n * predate the move.\n *\n * Kept because policies outlive the server that wrote them: a database migrated\n * by an older release still holds `auth.uid()` in its policy bodies until the\n * next push or boot recompiles them, and anything that reads policies back has\n * to recognise both eras or report the framework's own output as foreign drift.\n * Also used to give a project whose `securityRules` contain raw `auth.uid()` a\n * message naming the replacement, instead of a parse failure.\n */\nexport const LEGACY_RLS_SCHEMA = \"auth\";\nexport const LEGACY_RLS_UID_SQL = `${LEGACY_RLS_SCHEMA}.uid()`;\nexport const LEGACY_RLS_ROLES_SQL = `${LEGACY_RLS_SCHEMA}.roles()`;\nexport const LEGACY_RLS_JWT_SQL = `${LEGACY_RLS_SCHEMA}.jwt()`;\n\n/**\n * Rewrites the pre-1.0 function calls in a fragment of policy SQL.\n *\n * Deliberately anchored on a word boundary and the schema qualifier, so a column\n * called `auth_uid` or a table named `auth` is left alone.\n */\nexport function rewriteLegacyRlsFunctions(sql: string): string {\n return sql.replace(\n /\\bauth\\.(uid|jwt|roles)\\s*\\(\\s*\\)/gi,\n (_match, fn: string) => `${REBASE_SCHEMA}.${fn.toLowerCase()}()`\n );\n}\n\n/** Whether a fragment of SQL still calls the pre-1.0 functions. */\nexport function usesLegacyRlsFunctions(sql: string): boolean {\n return /\\bauth\\.(uid|jwt|roles)\\s*\\(\\s*\\)/i.test(sql);\n}\n"],"mappings":"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"}
|