@rebasepro/server 0.20.0 → 0.20.1-canary.g4d882ca

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.
@@ -163,26 +163,44 @@ function nestAdminKeysOf(source, adminKeys) {
163
163
  function nestAdminCollectionKeys(collection) {
164
164
  return nestAdminKeysOf(collection, ADMIN_COLLECTION_KEYS);
165
165
  }
166
+ /** A record of properties, keyed by name — a map's `properties`, or a `oneOf` block's. */
167
+ function nestEachProperty(properties) {
168
+ return Object.fromEntries(Object.entries(properties).map(([key, child]) => [key, isNestable(child) ? nestAdminPropertyKeys(child) : child]));
169
+ }
170
+ /** Anything the walk can descend into: a plain object, not an array. */
171
+ function isNestable(value) {
172
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
173
+ }
166
174
  /**
167
175
  * {@link nestAdminKeysOf} for a property, applied to its children too.
168
176
  *
169
- * A map property carries `properties`, an array property carries `of`, and both
170
- * hold properties with `admin` blocks of their own. A flat `readOnly` left on a
171
- * child is as dead and as fatal at the next boot as one left on the parent,
172
- * so the walk goes all the way down.
177
+ * A map property carries `properties`, an array property carries `of`, and an
178
+ * array of typed blocks carries `oneOf.properties` a record of properties like
179
+ * a map's. All of them hold properties with `admin` blocks of their own. A flat
180
+ * `readOnly` left on a child is as dead — and as fatal at the next boot — as one
181
+ * left on the parent, so the walk goes all the way down.
182
+ *
183
+ * `oneOf` was the container this walk did not know about, and it is the one the
184
+ * block-based collection templates are built out of: every block inside them
185
+ * kept its flat `markdown`, and the collection they created would not boot.
173
186
  *
174
187
  * @group Models
175
188
  */
176
189
  function nestAdminPropertyKeys(property) {
177
190
  const nested = nestAdminKeysOf(property, ADMIN_PROPERTY_KEYS);
178
191
  const children = nested.properties;
179
- if (children && typeof children === "object" && !Array.isArray(children)) nested.properties = Object.fromEntries(Object.entries(children).map(([key, child]) => [key, child && typeof child === "object" && !Array.isArray(child) ? nestAdminPropertyKeys(child) : child]));
192
+ if (isNestable(children)) nested.properties = nestEachProperty(children);
193
+ const oneOf = nested.oneOf;
194
+ if (isNestable(oneOf) && isNestable(oneOf.properties)) nested.oneOf = {
195
+ ...oneOf,
196
+ properties: nestEachProperty(oneOf.properties)
197
+ };
180
198
  const of = nested.of;
181
- if (Array.isArray(of)) nested.of = of.map((entry) => entry && typeof entry === "object" && !Array.isArray(entry) ? nestAdminPropertyKeys(entry) : entry);
182
- else if (of && typeof of === "object") nested.of = nestAdminPropertyKeys(of);
199
+ if (Array.isArray(of)) nested.of = of.map((entry) => isNestable(entry) ? nestAdminPropertyKeys(entry) : entry);
200
+ else if (isNestable(of)) nested.of = nestAdminPropertyKeys(of);
183
201
  return nested;
184
202
  }
185
203
  //#endregion
186
204
  export { nestAdminPropertyKeys as i, ADMIN_PROPERTY_KEYS as n, nestAdminCollectionKeys as r, ADMIN_COLLECTION_KEYS as t };
187
205
 
188
- //# sourceMappingURL=admin_block-0Xu0r6eZ.js.map
206
+ //# sourceMappingURL=admin_block-DxKLmdiv.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"admin_block-0Xu0r6eZ.js","names":[],"sources":["../../types/src/types/admin_block.ts"],"sourcesContent":["/**\n * The keys of a collection's admin block, as data.\n *\n * There is no *type* for the block in this package any more, and that is the point:\n * `admin` is not declared on `BaseCollectionConfig` or on any property here, so a\n * BaaS install cannot even write one. `@rebasepro/cms-types` adds the field back by\n * declaration merging, which is why installing it is what makes the admin surface\n * appear.\n *\n * The *list* still has to live here, because three runtime consumers need it and two\n * of them are core — see below.\n */\n\n/**\n * Every key that belongs inside a collection's `admin` block, as data.\n *\n * The type that describes these fields is `AdminCollectionOptions` in\n * `@rebasepro/cms-types`, and it is erased at build time — but three runtime\n * consumers need the list, and two of them are core:\n *\n * - `serializeCollections`, to drop the block from the contract\n * - the ts-morph schema editor in `@rebasepro/server`, which rewrites collection\n * files on disk from the admin panel and has to know where each key goes. A key\n * missing from this list gets written to the *top level* of the file, where the\n * backend ignores it and the panel never finds it again.\n * - the `collections-admin-block` codemod\n *\n * `@rebasepro/cms-types` re-exports this and asserts it names only real option\n * keys; the count is pinned by a test there.\n *\n * @group Models\n */\nexport const ADMIN_COLLECTION_KEYS = [\n \"Actions\",\n \"additionalFields\",\n \"alwaysApplyDefaultValues\",\n \"browserCallbacks\",\n \"components\",\n \"customViews\",\n \"defaultEntityAction\",\n \"defaultFilter\",\n \"defaultSelectedView\",\n \"defaultSize\",\n \"defaultViewMode\",\n \"disableDefaultActions\",\n \"display\",\n \"enabledViews\",\n \"entityActions\",\n \"entityViews\",\n \"exportable\",\n \"filterPresets\",\n \"fixedFilter\",\n \"form\",\n \"formAutoSave\",\n \"formView\",\n \"group\",\n \"hideFromEntityViews\",\n \"hideFromNavigation\",\n \"hideIdFromCollection\",\n \"hideIdFromForm\",\n \"icon\",\n \"includeJsonView\",\n \"inlineEditing\",\n \"kanban\",\n \"listProperties\",\n \"localChangesBackup\",\n \"openEntityMode\",\n \"orderProperty\",\n \"pagination\",\n \"previewProperties\",\n \"propertiesOrder\",\n \"selectionController\",\n \"selectionEnabled\",\n \"sideDialogWidth\",\n \"sort\"\n] as const;\n\n/** A key of a collection's `admin` block. @group Models */\nexport type AdminCollectionKey = typeof ADMIN_COLLECTION_KEYS[number];\n\n/**\n * Every key that belongs inside a *property's* `admin` block, as data.\n *\n * The union of `AdminPropertyOptions` and its per-type extensions\n * (`AdminStringOptions`, `AdminArrayOptions`, …) in `@rebasepro/cms-types`.\n * It lives here for the same reason {@link ADMIN_COLLECTION_KEYS} does: the\n * runtime consumers are core packages that the BaaS guard forbids from\n * importing `@rebasepro/cms-types`. Here it is the boot-time collection\n * validator in `@rebasepro/server`, which has to tell \"you left `readOnly` at\n * the top of the property, where nothing reads it\" apart from \"you invented a\n * key we have never heard of\".\n *\n * `@rebasepro/cms-types` re-exports this and asserts it names only real\n * option keys.\n *\n * @group Models\n */\nexport const ADMIN_PROPERTY_KEYS = [\n \"canAddElements\",\n \"clearable\",\n \"columnWidth\",\n \"customProps\",\n \"disabled\",\n \"expanded\",\n \"Field\",\n \"Filter\",\n \"filterOperators\",\n \"fixedFilter\",\n \"format\",\n \"hideFromCollection\",\n \"includeEntityLink\",\n \"includeId\",\n \"markdown\",\n \"minimalistView\",\n \"multiline\",\n \"Preview\",\n \"previewAsTag\",\n \"previewProperties\",\n \"readOnly\",\n \"renderInForm\",\n \"sortable\",\n \"span\",\n \"spreadChildren\",\n \"urlPreview\",\n \"widget\",\n] as const;\n\n/** A key of a property's `admin` block. @group Models */\nexport type AdminPropertyKey = typeof ADMIN_PROPERTY_KEYS[number];\n\n/**\n * Move flattened admin keys back down into the `admin` block.\n *\n * The admin panel works with a *flat* view model — the block merged onto the\n * collection — so what comes back from a form has `icon` and `defaultViewMode`\n * at the top level while `admin` still holds whatever the file was loaded with.\n * This is the way back.\n *\n * **The top-level value wins.** It is the one the form just wrote; the block is\n * the copy the collection was loaded with, and preferring it resolves every edit\n * in favour of the value the user changed away from.\n *\n * This lives here, next to the key lists, because it had two implementations —\n * `toAdminCollectionConfig` in `@rebasepro/cms-types` and `nestAdminKeys` in\n * `@rebasepro/server`'s schema editor — that agreed on everything except that\n * precedence, which is the only part that decides whether a save is visible.\n *\n * @group Models\n */\nexport function nestAdminKeysOf(\n source: Record<string, unknown>,\n adminKeys: readonly string[]\n): Record<string, unknown> {\n const keys = new Set<string>(adminKeys);\n const top: Record<string, unknown> = {};\n const block: Record<string, unknown> = { ...((source.admin as Record<string, unknown> | undefined) ?? {}) };\n\n for (const [key, value] of Object.entries(source)) {\n if (key === \"admin\") continue;\n if (keys.has(key)) block[key] = value;\n else top[key] = value;\n }\n\n if (Object.keys(block).length > 0) top.admin = block;\n return top;\n}\n\n/**\n * {@link nestAdminKeysOf} for a collection.\n *\n * @group Models\n */\nexport function nestAdminCollectionKeys(collection: Record<string, unknown>): Record<string, unknown> {\n return nestAdminKeysOf(collection, ADMIN_COLLECTION_KEYS);\n}\n\n/**\n * {@link nestAdminKeysOf} for a property, applied to its children too.\n *\n * A map property carries `properties`, an array property carries `of`, and both\n * hold properties with `admin` blocks of their own. A flat `readOnly` left on a\n * child is as dead — and as fatal at the next boot — as one left on the parent,\n * so the walk goes all the way down.\n *\n * @group Models\n */\nexport function nestAdminPropertyKeys(property: Record<string, unknown>): Record<string, unknown> {\n const nested = nestAdminKeysOf(property, ADMIN_PROPERTY_KEYS);\n\n const children = nested.properties;\n if (children && typeof children === \"object\" && !Array.isArray(children)) {\n nested.properties = Object.fromEntries(\n Object.entries(children as Record<string, unknown>).map(([key, child]) => [\n key,\n child && typeof child === \"object\" && !Array.isArray(child)\n ? nestAdminPropertyKeys(child as Record<string, unknown>)\n : child\n ])\n );\n }\n\n const of = nested.of;\n if (Array.isArray(of)) {\n nested.of = of.map(entry => entry && typeof entry === \"object\" && !Array.isArray(entry)\n ? nestAdminPropertyKeys(entry as Record<string, unknown>)\n : entry);\n } else if (of && typeof of === \"object\") {\n nested.of = nestAdminPropertyKeys(of as Record<string, unknown>);\n }\n\n return nested;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAa,wBAAwB;CACjC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;;;;;;;;;;;;;;;;;;AAsBA,IAAa,sBAAsB;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,gBACZ,QACA,WACuB;CACvB,MAAM,OAAO,IAAI,IAAY,SAAS;CACtC,MAAM,MAA+B,CAAC;CACtC,MAAM,QAAiC,EAAE,GAAK,OAAO,SAAiD,CAAC,EAAG;CAE1G,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG;EAC/C,IAAI,QAAQ,SAAS;EACrB,IAAI,KAAK,IAAI,GAAG,GAAG,MAAM,OAAO;OAC3B,IAAI,OAAO;CACpB;CAEA,IAAI,OAAO,KAAK,KAAK,CAAC,CAAC,SAAS,GAAG,IAAI,QAAQ;CAC/C,OAAO;AACX;;;;;;AAOA,SAAgB,wBAAwB,YAA8D;CAClG,OAAO,gBAAgB,YAAY,qBAAqB;AAC5D;;;;;;;;;;;AAYA,SAAgB,sBAAsB,UAA4D;CAC9F,MAAM,SAAS,gBAAgB,UAAU,mBAAmB;CAE5D,MAAM,WAAW,OAAO;CACxB,IAAI,YAAY,OAAO,aAAa,YAAY,CAAC,MAAM,QAAQ,QAAQ,GACnE,OAAO,aAAa,OAAO,YACvB,OAAO,QAAQ,QAAmC,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW,CACtE,KACA,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IACpD,sBAAsB,KAAgC,IACtD,KACV,CAAC,CACL;CAGJ,MAAM,KAAK,OAAO;CAClB,IAAI,MAAM,QAAQ,EAAE,GAChB,OAAO,KAAK,GAAG,KAAI,UAAS,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IAChF,sBAAsB,KAAgC,IACtD,KAAK;MACR,IAAI,MAAM,OAAO,OAAO,UAC3B,OAAO,KAAK,sBAAsB,EAA6B;CAGnE,OAAO;AACX"}
1
+ {"version":3,"file":"admin_block-DxKLmdiv.js","names":[],"sources":["../../types/src/types/admin_block.ts"],"sourcesContent":["/**\n * The keys of a collection's admin block, as data.\n *\n * There is no *type* for the block in this package any more, and that is the point:\n * `admin` is not declared on `BaseCollectionConfig` or on any property here, so a\n * BaaS install cannot even write one. `@rebasepro/cms-types` adds the field back by\n * declaration merging, which is why installing it is what makes the admin surface\n * appear.\n *\n * The *list* still has to live here, because three runtime consumers need it and two\n * of them are core — see below.\n */\n\n/**\n * Every key that belongs inside a collection's `admin` block, as data.\n *\n * The type that describes these fields is `AdminCollectionOptions` in\n * `@rebasepro/cms-types`, and it is erased at build time — but three runtime\n * consumers need the list, and two of them are core:\n *\n * - `serializeCollections`, to drop the block from the contract\n * - the ts-morph schema editor in `@rebasepro/server`, which rewrites collection\n * files on disk from the admin panel and has to know where each key goes. A key\n * missing from this list gets written to the *top level* of the file, where the\n * backend ignores it and the panel never finds it again.\n * - the `collections-admin-block` codemod\n *\n * `@rebasepro/cms-types` re-exports this and asserts it names only real option\n * keys; the count is pinned by a test there.\n *\n * @group Models\n */\nexport const ADMIN_COLLECTION_KEYS = [\n \"Actions\",\n \"additionalFields\",\n \"alwaysApplyDefaultValues\",\n \"browserCallbacks\",\n \"components\",\n \"customViews\",\n \"defaultEntityAction\",\n \"defaultFilter\",\n \"defaultSelectedView\",\n \"defaultSize\",\n \"defaultViewMode\",\n \"disableDefaultActions\",\n \"display\",\n \"enabledViews\",\n \"entityActions\",\n \"entityViews\",\n \"exportable\",\n \"filterPresets\",\n \"fixedFilter\",\n \"form\",\n \"formAutoSave\",\n \"formView\",\n \"group\",\n \"hideFromEntityViews\",\n \"hideFromNavigation\",\n \"hideIdFromCollection\",\n \"hideIdFromForm\",\n \"icon\",\n \"includeJsonView\",\n \"inlineEditing\",\n \"kanban\",\n \"listProperties\",\n \"localChangesBackup\",\n \"openEntityMode\",\n \"orderProperty\",\n \"pagination\",\n \"previewProperties\",\n \"propertiesOrder\",\n \"selectionController\",\n \"selectionEnabled\",\n \"sideDialogWidth\",\n \"sort\"\n] as const;\n\n/** A key of a collection's `admin` block. @group Models */\nexport type AdminCollectionKey = typeof ADMIN_COLLECTION_KEYS[number];\n\n/**\n * Every key that belongs inside a *property's* `admin` block, as data.\n *\n * The union of `AdminPropertyOptions` and its per-type extensions\n * (`AdminStringOptions`, `AdminArrayOptions`, …) in `@rebasepro/cms-types`.\n * It lives here for the same reason {@link ADMIN_COLLECTION_KEYS} does: the\n * runtime consumers are core packages that the BaaS guard forbids from\n * importing `@rebasepro/cms-types`. Here it is the boot-time collection\n * validator in `@rebasepro/server`, which has to tell \"you left `readOnly` at\n * the top of the property, where nothing reads it\" apart from \"you invented a\n * key we have never heard of\".\n *\n * `@rebasepro/cms-types` re-exports this and asserts it names only real\n * option keys.\n *\n * @group Models\n */\nexport const ADMIN_PROPERTY_KEYS = [\n \"canAddElements\",\n \"clearable\",\n \"columnWidth\",\n \"customProps\",\n \"disabled\",\n \"expanded\",\n \"Field\",\n \"Filter\",\n \"filterOperators\",\n \"fixedFilter\",\n \"format\",\n \"hideFromCollection\",\n \"includeEntityLink\",\n \"includeId\",\n \"markdown\",\n \"minimalistView\",\n \"multiline\",\n \"Preview\",\n \"previewAsTag\",\n \"previewProperties\",\n \"readOnly\",\n \"renderInForm\",\n \"sortable\",\n \"span\",\n \"spreadChildren\",\n \"urlPreview\",\n \"widget\",\n] as const;\n\n/** A key of a property's `admin` block. @group Models */\nexport type AdminPropertyKey = typeof ADMIN_PROPERTY_KEYS[number];\n\n/**\n * Move flattened admin keys back down into the `admin` block.\n *\n * The admin panel works with a *flat* view model — the block merged onto the\n * collection — so what comes back from a form has `icon` and `defaultViewMode`\n * at the top level while `admin` still holds whatever the file was loaded with.\n * This is the way back.\n *\n * **The top-level value wins.** It is the one the form just wrote; the block is\n * the copy the collection was loaded with, and preferring it resolves every edit\n * in favour of the value the user changed away from.\n *\n * This lives here, next to the key lists, because it had two implementations —\n * `toAdminCollectionConfig` in `@rebasepro/cms-types` and `nestAdminKeys` in\n * `@rebasepro/server`'s schema editor — that agreed on everything except that\n * precedence, which is the only part that decides whether a save is visible.\n *\n * @group Models\n */\nexport function nestAdminKeysOf(\n source: Record<string, unknown>,\n adminKeys: readonly string[]\n): Record<string, unknown> {\n const keys = new Set<string>(adminKeys);\n const top: Record<string, unknown> = {};\n const block: Record<string, unknown> = { ...((source.admin as Record<string, unknown> | undefined) ?? {}) };\n\n for (const [key, value] of Object.entries(source)) {\n if (key === \"admin\") continue;\n if (keys.has(key)) block[key] = value;\n else top[key] = value;\n }\n\n if (Object.keys(block).length > 0) top.admin = block;\n return top;\n}\n\n/**\n * {@link nestAdminKeysOf} for a collection.\n *\n * @group Models\n */\nexport function nestAdminCollectionKeys(collection: Record<string, unknown>): Record<string, unknown> {\n return nestAdminKeysOf(collection, ADMIN_COLLECTION_KEYS);\n}\n\n/** A record of properties, keyed by name — a map's `properties`, or a `oneOf` block's. */\nfunction nestEachProperty(properties: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(\n Object.entries(properties).map(([key, child]) => [\n key,\n isNestable(child) ? nestAdminPropertyKeys(child) : child\n ])\n );\n}\n\n/** Anything the walk can descend into: a plain object, not an array. */\nfunction isNestable(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n}\n\n/**\n * {@link nestAdminKeysOf} for a property, applied to its children too.\n *\n * A map property carries `properties`, an array property carries `of`, and an\n * array of typed blocks carries `oneOf.properties` — a record of properties like\n * a map's. All of them hold properties with `admin` blocks of their own. A flat\n * `readOnly` left on a child is as dead — and as fatal at the next boot — as one\n * left on the parent, so the walk goes all the way down.\n *\n * `oneOf` was the container this walk did not know about, and it is the one the\n * block-based collection templates are built out of: every block inside them\n * kept its flat `markdown`, and the collection they created would not boot.\n *\n * @group Models\n */\nexport function nestAdminPropertyKeys(property: Record<string, unknown>): Record<string, unknown> {\n const nested = nestAdminKeysOf(property, ADMIN_PROPERTY_KEYS);\n\n const children = nested.properties;\n if (isNestable(children)) {\n nested.properties = nestEachProperty(children);\n }\n\n // `oneOf` is not itself a property — it is a block holding `properties`\n // alongside `typeField`, `valueField` and `propertiesOrder`, none of which\n // may be walked as one.\n const oneOf = nested.oneOf;\n if (isNestable(oneOf) && isNestable(oneOf.properties)) {\n nested.oneOf = { ...oneOf, properties: nestEachProperty(oneOf.properties) };\n }\n\n const of = nested.of;\n if (Array.isArray(of)) {\n nested.of = of.map(entry => isNestable(entry) ? nestAdminPropertyKeys(entry) : entry);\n } else if (isNestable(of)) {\n nested.of = nestAdminPropertyKeys(of);\n }\n\n return nested;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAa,wBAAwB;CACjC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;;;;;;;;;;;;;;;;;;AAsBA,IAAa,sBAAsB;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,gBACZ,QACA,WACuB;CACvB,MAAM,OAAO,IAAI,IAAY,SAAS;CACtC,MAAM,MAA+B,CAAC;CACtC,MAAM,QAAiC,EAAE,GAAK,OAAO,SAAiD,CAAC,EAAG;CAE1G,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG;EAC/C,IAAI,QAAQ,SAAS;EACrB,IAAI,KAAK,IAAI,GAAG,GAAG,MAAM,OAAO;OAC3B,IAAI,OAAO;CACpB;CAEA,IAAI,OAAO,KAAK,KAAK,CAAC,CAAC,SAAS,GAAG,IAAI,QAAQ;CAC/C,OAAO;AACX;;;;;;AAOA,SAAgB,wBAAwB,YAA8D;CAClG,OAAO,gBAAgB,YAAY,qBAAqB;AAC5D;;AAGA,SAAS,iBAAiB,YAA8D;CACpF,OAAO,OAAO,YACV,OAAO,QAAQ,UAAU,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW,CAC7C,KACA,WAAW,KAAK,IAAI,sBAAsB,KAAK,IAAI,KACvD,CAAC,CACL;AACJ;;AAGA,SAAS,WAAW,OAAkD;CAClE,OAAO,QAAQ,KAAK,KAAK,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK;AAC9E;;;;;;;;;;;;;;;;AAiBA,SAAgB,sBAAsB,UAA4D;CAC9F,MAAM,SAAS,gBAAgB,UAAU,mBAAmB;CAE5D,MAAM,WAAW,OAAO;CACxB,IAAI,WAAW,QAAQ,GACnB,OAAO,aAAa,iBAAiB,QAAQ;CAMjD,MAAM,QAAQ,OAAO;CACrB,IAAI,WAAW,KAAK,KAAK,WAAW,MAAM,UAAU,GAChD,OAAO,QAAQ;EAAE,GAAG;EAAO,YAAY,iBAAiB,MAAM,UAAU;CAAE;CAG9E,MAAM,KAAK,OAAO;CAClB,IAAI,MAAM,QAAQ,EAAE,GAChB,OAAO,KAAK,GAAG,KAAI,UAAS,WAAW,KAAK,IAAI,sBAAsB,KAAK,IAAI,KAAK;MACjF,IAAI,WAAW,EAAE,GACpB,OAAO,KAAK,sBAAsB,EAAE;CAGxC,OAAO;AACX"}
@@ -8,6 +8,21 @@
8
8
  * saved or silently reverted to the value the user had just changed away from.
9
9
  */
10
10
  export declare function nestAdminKeys(collectionData: Record<string, unknown>): Record<string, unknown>;
11
+ /**
12
+ * {@link nestAdminKeys} for a whole collection — its properties included.
13
+ *
14
+ * `nestAdminKeys` only ever moved the COLLECTION's presentation keys. A
15
+ * property's — `markdown`, `multiline`, `previewAsTag` — went to disk exactly as
16
+ * the panel sent them, at the top level of the property, which the boot
17
+ * validator treats as fatal. `saveProperty` nests them; `saveCollection` did
18
+ * not, and *creating* a collection is a `saveCollection`.
19
+ *
20
+ * So a collection created from a template wrote a file that could not boot, and
21
+ * took the whole project down with it — the loader imports every collection in
22
+ * the directory, so one bad file stops `rebase dev` from starting at all. The
23
+ * panel reported success.
24
+ */
25
+ export declare function nestAdminKeysDeep(collectionData: Record<string, unknown>): Record<string, unknown>;
11
26
  export declare class AstSchemaEditor {
12
27
  private project;
13
28
  private collectionsDir;
@@ -3,7 +3,7 @@ import __rebaseProcess from "process";
3
3
  globalThis.process ??= __rebaseProcess;
4
4
  __rebaseCreateRequire(import.meta.url);
5
5
  import "./src-Br6ARbs6.js";
6
- import { i as nestAdminPropertyKeys, r as nestAdminCollectionKeys } from "./admin_block-0Xu0r6eZ.js";
6
+ import { i as nestAdminPropertyKeys, r as nestAdminCollectionKeys } from "./admin_block-DxKLmdiv.js";
7
7
  import * as fs$1 from "fs";
8
8
  import * as path$1 from "path";
9
9
  import { IndentationText, Node, Project, SyntaxKind } from "ts-morph";
@@ -58,6 +58,26 @@ var RawExpression = class {
58
58
  function nestAdminKeys(collectionData) {
59
59
  return nestAdminCollectionKeys(collectionData);
60
60
  }
61
+ /**
62
+ * {@link nestAdminKeys} for a whole collection — its properties included.
63
+ *
64
+ * `nestAdminKeys` only ever moved the COLLECTION's presentation keys. A
65
+ * property's — `markdown`, `multiline`, `previewAsTag` — went to disk exactly as
66
+ * the panel sent them, at the top level of the property, which the boot
67
+ * validator treats as fatal. `saveProperty` nests them; `saveCollection` did
68
+ * not, and *creating* a collection is a `saveCollection`.
69
+ *
70
+ * So a collection created from a template wrote a file that could not boot, and
71
+ * took the whole project down with it — the loader imports every collection in
72
+ * the directory, so one bad file stops `rebase dev` from starting at all. The
73
+ * panel reported success.
74
+ */
75
+ function nestAdminKeysDeep(collectionData) {
76
+ const nested = nestAdminKeys(collectionData);
77
+ const properties = nested.properties;
78
+ if (properties && typeof properties === "object" && !Array.isArray(properties)) nested.properties = Object.fromEntries(Object.entries(properties).map(([key, property]) => [key, property && typeof property === "object" && !Array.isArray(property) ? nestAdminPropertyKeys(property) : property]));
79
+ return nested;
80
+ }
61
81
  var AstSchemaEditor = class AstSchemaEditor {
62
82
  project;
63
83
  collectionsDir;
@@ -322,7 +342,7 @@ var AstSchemaEditor = class AstSchemaEditor {
322
342
  const newFilePath = this.safePath(`${safeId}.ts`);
323
343
  if (fs$1.existsSync(newFilePath)) throw new Error(`Refusing to overwrite ${newFilePath}: a file for "${collectionId}" already exists but could not be parsed.`);
324
344
  const varName = `${AstSchemaEditor.collectionVarName(safeId)}Collection`;
325
- file = this.project.createSourceFile(newFilePath, `import { CollectionConfig } from "@rebasepro/types";\n\nconst ${varName}: CollectionConfig = ${this.convertJsonToAstString(nestAdminKeys(collectionData))};\n\nexport default ${varName};\n`);
345
+ file = this.project.createSourceFile(newFilePath, `import { CollectionConfig } from "@rebasepro/types";\n\nconst ${varName}: CollectionConfig = ${this.convertJsonToAstString(nestAdminKeysDeep(collectionData))};\n\nexport default ${varName};\n`);
326
346
  } else {
327
347
  if (!partial) {
328
348
  if (!("securityRules" in collectionData) || collectionData.securityRules === void 0 || Array.isArray(collectionData.securityRules) && collectionData.securityRules.length === 0) {
@@ -331,7 +351,7 @@ var AstSchemaEditor = class AstSchemaEditor {
331
351
  delete collectionData["securityRules"];
332
352
  }
333
353
  }
334
- collectionData = nestAdminKeys(collectionData);
354
+ collectionData = nestAdminKeysDeep(collectionData);
335
355
  for (const key of Object.keys(collectionData)) {
336
356
  if (key === "relations") {
337
357
  this.writeRelations(collectionId, file, collectionObj, collectionData[key]);
@@ -442,4 +462,4 @@ var AstSchemaEditor = class AstSchemaEditor {
442
462
  //#endregion
443
463
  export { AstSchemaEditor };
444
464
 
445
- //# sourceMappingURL=ast-schema-editor-C6mDz0XN.js.map
465
+ //# sourceMappingURL=ast-schema-editor-CslO8Oje.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ast-schema-editor-CslO8Oje.js","names":[],"sources":["../src/api/ast-schema-editor.ts"],"sourcesContent":["import { Project, SyntaxKind, Node, ObjectLiteralExpression, ObjectLiteralElementLike, PropertyAssignment, SourceFile, IndentationText } from \"ts-morph\";\nimport { nestAdminCollectionKeys, nestAdminPropertyKeys } from \"@rebasepro/types\";\nimport * as path from \"path\";\nimport * as fs from \"fs\";\n\n/**\n * The helpers a collection file may be wrapped in.\n *\n * `rebase init` scaffolds every collection as\n * `const postsCollection = defineCollection({ … })` — a call expression, not the\n * bare object literal `rebase introspect` emits. An editor that only understood\n * the bare form found nothing to patch in any stock project, and then rewrote the\n * file from the panel's JSON: no wrapper, no imports, no relation thunks.\n */\nconst COLLECTION_FACTORIES = new Set([\"defineCollection\"]);\n\n/**\n * The only relation-target expression this editor will write through verbatim.\n *\n * A relation's `target` is emitted as SOURCE, not as a string literal, because\n * it has to be `() => otherCollection`. The test for \"is this already a thunk?\"\n * used to be \"does it contain an arrow\", which `() => { require(\"child_process\")\n * .execSync(\"…\") }` also satisfies — and `rebase dev` re-imports the file the\n * moment it changes, so the payload ran without anyone deploying anything.\n *\n * An arrow returning a single identifier is the whole grammar. Anything else is\n * read as a collection name and turned into a thunk by `targetThunk`, which\n * resolves it against the collections directory and refuses a name with no file.\n */\nconst ARROW_TO_IDENTIFIER = /^\\(\\s*\\)\\s*=>\\s*[A-Za-z_$][A-Za-z0-9_$]*$/;\n\n/**\n * A value that must be emitted as source code rather than as JSON.\n *\n * Everything reaching the writer has been through `JSON.stringify` on the wire,\n * so a function-valued key arrives either missing or as a string. A relation's\n * `target` is a thunk in the file and a slug in the payload; this is how the\n * thunk gets written back.\n */\nclass RawExpression {\n constructor(public readonly text: string) {\n }\n}\n\n/**\n * Move presentation keys into the `admin` block.\n *\n * The rule itself lives in `@rebasepro/types`, next to `ADMIN_COLLECTION_KEYS`,\n * because `@rebasepro/cms-types` has to apply the identical one on the panel's\n * side and this package cannot import that one. Two copies used to exist and\n * they disagreed about precedence, which decided whether a presentation edit was\n * saved or silently reverted to the value the user had just changed away from.\n */\nexport function nestAdminKeys(collectionData: Record<string, unknown>): Record<string, unknown> {\n return nestAdminCollectionKeys(collectionData);\n}\n\n/**\n * {@link nestAdminKeys} for a whole collection — its properties included.\n *\n * `nestAdminKeys` only ever moved the COLLECTION's presentation keys. A\n * property's — `markdown`, `multiline`, `previewAsTag` — went to disk exactly as\n * the panel sent them, at the top level of the property, which the boot\n * validator treats as fatal. `saveProperty` nests them; `saveCollection` did\n * not, and *creating* a collection is a `saveCollection`.\n *\n * So a collection created from a template wrote a file that could not boot, and\n * took the whole project down with it — the loader imports every collection in\n * the directory, so one bad file stops `rebase dev` from starting at all. The\n * panel reported success.\n */\nexport function nestAdminKeysDeep(collectionData: Record<string, unknown>): Record<string, unknown> {\n const nested = nestAdminKeys(collectionData);\n const properties = nested.properties;\n if (properties && typeof properties === \"object\" && !Array.isArray(properties)) {\n nested.properties = Object.fromEntries(\n Object.entries(properties as Record<string, unknown>).map(([key, property]) => [\n key,\n property && typeof property === \"object\" && !Array.isArray(property)\n ? nestAdminPropertyKeys(property as Record<string, unknown>)\n : property\n ])\n );\n }\n return nested;\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 /**\n * The variable name the generated file binds the collection to.\n *\n * `sanitizeCollectionId` guards the FILENAME, and permits hyphens and a\n * leading digit — both legal in a filename and neither legal in a\n * JavaScript identifier. So a collection created from the admin panel as\n * `my-notes` (the documented slug shape) or `2024 Archive` (auto-slugged to\n * `2024_archive`) wrote:\n *\n * const my-notesCollection: CollectionConfig = … \"',' expected\"\n * const 2024_archiveCollection: CollectionConfig = … \"Numeric separators\n * are not allowed here\"\n *\n * The panel reported success, and the next boot failed for EVERY collection\n * in the directory — the loader imports all of them — while the editor could\n * no longer parse the file it had just written, so it could not fix itself.\n *\n * Separators camel-case rather than vanish, so `my-notes` and `my_notes`\n * stay distinct; a leading digit is prefixed rather than stripped, so\n * `2024_archive` stays distinct from `archive`. Byte-identical for every\n * slug that already produced a valid identifier.\n */\n private static collectionVarName(safeId: string): string {\n const camel = safeId.replace(/[-_]+([a-zA-Z0-9])/g, (_, char: string) => char.toUpperCase());\n const identifier = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(camel) ? camel : `c${camel.replace(/^[0-9]/, (d) => d)}`;\n return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier) ? identifier : `c${identifier.replace(/[^A-Za-z0-9_$]/g, \"\")}`;\n }\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 /**\n * Find the object literal a collection is declared with, through whatever\n * wraps it.\n *\n * `defineCollection({ … })` is the shape `rebase init` writes and the one the\n * docs recommend; `satisfies` / `as` / parentheses are the other ways an\n * author can dress the same literal. Returning `null` for any of them meant\n * the three callers below each failed differently, and the worst of them\n * overwrote the file.\n */\n private unwrapCollectionObject(node: Node | undefined): ObjectLiteralExpression | null {\n if (!node) return null;\n if (Node.isObjectLiteralExpression(node)) return node;\n if (Node.isParenthesizedExpression(node) ||\n Node.isAsExpression(node) ||\n Node.isSatisfiesExpression(node) ||\n Node.isTypeAssertion(node) ||\n Node.isNonNullExpression(node)) {\n return this.unwrapCollectionObject(node.getExpression());\n }\n if (Node.isCallExpression(node)) {\n // `defineCollection`, `admin.defineCollection`, `defineCollection<Post>`\n const callee = node.getExpression().getText().split(\".\").pop();\n if (callee && COLLECTION_FACTORIES.has(callee)) {\n return this.unwrapCollectionObject(node.getArguments()[0]);\n }\n }\n return null;\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 const unwrapped = this.unwrapCollectionObject(varDecl?.getInitializer());\n if (unwrapped) return unwrapped;\n } else {\n // `export default defineCollection({ … })`\n const unwrapped = this.unwrapCollectionObject(expr);\n if (unwrapped) return unwrapped;\n }\n }\n }\n // Fallback: the first VariableDeclaration that holds a collection literal\n for (const varDecl of file.getVariableDeclarations()) {\n const init = this.unwrapCollectionObject(varDecl.getInitializer());\n if (init) return init;\n }\n return null;\n }\n\n /**\n * The collection's object literal, or a refusal that says what to do.\n *\n * Every caller needs this to be all-or-nothing: a missing object literal used\n * to mean \"throw\", \"report success and do nothing\" and \"recreate the file\n * from scratch\" depending on which method you called.\n */\n private requireCollectionObject(collectionId: string): ObjectLiteralExpression {\n const file = this.getCollectionFile(collectionId);\n if (!file) {\n throw new Error(`Collection \"${collectionId}\" has no file at ${path.join(this.collectionsDir, `${collectionId}.ts`)}.`);\n }\n const collectionObj = this.getCollectionObject(collectionId);\n if (!collectionObj) {\n throw new Error(this.unreadableFileMessage(collectionId, file));\n }\n return collectionObj;\n }\n\n private unreadableFileMessage(collectionId: string, file: SourceFile): string {\n return `Could not find the collection object in ${file.getFilePath()}. ` +\n \"The schema editor can only edit a collection declared as `const x = defineCollection({ … })` \" +\n \"or `const x: CollectionConfig = { … }` and exported as the file's default. \" +\n `Edit \"${collectionId}\" in code instead.`;\n }\n\n /** Look a key up on an object literal, quoted or not. */\n private findProperty(obj: ObjectLiteralExpression, name: string): ObjectLiteralElementLike | undefined {\n return obj.getProperty((p: ObjectLiteralElementLike) =>\n \"getName\" in p &&\n typeof (p as PropertyAssignment).getName === \"function\" &&\n ((p as PropertyAssignment).getName() === name ||\n (p as PropertyAssignment).getName() === `\"${name}\"` ||\n (p as PropertyAssignment).getName() === `'${name}'`));\n }\n\n private static quoteKey(key: string): string {\n return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);\n }\n\n private static isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value) && !(value instanceof RawExpression);\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 instanceof RawExpression) {\n return obj.text;\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 === \"browserCallbacks\" || name === \"permissions\" || name === \"securityRules\") {\n // Preserve this property exactly as it was\n preservedProps.push(`${AstSchemaEditor.quoteKey(name)}: ${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 = AstSchemaEditor.quoteKey(key);\n\n // If the value is an object, pass the old AST node to recurse\n let childAstNode: ObjectLiteralExpression | undefined;\n if (oldAstNode && AstSchemaEditor.isPlainObject(record[key])) {\n const oldProp = this.findProperty(oldAstNode, key);\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 /**\n * Write only the keys the patch names, leaving every sibling alone.\n *\n * A patch says what changed, not what the collection is. The panel sends one\n * — `{ propertiesOrder }` is what adding a column posts — and rewriting the\n * `admin` block from it deleted the collection's icon, group, list columns\n * and kanban config in the same write.\n */\n private mergeIntoObjectLiteral(target: ObjectLiteralExpression, data: Record<string, unknown>, indentLevel: number): void {\n for (const [key, value] of Object.entries(data)) {\n const existing = this.findProperty(target, key);\n const existingObj = existing && existing.isKind(SyntaxKind.PropertyAssignment)\n ? existing.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression)\n : undefined;\n\n if (existingObj && AstSchemaEditor.isPlainObject(value)) {\n this.mergeIntoObjectLiteral(existingObj, value, indentLevel + 1);\n continue;\n }\n\n const initializer = this.convertJsonToAstString(value, indentLevel, existingObj);\n if (existing && existing.isKind(SyntaxKind.PropertyAssignment)) {\n existing.setInitializer(initializer);\n } else {\n target.addPropertyAssignment({\n name: AstSchemaEditor.quoteKey(key),\n initializer\n });\n }\n }\n }\n\n public async saveProperty(collectionId: string, propertyKey: string, propertyConfig: Record<string, unknown>) {\n const collectionObj = this.requireCollectionObject(collectionId);\n\n // The panel's property forms bind to the flat names — `readOnly`,\n // `hideFromCollection` — while on disk they belong inside the property's\n // own `admin` block. Written flat they are not merely ignored: the boot\n // validator treats a moved key as fatal.\n const nestedConfig = nestAdminPropertyKeys(propertyConfig);\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 = this.findProperty(propsObj, propertyKey);\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(nestedConfig, 2, oldPropAstNode);\n\n if (existingProp) {\n if (existingProp.isKind(SyntaxKind.PropertyAssignment)) {\n existingProp.setInitializer(newInitializer);\n }\n } else {\n propsObj.addPropertyAssignment({\n name: AstSchemaEditor.quoteKey(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.requireCollectionObject(collectionId);\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 = this.findProperty(propsObj, propertyKey);\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 /**\n * Write a collection back to its file.\n *\n * `partial` is the difference between \"this is the collection\" and \"this is\n * what changed about it\". The panel sends both — a full save from the editor\n * dialog, and a one-key patch whenever a property is added, deleted or\n * reordered — and they cannot be told apart by looking at the payload. Read\n * as a full save, a patch deletes everything it does not mention, including\n * `securityRules`; the loader then hands the collection the directory\n * default, which in the scaffold is `access: \"public\"`.\n */\n public async saveCollection(collectionId: string, collectionData: Record<string, unknown>, options: { partial?: boolean } = {}) {\n const partial = options.partial === true;\n let file = this.getCollectionFile(collectionId);\n const collectionObj = file ? this.getCollectionObject(collectionId) : null;\n\n if (file && !collectionObj) {\n // The file is there and we could not read it. Recreating it from the\n // panel's JSON would drop the imports, the callbacks and the relation\n // thunks that JSON cannot carry.\n throw new Error(this.unreadableFileMessage(collectionId, file));\n }\n\n if (!file || !collectionObj) {\n if (partial) {\n throw new Error(`Cannot apply a partial update to \"${collectionId}\": it has no collection file yet.`);\n }\n // Create a new file\n const safeId = this.sanitizeCollectionId(collectionId);\n const newFilePath = this.safePath(`${safeId}.ts`);\n if (fs.existsSync(newFilePath)) {\n throw new Error(`Refusing to overwrite ${newFilePath}: a file for \"${collectionId}\" already exists but could not be parsed.`);\n }\n const varName = `${AstSchemaEditor.collectionVarName(safeId)}Collection`;\n file = this.project.createSourceFile(newFilePath, `import { CollectionConfig } from \"@rebasepro/types\";\\n\\nconst ${varName}: CollectionConfig = ${this.convertJsonToAstString(nestAdminKeysDeep(collectionData))};\\n\\nexport default ${varName};\\n`);\n } else {\n // Update root level properties gracefully\n\n if (!partial) {\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\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. A\n // property's presentation keys are worse than ignored: they are fatal\n // at the next boot, which is why this goes all the way down.\n collectionData = nestAdminKeysDeep(collectionData);\n\n for (const key of Object.keys(collectionData)) {\n if (key === \"relations\") {\n this.writeRelations(collectionId, file, collectionObj, collectionData[key]);\n continue;\n }\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 if (partial && oldAstNode && AstSchemaEditor.isPlainObject(collectionData[key])) {\n this.mergeIntoObjectLiteral(oldAstNode, collectionData[key] as Record<string, unknown>, 2);\n continue;\n }\n\n const newInit = this.convertJsonToAstString(collectionData[key], 1, oldAstNode);\n if (prop) {\n prop.setInitializer(newInit);\n } else {\n collectionObj.addPropertyAssignment({\n // `quoteKey`, like every other site that emits a key.\n // This one did not, and ts-morph writes a property name\n // out verbatim — so a top-level key of\n // `injected: (() => { … })(), tail` closed the property\n // and opened an expression, which `rebase dev` then\n // re-imported and ran. The rest of this file has always\n // quoted; this was the one that did not.\n name: AstSchemaEditor.quoteKey(key),\n initializer: newInit\n });\n }\n }\n }\n if (file) {\n file.formatText();\n }\n await this.project.save();\n }\n\n /**\n * Write the collection-level `relations` array.\n *\n * Relations are the one key whose values are not all data: `target` is a\n * thunk in the file, and the panel sends the target collection's slug (or,\n * for a relation it did not touch, nothing at all — `JSON.stringify` drops\n * the function). So each entry's target is resolved in that order: a slug\n * becomes `() => xCollection` plus the import it needs, and anything else\n * falls back to the thunk already in the file.\n *\n * This used to be a `continue` with a comment saying relations were handled\n * elsewhere. Nothing handled them; every edit in the Relations tab was\n * dropped without a word.\n */\n private writeRelations(collectionId: string, file: SourceFile, collectionObj: ObjectLiteralExpression, value: unknown): void {\n if (!Array.isArray(value)) return;\n\n const existing = this.findProperty(collectionObj, \"relations\");\n const oldArray = existing && existing.isKind(SyntaxKind.PropertyAssignment)\n ? existing.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression)\n : undefined;\n\n const oldElements = oldArray?.getElements() ?? [];\n const oldTargetsByName = new Map<string, string>();\n // Only for entries the file left unnamed — `relationName` is optional, and\n // those cannot be matched any other way. Positional matching is off the\n // moment the array's length changes, because then a position no longer\n // means the same relation.\n const anonymousTargetsByIndex: (string | undefined)[] = [];\n oldElements.forEach((element, index) => {\n const elementObj = element.asKind(SyntaxKind.ObjectLiteralExpression);\n if (!elementObj) return;\n const targetProp = this.findProperty(elementObj, \"target\");\n const targetInit = targetProp && targetProp.isKind(SyntaxKind.PropertyAssignment)\n ? targetProp.getInitializer()\n : undefined;\n if (!targetInit) return;\n\n const nameProp = this.findProperty(elementObj, \"relationName\");\n const nameInit = nameProp && nameProp.isKind(SyntaxKind.PropertyAssignment)\n ? nameProp.getInitializerIfKind(SyntaxKind.StringLiteral)\n : undefined;\n if (nameInit) oldTargetsByName.set(nameInit.getLiteralValue(), targetInit.getText());\n else anonymousTargetsByIndex[index] = targetInit.getText();\n });\n\n const items = value.map((entry, index) => {\n if (!AstSchemaEditor.isPlainObject(entry)) return entry;\n const relation: Record<string, unknown> = { ...entry };\n const relationName = typeof relation.relationName === \"string\" ? relation.relationName : undefined;\n const rawTarget = relation.target;\n\n let targetText: string | undefined;\n if (typeof rawTarget === \"string\" && rawTarget.trim().length > 0) {\n const trimmed = rawTarget.trim();\n // A target that already looks like a thunk is written into the\n // file as source, so \"contains an arrow\" is not a good enough\n // reason to trust it: `() => { require(\"child_process\")… }` also\n // contains one. Only the exact shape this emits is accepted —\n // an arrow returning one identifier — and anything else is\n // treated as a collection NAME and turned into a thunk here,\n // which is the path that was always safe.\n targetText = ARROW_TO_IDENTIFIER.test(trimmed)\n ? trimmed\n : this.targetThunk(file, trimmed);\n } else if (relationName && oldTargetsByName.has(relationName)) {\n targetText = oldTargetsByName.get(relationName);\n } else if (value.length === oldElements.length) {\n targetText = anonymousTargetsByIndex[index];\n }\n\n if (!targetText) {\n throw new Error(`Relation \"${relationName ?? `#${index}`}\" on collection \"${collectionId}\" has no target collection. Pick one before saving.`);\n }\n\n relation.target = new RawExpression(targetText);\n return relation;\n });\n\n const initializer = this.convertJsonToAstString(items, 1);\n if (existing && existing.isKind(SyntaxKind.PropertyAssignment)) {\n existing.setInitializer(initializer);\n } else {\n collectionObj.addPropertyAssignment({ name: \"relations\", initializer });\n }\n }\n\n /**\n * `() => targetCollection` for a target named by its slug, importing it if\n * the file does not already.\n */\n private targetThunk(file: SourceFile, targetSlug: string): string {\n const targetFile = this.getCollectionFile(targetSlug);\n if (!targetFile) {\n throw new Error(`Cannot link to collection \"${targetSlug}\": no file for it in ${this.collectionsDir}.`);\n }\n\n const identifier = this.getDefaultExportName(targetFile);\n if (!identifier) {\n throw new Error(`Cannot link to collection \"${targetSlug}\": ${targetFile.getFilePath()} has no default export to import.`);\n }\n\n if (targetFile.getFilePath() !== file.getFilePath() && !this.hasDefaultImport(file, identifier)) {\n const relative = path.relative(path.dirname(file.getFilePath()), targetFile.getFilePath())\n .split(path.sep)\n .join(\"/\")\n .replace(/\\.tsx?$/, \".js\");\n file.addImportDeclaration({\n defaultImport: identifier,\n moduleSpecifier: relative.startsWith(\".\") ? relative : `./${relative}`\n });\n }\n\n return `() => ${identifier}`;\n }\n\n private hasDefaultImport(file: SourceFile, identifier: string): boolean {\n return file.getImportDeclarations().some(decl => decl.getDefaultImport()?.getText() === identifier);\n }\n\n private getDefaultExportName(file: SourceFile): string | undefined {\n const declaration = file.getDefaultExportSymbol()?.getDeclarations()[0];\n const expr = declaration?.asKind(SyntaxKind.ExportAssignment)?.getExpression();\n if (expr && expr.getKind() === SyntaxKind.Identifier) return expr.getText();\n\n for (const varDecl of file.getVariableDeclarations()) {\n if (this.unwrapCollectionObject(varDecl.getInitializer())) return varDecl.getName();\n }\n return undefined;\n }\n\n public async deleteCollection(collectionId: string) {\n const file = this.getCollectionFile(collectionId);\n if (file) {\n file.deleteImmediatelySync();\n }\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"contract-routes-eLxV0le1.js","names":[],"sources":["../../types/src/types/project_manifest.ts","../../types/src/types/collection_contract.ts","../../types/src/types/schema_version.ts","../src/api/contract-routes.ts"],"sourcesContent":["/**\n * The project manifest (`rebase.json`) and the build artifacts derived from it.\n *\n * Three separate documents live in this file, and keeping them distinct matters:\n *\n * 1. {@link RebaseProjectManifest} — `rebase.json`. **Authored** by the developer,\n * committed to the repository. Declares topology only: which runtime major the\n * project targets, and which apps *this repository* contributes to the project.\n * Schema, security rules, hooks and functions stay in TypeScript under the\n * config package — nothing that needs a type system belongs here.\n *\n * 2. {@link RebaseProjectLink} — the per-checkout link (`.rebase/cloud.json`).\n * **Not committed**, because it is per-developer like a git remote. Says which\n * deployed project this working copy points at, whether that is a Rebase Cloud\n * project or the base URL of a self-hosted backend.\n *\n * 3. {@link RebaseBundleManifest} — `manifest.json` inside a built bundle.\n * **Generated**, never hand-edited. It is the lockfile analogue: the exact\n * contract a built artifact claims to satisfy, which the runtime validates\n * before it boots and a control plane validates before it deploys.\n *\n * A repository declares only the apps it contains. The set of apps belonging to a\n * project is held by the project itself, which is what makes multi-repo projects\n * work: two repositories never need to know about each other, only about the\n * project.\n */\n\nimport type { StorageSourceDefinition } from \"./storage_source\";\nimport type { ResourceGraph } from \"./resources\";\n\n/**\n * Which kind of thing an app is.\n *\n * - `backend` — the collections/hooks/functions that define the project's API.\n * Exactly one per *project* (not per repository); the registry enforces it.\n * - `static` — a pre-built client bundle (SPA, static site), served from the\n * backend process at its declared `path` or from a CDN. The admin panel is\n * one of these: it is an app in the user's repository like any other.\n *\n * That is the whole list. Ownership of the server process is a property of the\n * backend app ({@link RebaseBackendAppConfig.runtime}), not an app type.\n */\nexport type RebaseAppType = \"backend\" | \"static\";\n\n/**\n * The backend app: the project's API surface.\n *\n * Paths are relative to the directory holding `rebase.json`. The defaults match\n * the layout `rebase init` scaffolds, so a stock project may declare simply\n * `{ \"type\": \"backend\", \"runtime\": \"managed\" }`.\n */\nexport interface RebaseBackendAppConfig {\n type: \"backend\";\n /**\n * Who owns the process this backend runs in.\n *\n * - `managed` — the platform's runtime image boots this project's bundle.\n * You supply collections, functions, crons and schema; Rebase supplies the\n * server.\n * - `custom` — this repository builds its own image and entrypoint. The\n * escape hatch: full control, no managed-runtime guarantees.\n *\n * Independent of *where* it runs. Both run on Rebase Cloud and both\n * self-host — the destination lives in `.rebase/cloud.json`, not here. See\n * `infra/docker/docker-compose.selfhost.yml`, which boots a managed bundle on a\n * developer's own Docker host.\n *\n * This is authored rather than inferred on purpose. It is the single most\n * consequential fact about a deployment, and inferring it is what used to\n * land projects on the custom runtime without anyone choosing it.\n */\n runtime: \"managed\" | \"custom\";\n /** Directory of the config package (collections + index). Default `config`. */\n config?: string;\n /** Directory of server functions. Default `backend/functions`. */\n functions?: string;\n /** Directory of cron job definitions. Default `backend/crons` when present. */\n crons?: string;\n /**\n * Path to the generated Drizzle schema module (tables/enums/relations).\n * Default `backend/src/schema.generated.ts`.\n */\n schema?: string;\n /**\n * Module path (relative to `config`) exporting the auth users collection as\n * its default export. Default `collections/users`.\n */\n usersCollection?: string;\n\n /**\n * `runtime: \"custom\"` only. Dockerfile path relative to the repository root.\n * Default `Dockerfile`.\n */\n dockerfile?: string;\n /** `runtime: \"custom\"` only. Build context relative to the root. Default `.`. */\n context?: string;\n /** `runtime: \"custom\"` only. Port the container listens on. Default 8080. */\n port?: number;\n}\n\n/**\n * A static client bundle — SPA or static site — built here and served at `path`.\n */\nexport interface RebaseStaticAppConfig {\n type: \"static\";\n /** Package directory containing the client sources. */\n root: string;\n /** Command that produces `output`. Run from the repository root. */\n build?: string;\n /** Directory of built assets, relative to the repository root. */\n output: string;\n /**\n * Public base path this app is served under. Default `/`.\n *\n * Several static apps run in one process, each at its own path — the API at\n * `/api`, a site at `/`, the admin at `/admin` — which is what keeps a\n * self-hosted deployment a single container.\n *\n * **This is a build-time input, not only a serving concern.** An app mounted\n * at `/admin` must be *built* for `/admin` (Vite's `base`), or `index.html`\n * loads and every asset 404s: a blank page with no server error. `rebase\n * build` passes it as `REBASE_APP_BASE` and asserts the emitted HTML honours\n * it. Changing this value requires rebuilding the app.\n */\n path?: string;\n /**\n * Serve `index.html` for unmatched paths under `path` (client-side routing).\n * Default `true` — the overwhelmingly common case for a client app, and a\n * static *site* generator emits real files for its routes anyway.\n */\n spa?: boolean;\n}\n\nexport type RebaseAppConfig = RebaseBackendAppConfig | RebaseStaticAppConfig;\n\n/**\n * Path prefixes the backend owns, which no static app may claim.\n *\n * One process — and, on the platform, one hostname — serves both the API and\n * however many static apps a project has. Mounting is longest-path-first, so an\n * app declaring `/api` would win against the API itself and every request to it\n * would be answered with that app's `index.html`: a 200 carrying HTML where the\n * caller expected JSON, from a project that looks deployed and healthy.\n *\n * Declared here rather than in either enforcer because both must agree. The CLI\n * checks it so a developer finds out while editing `rebase.json`; the control\n * plane checks it again at deploy intake, because the front door's correctness\n * cannot rest on a check that ran in somebody else's CLI — and a repository can\n * be deployed by a CLI older than this rule.\n */\nexport const RESERVED_BACKEND_PREFIXES = [\"/api\", \"/health\", \"/healthz\", \"/livez\", \"/readyz\", \"/metrics\"] as const;\n\n/**\n * Whether `path` collides with a prefix the backend owns.\n *\n * Compares at segment boundaries, so `/api` and `/api/v2` collide while\n * `/apidocs` does not — the same rule the router matches with, because a check\n * that is stricter than the router rejects paths that would have worked, and one\n * that is looser admits paths that will not.\n */\nexport function reservedPrefixFor(path: string): string | undefined {\n const normalized = path.endsWith(\"/\") && path !== \"/\" ? path.slice(0, -1) : path;\n return RESERVED_BACKEND_PREFIXES.find(\n reserved => normalized === reserved || normalized.startsWith(`${reserved}/`)\n );\n}\n\n/**\n * One declared storage source, as authored in `rebase.json`.\n *\n * The key comes from the enclosing record, so this is\n * {@link StorageSourceDefinition} minus its `key` — the same document the\n * runtime registry and the frontend router consume, expressed the way a JSON\n * object naturally expresses \"a set of named things\".\n */\nexport interface RebaseStorageSourceConfig {\n /** Engine backing this source: `local`, `s3`, `gcs`, or a custom id. */\n engine: string;\n /**\n * How the frontend reaches it. Default `server` (proxied through\n * `/api/storage`). `direct` means a provider SDK talks to the bucket and the\n * backend is not in the upload path.\n */\n transport?: \"server\" | \"direct\";\n /** Human-readable label for the console and the admin UI. */\n label?: string;\n}\n\n/**\n * `rebase.json` — the authored project manifest.\n */\nexport interface RebaseProjectManifest {\n /** JSON Schema URL, for editor completion. Ignored by the tooling. */\n $schema?: string;\n /**\n * The runtime contract **major** this project targets, as a semver range\n * (e.g. `^1`, `~1.4`, or an exact `1.4.2` to pin).\n *\n * The platform upgrades patches and minors underneath a project without\n * asking; it never crosses a major. See {@link RUNTIME_CONTRACT_VERSION}.\n *\n * Named `rebase` rather than `runtime` so that `runtime` means exactly one\n * thing — {@link RebaseBackendAppConfig.runtime}, who owns the process. It\n * reads like `engines` in a `package.json`, which is what it is.\n */\n rebase: string;\n /**\n * Apps this repository contributes, keyed by app name. The key is the app's\n * identity within the project: it is what `rebase deploy <app>` names, what\n * client credentials are issued against, and what a second repository must\n * not collide with.\n */\n apps: Record<string, RebaseAppConfig>;\n /**\n * Buckets are NOT declared here any more.\n *\n * They were, and the runtime merged this block with the declarations in\n * config code — a bucket named in both had one engine kept and the other\n * silently discarded. Two homes for one concept, with a merge to decide\n * between them, is the shape this whole model replaced.\n *\n * `bucket(\"media\", { engine: \"s3\" })` in the project's config declares one\n * now, and `rebase resources --write` generates `rebase.resources.json`,\n * which is what a host reads before a build. A `storage` block left in this\n * file is refused by the validator, by name, with the replacement in the\n * message — not ignored, because a key that still parses and does nothing\n * is the failure this removed.\n */\n /**\n * Repository-wide opt-out from anonymous CLI usage sharing.\n *\n * **Only `false` does anything.** It suppresses sharing for everyone who\n * clones this repository, overriding each developer's own opt-in — an\n * organisation setting policy for work done on its behalf, the same shape\n * as a committed `.npmrc`.\n *\n * `true` is deliberately ignored, and the CLI says so rather than obeying\n * quietly. This file is committed, so a `true` here would be one developer\n * answering a privacy question for every colleague who later clones the\n * repo — consent by proxy, which is the exact thing opt-in exists to\n * prevent. Individuals opt in with `rebase telemetry enable`.\n */\n telemetry?: boolean;\n}\n\n/**\n * The per-checkout project link.\n *\n * Deliberately separate from `rebase.json`: the manifest is committed and shared,\n * while the link is per-developer. Keeping them in one file would mean either\n * committing someone's project id or gitignoring the topology.\n */\nexport interface RebaseProjectLink {\n /**\n * A Rebase Cloud project id, or the base URL of any running Rebase backend\n * (`https://api.example.com`). Both are first-class: every command that\n * accepts a project reference accepts either, so a self-hosted project has\n * the same tooling as a cloud one.\n */\n project: string;\n /** Organization slug. Cloud projects only. */\n org?: string;\n /** Explicit API base URL, when it differs from the project's default. */\n apiUrl?: string;\n}\n\n/**\n * Whether a project can run on the managed runtime, and if not, precisely why.\n *\n * The reasons are returned rather than summarised so tooling can print something\n * a developer can act on. \"Not eligible\" is never a dead end — it selects the\n * custom-runtime path, which still deploys.\n */\nexport interface ManagedCompatibility {\n eligible: boolean;\n reasons: string[];\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Bundle\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Version of the bundle *format* itself.\n *\n * Bumped only when the on-disk layout changes in a way an older runtime could\n * not read. A runtime accepts any bundle whose `bundleFormat` is less than or\n * equal to its own — old bundles keep booting on new runtimes, which is the\n * whole point of separating the artifact from the engine.\n *\n * - **1** — `mode: \"cms\" | \"baas\" | \"static\"`, `entry.static` a single directory\n * string, `entry.admin` for a bundled admin panel.\n * - **2** — `kind: \"backend\" | \"static\"`, `entry.static` a list of\n * {@link RebaseBundleStatic}, `entry.admin` removed. A format-1 runtime reading\n * one of these would find no `mode` and an array where it expects a string, so\n * the bump is what turns that into a refusal to boot instead of a bundle that\n * starts and serves nothing.\n */\nexport const BUNDLE_FORMAT_VERSION = 2;\n\n/**\n * The runtime contract major.\n *\n * Distinct from the `@rebasepro/server` package version: the package may release\n * any number of minors and patches while this stays put. It changes only when\n * the bundle/runtime contract breaks compatibility, and a project's\n * `manifest.runtime` range is matched against *this*.\n *\n * ## v2 — resources are declared, not configured\n *\n * `RebaseBackendConfig.dataSources` and `.storageSources` are gone. A project\n * declares its databases and buckets with `database()` / `bucket()` in its\n * config, and the runtime reads those declarations.\n *\n * This had to be a major, and the reason is the managed tier: it moves projects\n * onto new images WITHOUT rebuilding them. A bundle built against v1 exports\n * those keys, and a v2 runtime refuses them at boot — so without this bump, one\n * image rollout would crash-loop every tenant that had ever declared a second\n * database or bucket, in a wave, with the cause in a container log nobody is\n * watching.\n *\n * With the bump, a v1 bundle on a v2 runtime is refused by\n * `assertBundleCompatibility` with the remedy in the message, and the platform\n * keeps it on a v1 image until it is rebuilt. That is the whole purpose of this\n * number.\n *\n * **Release order matters and is not optional.** The control plane is the side\n * that rejects, so it ships FIRST: raise `SUPPORTED_RUNTIME_CONTRACT` in the\n * saas repo (it rejects only `contract >` its own, so it then accepts both),\n * deploy that, and only then release a runtime implementing v2. Shipping the\n * runtime first turns every deploy into a rejected intake blaming the tenant's\n * bundle.\n */\nexport const RUNTIME_CONTRACT_VERSION = 1;\n\n/** Where the runtime finds each part of the bundle. Paths are bundle-relative. */\nexport interface RebaseBundleEntrypoints {\n /** Compiled config package directory (collections live under it). */\n config?: string;\n /** Compiled collections directory, when it differs from `<config>/collections`. */\n collections?: string;\n /** Compiled functions directory. */\n functions?: string;\n /** Compiled crons directory. */\n crons?: string;\n /** Compiled Drizzle schema module. */\n schema?: string;\n /** Module exporting the auth users collection (default export). */\n usersCollection?: string;\n /**\n * Built static apps to serve from this process, in declaration order.\n *\n * A list rather than a single directory because one process serves several\n * apps at different paths — a site at `/` and the admin at `/admin`. The\n * runtime mounts them longest-path-first so the `/`-rooted app's catch-all\n * does not claim its siblings' URLs.\n */\n static?: RebaseBundleStatic[];\n}\n\n/** One built static app inside a bundle. */\nexport interface RebaseBundleStatic {\n /** Public base path, e.g. `/` or `/admin`. */\n path: string;\n /** Bundle-relative directory holding the built assets. */\n dir: string;\n /** Serve `index.html` for unmatched paths under `path`. */\n spa: boolean;\n}\n\n/**\n * A native module found in the dependency closure.\n *\n * Recorded rather than merely counted so a rejection can name the offending\n * package instead of saying \"something here is native\".\n */\nexport interface NativeDependency {\n name: string;\n /** Why it was flagged — a `.node` binary, a gyp build, or an install script. */\n reason: string;\n}\n\n/**\n * `manifest.json` — generated, and the document the runtime and control plane\n * both validate against.\n */\n/**\n * One custom function, as recorded in a built bundle.\n *\n * @see RebaseBundleManifest.functions\n */\nexport interface RebaseBundleFunction {\n /**\n * The filename without its extension — which is also the URL segment it\n * mounts at (`/api/functions/<name>`), the API-key permission that grants\n * it, and the name `REBASE_FUNCTIONS_ONLY` selects by. One identity, used\n * everywhere.\n */\n name: string;\n /** Path inside the bundle, so a host can point at the file. */\n file: string;\n /**\n * `false` when the function's own source imports a Node built-in or a\n * package that needs one.\n *\n * Descriptive, never a gate: nothing refuses to build or deploy on this. It\n * says where this function *could* run, not where it should.\n */\n portable: boolean;\n /**\n * Why it is not portable — one short phrase per reason, deduplicated.\n * Absent when it is.\n */\n requires?: string[];\n}\n\nexport interface RebaseBundleManifest {\n /** @see BUNDLE_FORMAT_VERSION */\n bundleFormat: number;\n runtime: {\n /** The `runtime` range copied from `rebase.json`. */\n range: string;\n /** Exact `@rebasepro/server` version this bundle was built against. */\n builtAgainst: string;\n /** Runtime contract major this bundle requires. */\n contract: number;\n };\n /**\n * Hash of the compiled collection definitions.\n *\n * This is the contract stamp. A generated SDK records the value it was built\n * from, a client sends it back, and a mismatch is what lets the platform say\n * \"this app was built against an older schema\" instead of failing mysteriously\n * at the first request. It covers collections only — a hook edit does not\n * change a client's contract, so it must not invalidate every SDK.\n */\n schemaVersion: string;\n /** Which app in `rebase.json` this bundle was built from. */\n app: string;\n /**\n * What the runtime does with this bundle.\n *\n * - `backend` — boot the full server: database, auth and the data API, plus\n * any static apps in `entry.static`.\n * - `static` — no backend at all: serve `entry.static` and nothing else. No\n * database, no auth, no data sources. This is how a static app runs on the\n * same image as the backend.\n *\n * Replaces an earlier `mode: \"cms\" | \"baas\" | \"static\"`. The cms/baas\n * distinction was never a third kind of thing — it is simply whether\n * `entry.config` is present, so it is derived rather than declared.\n */\n kind: \"backend\" | \"static\";\n entry: RebaseBundleEntrypoints;\n /** Collection slugs contained in the bundle, for quick inspection. */\n collections?: string[];\n /**\n * Every custom function in the bundle, named and classified.\n *\n * Two things are recorded per function, and both are answers a host would\n * otherwise have to get by importing user code:\n *\n * - **What it is called.** That name is the function's identity everywhere —\n * the URL segment it mounts at, the `functions/<name>` API-key\n * permission, the value `REBASE_FUNCTIONS_ONLY` selects by. A host that\n * wants to give one slow function its own replica count currently has to\n * boot the bundle to discover what is in it.\n * - **Whether it needs Node.** Purely descriptive: a function that opens a\n * file or runs raw SQL is a fine function, and every deployment today is\n * a Node process. It is recorded because the question \"which of these\n * could run somewhere else\" has to be answerable from the artifact, and\n * because answering it per-file after the fact — across a codebase\n * already written — is the expensive version of the same question.\n *\n * Absent on a bundle built before this field existed, which is why every\n * consumer must treat it as optional rather than as an empty list.\n */\n functions?: RebaseBundleFunction[];\n hooks: {\n /**\n * Whether the dependency closure contains native code.\n *\n * The managed runtime refuses these: a prebuilt binary cannot be run on\n * an image the platform did not build it for, and the honest failure is\n * at deploy time rather than at 3am in a crash loop.\n */\n native: boolean;\n nativeModules?: NativeDependency[];\n };\n /**\n * What the bundle's config says about storage access control.\n *\n * Storage is not under RLS and its keys share one flat namespace, so a\n * deployment with file storage enabled and no access model serves every\n * user's files to every signed-in user. The runtime refuses to boot in that\n * state — which, on a hosted platform that enables storage from the *console*\n * rather than from the bundle, surfaces as a crash loop the developer cannot\n * read.\n *\n * Recording it here lets a host reject the deploy with the reason instead.\n * Absent on bundles built before this field existed.\n */\n storage?: {\n /** Whether the config package exports a `storageAuthorize` hook. */\n authorize: boolean;\n /**\n * Buckets, on bundles built before {@link RebaseBundleManifest.resources}.\n *\n * No longer written. A host reads `resources`, which carries every kind\n * in one list; this stays declared so a control plane can keep reading\n * the bundles a project shipped before it was rebuilt.\n */\n sources?: StorageSourceDefinition[];\n };\n /**\n * Everything the project declares it needs — databases, buckets, topics,\n * and whatever kind is registered next.\n *\n * Recorded so a host can tell, from the artifact alone and before starting\n * anything, what a deploy will need provisioned. That question used to be\n * answerable for buckets and for nothing else, because buckets were the\n * only kind written into an artifact — which is how a project's databases\n * became invisible to the platform that runs them.\n *\n * Absent on bundles built before this field existed.\n */\n resources?: ResourceGraph;\n deps: {\n /** Runtime dependencies of user code, as declared. */\n declared: Record<string, string>;\n /**\n * The dependency tree ships *inside* the bundle, already installed.\n *\n * Absent or false means the tree is declared but not present, and\n * whoever boots the bundle has to install it. On the managed runtime that\n * install runs in an init container on **every** pod start — the bundle\n * lives on a volume that is wiped each time — and it is the single\n * largest cost in a managed pod's life: 35–55 seconds of a 40–60 second\n * cold start. Since a pod restarts on every eviction, node failure, OOM\n * and runtime rollout, that number is not a startup detail. It is what an\n * outage costs.\n *\n * Vendoring moves the install to build time, where it happens once. It is\n * skipped when the closure contains native code, because a prebuilt\n * binary is only valid for the platform it was built for — see\n * {@link vendorTarget} for what \"the platform\" means here.\n */\n vendored?: boolean;\n /**\n * What {@link vendored} was resolved for, recorded so a mismatch can be\n * refused rather than discovered at import time.\n *\n * Cross-platform vendoring is safe for pure JavaScript and unsafe for\n * anything compiled, and the boundary between them is not always visible\n * in a dependency list: `esbuild` is pure-JS with a *platform-specific\n * optional dependency* holding the actual binary, so an install run on a\n * developer's Mac silently produces a tree that cannot run on the Linux\n * image. The install therefore resolves optional dependencies for the\n * target explicitly rather than for the machine it runs on, and records\n * the answer here.\n */\n vendorTarget?: {\n /** npm `--os`, e.g. `linux`. */\n os: string;\n /** npm `--cpu`, e.g. `x64`. */\n cpu: string;\n /** Node major the tree was resolved for. */\n node: string;\n };\n };\n build: {\n /** `@rebasepro/cli` version that produced this bundle. */\n cli: string;\n /** Node major the bundle was compiled on. */\n node: string;\n /** ISO-8601. */\n createdAt: string;\n };\n}\n\n/** The contract a running backend serves at `GET /api/meta/contract`. */\nexport interface RebaseProjectContract {\n /** Matches {@link RebaseBundleManifest.schemaVersion}. */\n schemaVersion: string;\n runtime: {\n /** `@rebasepro/server` version currently running. */\n version: string;\n contract: number;\n };\n /** Full collection definitions, serialized — the input to SDK generation. */\n collections: unknown[];\n /** Collection slugs, for cheap inspection without parsing the definitions. */\n collectionSlugs: string[];\n generatedAt: string;\n}\n\n/** Header carrying the schema version an SDK was generated from. */\nexport const SCHEMA_VERSION_HEADER = \"x-rebase-schema\";\n","import type { CollectionConfig } from \"./collections\";\n\n/**\n * Serializing collections so they survive a network hop.\n *\n * A collection definition is not plain data. Relations point at their target\n * with a *function* (`target: () => usersCollection`) so two collections can\n * reference each other without an import cycle, and collections also carry\n * callbacks, custom views and component references. `JSON.stringify` silently\n * drops every one of those, which matters because the SDK generator *calls*\n * `relation.target()` to decide whether a foreign key is a string or a number.\n * Serialize naively and remote SDK generation produces subtly wrong types\n * instead of failing — the worst possible outcome.\n *\n * So relation targets are resolved to a slug reference on the way out and\n * rebuilt into functions on the way in. Everything else that cannot cross a wire\n * is dropped deliberately: an SDK is generated from the *shape* of the data, and\n * server-side behaviour is neither useful to a client nor safe to publish.\n */\n\n/** Marker replacing a relation's `target` function in serialized form. */\nexport interface SerializedCollectionRef {\n __collectionRef: string;\n}\n\nexport function isSerializedCollectionRef(value: unknown): value is SerializedCollectionRef {\n return typeof value === \"object\"\n && value !== null\n && typeof (value as SerializedCollectionRef).__collectionRef === \"string\";\n}\n\n/** Depth limit for the walk — deep enough for real configs, finite for cyclic ones. */\nconst MAX_DEPTH = 64;\n\n/**\n * Resolve whatever a `target` thunk returns down to a collection.\n *\n * A target may be the collection, a module namespace (when the authoring file\n * used `import * as`), or a default-export wrapper. All three appear in real\n * projects, and the SDK generator already unwraps them the same way.\n */\nfunction unwrapTarget(value: unknown): CollectionConfig | undefined {\n if (!value || typeof value !== \"object\") return undefined;\n const candidate = value as { default?: unknown; __esModule?: boolean; properties?: unknown };\n if (candidate.default || candidate.__esModule) {\n const inner = candidate.default;\n if (inner && typeof inner === \"object\") return inner as CollectionConfig;\n }\n if (candidate.properties) return value as CollectionConfig;\n return undefined;\n}\n\n/** The identity a serialized reference uses. Slug first — it is the routing key. */\nfunction refFor(collection: CollectionConfig | undefined): string | undefined {\n if (!collection) return undefined;\n const withPath = collection as CollectionConfig & { path?: string };\n return collection.slug || withPath.path || collection.name;\n}\n\n/**\n * Deep-copy a value into something JSON can carry.\n *\n * `target` keys are special-cased into refs. Other functions vanish, cycles are\n * cut, and everything else is copied structurally.\n */\n/** Shared walk state: the memo, plus a count of depth-cap hits. */\ninterface WalkState {\n memo: WeakMap<object, unknown>;\n /**\n * How many times the walk has truncated a subtree — by hitting the depth\n * cap, or by cutting a cycle.\n *\n * Either kind of truncation makes a result valid only at the *position* it\n * was produced at, so caching it and serving it elsewhere silently drops\n * content that would have been included. Comparing this counter before and\n * after a node's children tells us whether its result is position-\n * independent and therefore safe to memoize.\n *\n * The cycle case is the subtle one: with `a.b = b` and `b.a = a`, serializing\n * `{ first: b, second: a }` visits `a` beneath `b` — where the cycle back to\n * `b` is cut — and would then reuse that truncated `a` for `second`, where\n * nothing needed cutting.\n */\n truncations: number;\n}\n\nfunction toSerializable(\n value: unknown,\n seen: WeakSet<object>,\n depth: number,\n state: WalkState,\n key?: string\n): unknown {\n if (depth > MAX_DEPTH) {\n state.truncations++;\n return undefined;\n }\n\n if (typeof value === \"function\") {\n // Only a relation target carries information a client needs. Calling it\n // is safe here — this runs on the server, where the target module is\n // already loaded — and a throwing target simply yields no reference,\n // which degrades the generated FK type rather than failing the request.\n if (key === \"target\") {\n try {\n const resolved = unwrapTarget((value as () => unknown)());\n const ref = refFor(resolved);\n return ref ? { __collectionRef: ref } : undefined;\n } catch {\n return undefined;\n }\n }\n return undefined;\n }\n\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n if (value instanceof Date) return value.toISOString();\n if (value instanceof RegExp) return value.source;\n\n if (seen.has(value as object)) {\n state.truncations++;\n return undefined;\n }\n\n // A shared (non-cyclic) subgraph is reachable by many paths, and `seen` is a\n // *path* set — released in the `finally` below so a node referenced twice in\n // different branches is emitted twice rather than dropped as a false cycle.\n // Without memoization that makes the walk exponential in depth: a diamond\n // graph 20 levels deep took ~400ms, and each further level doubled it. The\n // result is a plain data tree, so handing back the same converted object for\n // a repeat visit is indistinguishable after JSON.stringify.\n const cached = state.memo.get(value as object);\n if (cached !== undefined) return cached;\n\n seen.add(value as object);\n const truncationsBefore = state.truncations;\n const memoize = (result: unknown): unknown => {\n // Only cache a result that nothing was cut from.\n if (result !== undefined && state.truncations === truncationsBefore) {\n state.memo.set(value as object, result);\n }\n return result;\n };\n\n try {\n if (Array.isArray(value)) {\n const items = value\n .map(item => toSerializable(item, seen, depth + 1, state))\n .filter(item => item !== undefined);\n // A container that had content, none of which can be represented, is\n // itself unrepresentable — see the note below.\n return memoize(value.length > 0 && items.length === 0 ? undefined : items);\n }\n\n // A React element or component reference has no meaning to a client and\n // will not survive JSON anyway.\n if (\"$$typeof\" in (value as Record<string, unknown>)) return undefined;\n\n const entries = Object.entries(value as Record<string, unknown>);\n const out: Record<string, unknown> = {};\n for (const [k, v] of entries) {\n const converted = toSerializable(v, seen, depth + 1, state, k);\n if (converted !== undefined) out[k] = converted;\n }\n\n // Drop a container whose entire content was dropped.\n //\n // `callbacks: { beforeSave() {…} }` would otherwise serialize to\n // `callbacks: {}` — an empty husk that carries no information but is not\n // *nothing*, so it lands in the payload and, worse, in the schema hash.\n // Editing a hook would then change every client's schema version and\n // report perfectly current SDKs as stale.\n //\n // A container that started empty stays empty: `properties: {}` is a\n // deliberate statement, not a casualty.\n if (entries.length > 0 && Object.keys(out).length === 0) return undefined;\n\n return memoize(out);\n } finally {\n // Released so a collection referenced twice in different branches is\n // emitted twice rather than being dropped as a false cycle.\n seen.delete(value as object);\n }\n}\n\n/**\n * Serialize collections for transport over the contract endpoint.\n *\n * Sorted by slug so the output — and therefore the schema hash computed from it\n * — does not depend on filesystem ordering.\n */\nexport function serializeCollections(collections: CollectionConfig[]): unknown[] {\n return [...collections]\n .sort((a, b) => String(a.slug ?? \"\").localeCompare(String(b.slug ?? \"\")))\n .map(collection => toSerializable(withoutAdminBlock(collection), new WeakSet(), 0, {\n memo: new WeakMap(),\n truncations: 0\n }))\n .filter((c): c is Record<string, unknown> => c !== undefined);\n}\n\n/**\n * Drop the admin block before the walk.\n *\n * Nothing downstream of serialization is an admin panel. The contract endpoint\n * feeds remote SDK generation, and `rebase build` writes the result into a bundle\n * manifest that only the backend runtime reads. The block would survive the walk\n * as a husk anyway — its React elements and component functions are dropped\n * individually — and that husk has two costs worth avoiding: it puts every custom\n * component's *file path* on an endpoint whose job is to describe data shapes, and\n * it grows a payload that is fetched and cached per project.\n *\n * Removing it here rather than at each call site means one chokepoint, so a future\n * consumer of `serializeCollections` cannot forget.\n *\n * Child collections carry their own block, so this recurses — stripping only the\n * top level was the mistake `stripNonClientFields` in the contract routes already\n * had to fix once for security rules.\n */\nfunction withoutAdminBlock(collection: CollectionConfig): CollectionConfig {\n const { admin: _admin, ...rest } = collection as CollectionConfig & Record<string, unknown>;\n const nested = rest as Record<string, unknown>;\n if (Array.isArray(nested.subcollections)) {\n nested.subcollections = nested.subcollections.map(\n (child) => withoutAdminBlock(child as CollectionConfig)\n );\n }\n return rest as CollectionConfig;\n}\n\n/**\n * Rebuild collections received from a contract endpoint.\n *\n * Relation refs become real thunks resolving through the returned set, so\n * downstream consumers — the SDK generator above all — see exactly the shape\n * they would have seen had the collections been imported from source.\n *\n * A ref naming a collection that is not in the payload resolves to `undefined`\n * rather than throwing: the generator already tolerates an unresolvable target\n * by falling back to a permissive key type, and a partial contract should still\n * produce a usable SDK.\n */\nexport function deserializeCollections(payload: unknown[]): CollectionConfig[] {\n const collections = payload\n .filter((c): c is Record<string, unknown> => typeof c === \"object\" && c !== null)\n .map(c => ({ ...c })) as unknown as CollectionConfig[];\n\n const bySlug = new Map<string, CollectionConfig>();\n for (const collection of collections) {\n const ref = refFor(collection);\n if (ref) bySlug.set(ref, collection);\n }\n\n const rehydrate = (value: unknown, depth: number): void => {\n if (depth > MAX_DEPTH || !value || typeof value !== \"object\") return;\n\n if (Array.isArray(value)) {\n for (const item of value) rehydrate(item, depth + 1);\n return;\n }\n\n const record = value as Record<string, unknown>;\n for (const [key, child] of Object.entries(record)) {\n if (key === \"target\" && isSerializedCollectionRef(child)) {\n const slug = child.__collectionRef;\n record.target = () => bySlug.get(slug);\n continue;\n }\n rehydrate(child, depth + 1);\n }\n };\n\n for (const collection of collections) rehydrate(collection, 0);\n return collections;\n}\n","import type { CollectionConfig } from \"./collections\";\nimport { serializeCollections } from \"./collection_contract\";\n\n/**\n * The schema version stamp.\n *\n * One function, used in three places that must agree or the whole drift-detection\n * story is noise: `rebase build` writes it into a bundle manifest, the runtime\n * serves it from the contract endpoint, and a generated SDK records the value it\n * was built from. If any two of those computed it differently, every client would\n * look permanently out of date.\n *\n * It covers **collections only** — the client's contract is the shape of the\n * data, so editing a hook or a server function must not invalidate every SDK in\n * every repository. That is a deliberate narrowing, not an oversight.\n */\n\n/** Stable stringify: object keys sorted at every level, so key order cannot alter the hash. */\nfunction canonicalize(value: unknown): string {\n if (value === null || typeof value !== \"object\") {\n return JSON.stringify(value) ?? \"null\";\n }\n if (Array.isArray(value)) {\n return `[${value.map(canonicalize).join(\",\")}]`;\n }\n const entries = Object.entries(value as Record<string, unknown>)\n .filter(([, v]) => v !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalize(v)}`).join(\",\")}}`;\n}\n\n/**\n * Reduce a collection to the parts a generated client is actually built from.\n *\n * The version answers one question — \"is this SDK stale?\" — so it must change\n * exactly when the generated types could change, and never otherwise. Hashing a\n * whole collection fails both halves of that:\n *\n * - Security rules, callbacks, icons, groups and UI settings do not appear in a\n * generated client, so including them reports perfectly current SDKs as stale.\n * - Worse, they are not stable *inputs*. The runtime applies default security\n * rules when it loads collections, so the same source hashed before and after\n * loading produced two different answers — a build-time stamp that could never\n * match the server that served it.\n *\n * Codegen reads the slug (for the `Database` key and type names), the properties,\n * and the relations. That is the projection.\n */\nfunction projectForCodegen(collection: CollectionConfig): Record<string, unknown> {\n const source = collection as CollectionConfig & {\n relations?: unknown;\n subcollections?: CollectionConfig[];\n path?: string;\n engine?: unknown;\n dataSource?: unknown;\n };\n\n return {\n slug: collection.slug ?? source.path,\n properties: collection.properties,\n relations: source.relations,\n // The engine decides whether relations are resolved at all: codegen asks\n // `getDataSourceCapabilities(collection.engine).supportsRelations`, and an\n // engine that answers no drops every foreign-key column from the\n // generated Row/Insert/Update types. Moving a collection to such an\n // engine is a real change to the generated types, so it has to move the\n // version. `dataSource` is what resolves to `engine`, so it counts too.\n engine: source.engine,\n dataSource: source.dataSource,\n subcollections: source.subcollections?.map(projectForCodegen)\n };\n}\n\n/**\n * Compute the canonical string a schema version hashes.\n *\n * Exposed separately so the hashing itself can differ by environment: Node has\n * `crypto`, and callers without it can still compare canonical forms directly.\n */\nexport function canonicalSchemaPayload(collections: CollectionConfig[]): string {\n const projected = serializeCollections(collections)\n .map(collection => projectForCodegen(collection as CollectionConfig));\n return canonicalize(projected);\n}\n\n/**\n * A short, non-cryptographic digest of the canonical payload.\n *\n * FNV-1a style, 64 bits, as two 32-bit halves. This is an identity, not a\n * security boundary: nothing trusts a schema version to prove anything, it only\n * answers \"is this the same schema as before\". A hand-rolled hash keeps this\n * module free of `node:crypto`, so the identical function runs in the browser,\n * in the CLI, and in the runtime — which is the property that actually matters.\n */\nexport function computeSchemaVersion(collections: CollectionConfig[]): string {\n const payload = canonicalSchemaPayload(collections);\n\n let h1 = 0x811c9dc5;\n let h2 = 0x01000193;\n\n for (let i = 0; i < payload.length; i++) {\n const code = payload.charCodeAt(i);\n h1 ^= code;\n // Multiply by the FNV prime using shifts to stay in 32-bit integer math.\n h1 = (h1 + ((h1 << 1) + (h1 << 4) + (h1 << 7) + (h1 << 8) + (h1 << 24))) >>> 0;\n h2 ^= code + i;\n h2 = (h2 + ((h2 << 1) + (h2 << 5) + (h2 << 9) + (h2 << 15) + (h2 << 24))) >>> 0;\n }\n\n const hex = (n: number): string => n.toString(16).padStart(8, \"0\");\n return `v1:${hex(h1)}${hex(h2)}`;\n}\n","import { Hono } from \"hono\";\nimport {\n RUNTIME_CONTRACT_VERSION,\n SCHEMA_VERSION_HEADER,\n computeSchemaVersion,\n serializeCollections,\n type CollectionConfig,\n type RebaseProjectContract\n} from \"@rebasepro/types\";\nimport type { HonoEnv } from \"./types\";\nimport { logger } from \"../utils/logger\";\n\n/**\n * The project contract endpoint.\n *\n * This is what makes a repository able to build against a project it does not\n * contain. Without it, a typed client can only be generated from local\n * collection *source*, which means every frontend must live in the same\n * repository as the backend. Serving the contract turns that around: an app\n * asks the project what its shape is, so a web app, a second web app and a\n * mobile app can each live wherever they like and none of them needs to know\n * about the others.\n *\n * Admin-gated. Collection definitions describe every table, column and relation\n * in the project, including ones no security rule would ever expose — that is a\n * map of the database, not public API documentation.\n */\n\nexport interface ContractRoutesConfig {\n collectionRegistry: { getRawCollections(): CollectionConfig[] };\n /**\n * The schema version recorded at build time.\n *\n * Preferred over recomputing, so that what a client is told matches exactly\n * what the bundle claims. It is recomputed only when a bundle did not record\n * one — a `baas`-mode project derives its collections from the live database\n * at boot, so there was nothing to hash when it was built.\n */\n schemaVersion?: string;\n /** Runtime package version, surfaced so a client can report what it built against. */\n runtimeVersion?: string;\n}\n\n/**\n * Strip everything a client does not need from a serialized collection.\n *\n * The generator reads the slug, the properties and the relations. It never reads\n * a security rule — but `securityRules` carries the raw SQL of every RLS\n * predicate guarding the project, which is a description of the authorization\n * model rather than of the data shape. Publishing it to anyone who can generate\n * an SDK gives away more than the endpoint is for, so it is removed here rather\n * than trusted not to matter.\n */\nfunction stripNonClientFields(collection: unknown): unknown {\n if (!collection || typeof collection !== \"object\") return collection;\n const {\n securityRules: _securityRules,\n callbacks: _callbacks,\n ...rest\n } = collection as Record<string, unknown>;\n\n // Subcollections are collections, and carry their own rules. Stripping only\n // the top level published every nested policy — the leak this exists to\n // prevent, just one level down.\n if (Array.isArray(rest.subcollections)) {\n rest.subcollections = rest.subcollections.map(stripNonClientFields);\n }\n\n return rest;\n}\n\nexport function createContractRoutes(config: ContractRoutesConfig): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n\n // Computing a version walks and canonicalizes every collection, and\n // `/schema-version` is deliberately unauthenticated and meant to be polled.\n // Recomputing per request would make a CI convenience into a CPU\n // amplification anyone could aim at the server. Collections do not change\n // after boot, so once is enough.\n let cachedVersion: string | undefined;\n const schemaVersionOf = (collections: CollectionConfig[]): string => {\n if (config.schemaVersion) return config.schemaVersion;\n if (cachedVersion === undefined) cachedVersion = computeSchemaVersion(collections);\n return cachedVersion;\n };\n\n router.get(\"/contract\", (c) => {\n const collections = config.collectionRegistry.getRawCollections();\n const serialized = serializeCollections(collections).map(stripNonClientFields);\n\n // In `baas` mode the collections are whatever introspection found at\n // boot, so the version has to be computed from them rather than taken\n // from a build that never saw them.\n const schemaVersion = schemaVersionOf(collections);\n\n const contract: RebaseProjectContract = {\n schemaVersion,\n runtime: {\n version: config.runtimeVersion ?? \"unknown\",\n contract: RUNTIME_CONTRACT_VERSION\n },\n collections: serialized,\n collectionSlugs: collections\n .map(collection => collection.slug)\n .filter((slug): slug is string => Boolean(slug))\n .sort(),\n generatedAt: new Date().toISOString()\n };\n\n c.header(SCHEMA_VERSION_HEADER, schemaVersion);\n return c.json(contract);\n });\n\n /**\n * Cheap drift check.\n *\n * Deliberately unauthenticated and deliberately tiny: two version stamps\n * and nothing else. A CI job that only wants to know whether its generated\n * SDK is stale should not need admin credentials, and a version stamp\n * reveals nothing about the schema it stands for.\n *\n * `runtime` is here as well as on `/contract` because the two answer\n * different questions and only one of them was reachable. Which runtime a\n * project is on decides whether a client's wire format is understood at\n * all, and it was published solely on the admin-gated route — so a CLI or\n * an SDK, the two callers that actually need to know, could not ask. That\n * is the same shape as the header this route echoes: a documented signal\n * with no reachable sender. `contract` is the number that matters for\n * compatibility; `version` names the release a human should quote.\n */\n router.get(\"/schema-version\", (c) => {\n const schemaVersion = schemaVersionOf(config.collectionRegistry.getRawCollections());\n c.header(SCHEMA_VERSION_HEADER, schemaVersion);\n return c.json({\n schemaVersion,\n runtime: {\n version: config.runtimeVersion ?? \"unknown\",\n contract: RUNTIME_CONTRACT_VERSION\n }\n });\n });\n\n logger.debug(\"Contract routes mounted\");\n return router;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"contract-routes-eLxV0le1.js","names":[],"sources":["../../types/src/types/project_manifest.ts","../../types/src/types/collection_contract.ts","../../types/src/types/schema_version.ts","../src/api/contract-routes.ts"],"sourcesContent":["/**\n * The project manifest (`rebase.json`) and the build artifacts derived from it.\n *\n * Three separate documents live in this file, and keeping them distinct matters:\n *\n * 1. {@link RebaseProjectManifest} — `rebase.json`. **Authored** by the developer,\n * committed to the repository. Declares topology only: which runtime major the\n * project targets, and which apps *this repository* contributes to the project.\n * Schema, security rules, hooks and functions stay in TypeScript under the\n * config package — nothing that needs a type system belongs here.\n *\n * 2. {@link RebaseProjectLink} — the per-checkout link (`.rebase/cloud.json`).\n * **Not committed**, because it is per-developer like a git remote. Says which\n * deployed project this working copy points at, whether that is a Rebase Cloud\n * project or the base URL of a self-hosted backend.\n *\n * 3. {@link RebaseBundleManifest} — `manifest.json` inside a built bundle.\n * **Generated**, never hand-edited. It is the lockfile analogue: the exact\n * contract a built artifact claims to satisfy, which the runtime validates\n * before it boots and a control plane validates before it deploys.\n *\n * A repository declares only the apps it contains. The set of apps belonging to a\n * project is held by the project itself, which is what makes multi-repo projects\n * work: two repositories never need to know about each other, only about the\n * project.\n */\n\nimport type { StorageSourceDefinition } from \"./storage_source\";\nimport type { ResourceGraph } from \"./resources\";\n\n/**\n * Which kind of thing an app is.\n *\n * - `backend` — the collections/hooks/functions that define the project's API.\n * Exactly one per *project* (not per repository); the registry enforces it.\n * - `static` — a pre-built client bundle (SPA, static site), served from the\n * backend process at its declared `path` or from a CDN. The admin panel is\n * one of these: it is an app in the user's repository like any other.\n *\n * That is the whole list. Ownership of the server process is a property of the\n * backend app ({@link RebaseBackendAppConfig.runtime}), not an app type.\n */\nexport type RebaseAppType = \"backend\" | \"static\";\n\n/**\n * The backend app: the project's API surface.\n *\n * Paths are relative to the directory holding `rebase.json`. The defaults match\n * the layout `rebase init` scaffolds, so a stock project may declare simply\n * `{ \"type\": \"backend\", \"runtime\": \"managed\" }`.\n */\nexport interface RebaseBackendAppConfig {\n type: \"backend\";\n /**\n * Who owns the process this backend runs in.\n *\n * - `managed` — the platform's runtime image boots this project's bundle.\n * You supply collections, functions, crons and schema; Rebase supplies the\n * server.\n * - `custom` — this repository builds its own image and entrypoint. The\n * escape hatch: full control, no managed-runtime guarantees.\n *\n * Independent of *where* it runs. Both run on Rebase Cloud and both\n * self-host — the destination lives in `.rebase/cloud.json`, not here. See\n * `infra/docker/docker-compose.selfhost.yml`, which boots a managed bundle on a\n * developer's own Docker host.\n *\n * This is authored rather than inferred on purpose. It is the single most\n * consequential fact about a deployment, and inferring it is what used to\n * land projects on the custom runtime without anyone choosing it.\n */\n runtime: \"managed\" | \"custom\";\n /** Directory of the config package (collections + index). Default `config`. */\n config?: string;\n /** Directory of server functions. Default `backend/functions`. */\n functions?: string;\n /** Directory of cron job definitions. Default `backend/crons` when present. */\n crons?: string;\n /**\n * Path to the generated Drizzle schema module (tables/enums/relations).\n * Default `backend/src/schema.generated.ts`.\n */\n schema?: string;\n /**\n * Module path (relative to `config`) exporting the auth users collection as\n * its default export. Default `collections/users`.\n */\n usersCollection?: string;\n\n /**\n * `runtime: \"custom\"` only. Dockerfile path relative to the repository root.\n * Default `Dockerfile`.\n */\n dockerfile?: string;\n /** `runtime: \"custom\"` only. Build context relative to the root. Default `.`. */\n context?: string;\n /** `runtime: \"custom\"` only. Port the container listens on. Default 8080. */\n port?: number;\n}\n\n/**\n * A static client bundle — SPA or static site — built here and served at `path`.\n */\nexport interface RebaseStaticAppConfig {\n type: \"static\";\n /** Package directory containing the client sources. */\n root: string;\n /** Command that produces `output`. Run from the repository root. */\n build?: string;\n /** Directory of built assets, relative to the repository root. */\n output: string;\n /**\n * Public base path this app is served under. Default `/`.\n *\n * Several static apps run in one process, each at its own path — the API at\n * `/api`, a site at `/`, the admin at `/admin` — which is what keeps a\n * self-hosted deployment a single container.\n *\n * **This is a build-time input, not only a serving concern.** An app mounted\n * at `/admin` must be *built* for `/admin` (Vite's `base`), or `index.html`\n * loads and every asset 404s: a blank page with no server error. `rebase\n * build` passes it as `REBASE_APP_BASE` and asserts the emitted HTML honours\n * it. Changing this value requires rebuilding the app.\n */\n path?: string;\n /**\n * Serve `index.html` for unmatched paths under `path` (client-side routing).\n * Default `true` — the overwhelmingly common case for a client app, and a\n * static *site* generator emits real files for its routes anyway.\n */\n spa?: boolean;\n /**\n * Where this app mounts the Rebase CMS, as a URL path — the address you\n * would type to reach it, not a path relative to `path`.\n *\n * The CMS is an ordinary React component in the developer's own app\n * (`<RebaseCMS basePath=\"/admin\">`), so its address is a *client-side\n * route*: nothing on the server, in the bundle, or in the control plane can\n * observe it. A project whose CMS sits at `/admin` inside a frontend that\n * also serves a product at `/` is indistinguishable, from the outside, from\n * one that has no CMS at all — which is exactly how a Rebase Cloud project\n * came to have no discoverable admin URL anywhere in its console.\n *\n * Declaring it is the only way that fact travels. It is carried into the\n * bundle manifest, recorded on the project's app row at deploy, and is what\n * lets the console (and `rebase apps list`) offer a link straight to it.\n *\n * Must be `path` itself or something beneath it, since the app serving that\n * URL is the one that has to answer for it. Absent means this app does not\n * mount the CMS — the common case for a marketing site or a product app.\n *\n * @example \"/\" — the whole app is the CMS, as `rebase init` scaffolds it\n * @example \"/admin\" — the CMS is one route of a larger app\n */\n cms?: string;\n}\n\nexport type RebaseAppConfig = RebaseBackendAppConfig | RebaseStaticAppConfig;\n\n/**\n * Path prefixes the backend owns, which no static app may claim.\n *\n * One process — and, on the platform, one hostname — serves both the API and\n * however many static apps a project has. Mounting is longest-path-first, so an\n * app declaring `/api` would win against the API itself and every request to it\n * would be answered with that app's `index.html`: a 200 carrying HTML where the\n * caller expected JSON, from a project that looks deployed and healthy.\n *\n * Declared here rather than in either enforcer because both must agree. The CLI\n * checks it so a developer finds out while editing `rebase.json`; the control\n * plane checks it again at deploy intake, because the front door's correctness\n * cannot rest on a check that ran in somebody else's CLI — and a repository can\n * be deployed by a CLI older than this rule.\n */\nexport const RESERVED_BACKEND_PREFIXES = [\"/api\", \"/health\", \"/healthz\", \"/livez\", \"/readyz\", \"/metrics\"] as const;\n\n/**\n * Whether `path` collides with a prefix the backend owns.\n *\n * Compares at segment boundaries, so `/api` and `/api/v2` collide while\n * `/apidocs` does not — the same rule the router matches with, because a check\n * that is stricter than the router rejects paths that would have worked, and one\n * that is looser admits paths that will not.\n */\nexport function reservedPrefixFor(path: string): string | undefined {\n const normalized = path.endsWith(\"/\") && path !== \"/\" ? path.slice(0, -1) : path;\n return RESERVED_BACKEND_PREFIXES.find(\n reserved => normalized === reserved || normalized.startsWith(`${reserved}/`)\n );\n}\n\n/**\n * One declared storage source, as authored in `rebase.json`.\n *\n * The key comes from the enclosing record, so this is\n * {@link StorageSourceDefinition} minus its `key` — the same document the\n * runtime registry and the frontend router consume, expressed the way a JSON\n * object naturally expresses \"a set of named things\".\n */\nexport interface RebaseStorageSourceConfig {\n /** Engine backing this source: `local`, `s3`, `gcs`, or a custom id. */\n engine: string;\n /**\n * How the frontend reaches it. Default `server` (proxied through\n * `/api/storage`). `direct` means a provider SDK talks to the bucket and the\n * backend is not in the upload path.\n */\n transport?: \"server\" | \"direct\";\n /** Human-readable label for the console and the admin UI. */\n label?: string;\n}\n\n/**\n * `rebase.json` — the authored project manifest.\n */\nexport interface RebaseProjectManifest {\n /** JSON Schema URL, for editor completion. Ignored by the tooling. */\n $schema?: string;\n /**\n * The runtime contract **major** this project targets, as a semver range\n * (e.g. `^1`, `~1.4`, or an exact `1.4.2` to pin).\n *\n * The platform upgrades patches and minors underneath a project without\n * asking; it never crosses a major. See {@link RUNTIME_CONTRACT_VERSION}.\n *\n * Named `rebase` rather than `runtime` so that `runtime` means exactly one\n * thing — {@link RebaseBackendAppConfig.runtime}, who owns the process. It\n * reads like `engines` in a `package.json`, which is what it is.\n */\n rebase: string;\n /**\n * Apps this repository contributes, keyed by app name. The key is the app's\n * identity within the project: it is what `rebase deploy <app>` names, what\n * client credentials are issued against, and what a second repository must\n * not collide with.\n */\n apps: Record<string, RebaseAppConfig>;\n /**\n * Buckets are NOT declared here any more.\n *\n * They were, and the runtime merged this block with the declarations in\n * config code — a bucket named in both had one engine kept and the other\n * silently discarded. Two homes for one concept, with a merge to decide\n * between them, is the shape this whole model replaced.\n *\n * `bucket(\"media\", { engine: \"s3\" })` in the project's config declares one\n * now, and `rebase resources --write` generates `rebase.resources.json`,\n * which is what a host reads before a build. A `storage` block left in this\n * file is refused by the validator, by name, with the replacement in the\n * message — not ignored, because a key that still parses and does nothing\n * is the failure this removed.\n */\n /**\n * Repository-wide opt-out from anonymous CLI usage sharing.\n *\n * **Only `false` does anything.** It suppresses sharing for everyone who\n * clones this repository, overriding each developer's own answer — an\n * organisation setting policy for work done on its behalf, the same shape\n * as a committed `.npmrc`.\n *\n * `true` is deliberately ignored, and the CLI says so rather than obeying\n * quietly. This file is committed, so a `true` here would be one developer\n * answering a privacy question for every colleague who later clones the\n * repo — consent by proxy, which is the exact thing opt-in exists to\n * prevent. Individuals answer at `rebase init`, or with `rebase telemetry enable` / `disable`.\n */\n telemetry?: boolean;\n}\n\n/**\n * The per-checkout project link.\n *\n * Deliberately separate from `rebase.json`: the manifest is committed and shared,\n * while the link is per-developer. Keeping them in one file would mean either\n * committing someone's project id or gitignoring the topology.\n */\nexport interface RebaseProjectLink {\n /**\n * A Rebase Cloud project id, or the base URL of any running Rebase backend\n * (`https://api.example.com`). Both are first-class: every command that\n * accepts a project reference accepts either, so a self-hosted project has\n * the same tooling as a cloud one.\n */\n project: string;\n /** Organization slug. Cloud projects only. */\n org?: string;\n /** Explicit API base URL, when it differs from the project's default. */\n apiUrl?: string;\n}\n\n/**\n * Whether a project can run on the managed runtime, and if not, precisely why.\n *\n * The reasons are returned rather than summarised so tooling can print something\n * a developer can act on. \"Not eligible\" is never a dead end — it selects the\n * custom-runtime path, which still deploys.\n */\nexport interface ManagedCompatibility {\n eligible: boolean;\n reasons: string[];\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Bundle\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Version of the bundle *format* itself.\n *\n * Bumped only when the on-disk layout changes in a way an older runtime could\n * not read. A runtime accepts any bundle whose `bundleFormat` is less than or\n * equal to its own — old bundles keep booting on new runtimes, which is the\n * whole point of separating the artifact from the engine.\n *\n * - **1** — `mode: \"cms\" | \"baas\" | \"static\"`, `entry.static` a single directory\n * string, `entry.admin` for a bundled admin panel.\n * - **2** — `kind: \"backend\" | \"static\"`, `entry.static` a list of\n * {@link RebaseBundleStatic}, `entry.admin` removed. A format-1 runtime reading\n * one of these would find no `mode` and an array where it expects a string, so\n * the bump is what turns that into a refusal to boot instead of a bundle that\n * starts and serves nothing.\n */\nexport const BUNDLE_FORMAT_VERSION = 2;\n\n/**\n * The runtime contract major.\n *\n * Distinct from the `@rebasepro/server` package version: the package may release\n * any number of minors and patches while this stays put. It changes only when\n * the bundle/runtime contract breaks compatibility, and a project's\n * `manifest.runtime` range is matched against *this*.\n *\n * ## v2 — resources are declared, not configured\n *\n * `RebaseBackendConfig.dataSources` and `.storageSources` are gone. A project\n * declares its databases and buckets with `database()` / `bucket()` in its\n * config, and the runtime reads those declarations.\n *\n * This had to be a major, and the reason is the managed tier: it moves projects\n * onto new images WITHOUT rebuilding them. A bundle built against v1 exports\n * those keys, and a v2 runtime refuses them at boot — so without this bump, one\n * image rollout would crash-loop every tenant that had ever declared a second\n * database or bucket, in a wave, with the cause in a container log nobody is\n * watching.\n *\n * With the bump, a v1 bundle on a v2 runtime is refused by\n * `assertBundleCompatibility` with the remedy in the message, and the platform\n * keeps it on a v1 image until it is rebuilt. That is the whole purpose of this\n * number.\n *\n * **Release order matters and is not optional.** The control plane is the side\n * that rejects, so it ships FIRST: raise `SUPPORTED_RUNTIME_CONTRACT` in the\n * saas repo (it rejects only `contract >` its own, so it then accepts both),\n * deploy that, and only then release a runtime implementing v2. Shipping the\n * runtime first turns every deploy into a rejected intake blaming the tenant's\n * bundle.\n */\nexport const RUNTIME_CONTRACT_VERSION = 1;\n\n/** Where the runtime finds each part of the bundle. Paths are bundle-relative. */\nexport interface RebaseBundleEntrypoints {\n /** Compiled config package directory (collections live under it). */\n config?: string;\n /** Compiled collections directory, when it differs from `<config>/collections`. */\n collections?: string;\n /** Compiled functions directory. */\n functions?: string;\n /** Compiled crons directory. */\n crons?: string;\n /** Compiled Drizzle schema module. */\n schema?: string;\n /** Module exporting the auth users collection (default export). */\n usersCollection?: string;\n /**\n * Built static apps to serve from this process, in declaration order.\n *\n * A list rather than a single directory because one process serves several\n * apps at different paths — a site at `/` and the admin at `/admin`. The\n * runtime mounts them longest-path-first so the `/`-rooted app's catch-all\n * does not claim its siblings' URLs.\n */\n static?: RebaseBundleStatic[];\n}\n\n/** One built static app inside a bundle. */\nexport interface RebaseBundleStatic {\n /** Public base path, e.g. `/` or `/admin`. */\n path: string;\n /** Bundle-relative directory holding the built assets. */\n dir: string;\n /** Serve `index.html` for unmatched paths under `path`. */\n spa: boolean;\n /**\n * The app's name in `rebase.json`.\n *\n * `dir` is `static/<name>` and has been since folding was written, so this\n * is recoverable by string surgery — which is precisely why it is stated\n * instead. A control plane reconciling app rows against this list has to\n * match them by name, and a consumer that has to re-derive an identifier\n * from a path is one refactor away from matching nothing and registering a\n * duplicate app on every deploy.\n *\n * Optional because bundles built before this field exists do not carry it;\n * a reader that needs a name falls back to the last segment of `dir`.\n */\n name?: string;\n /**\n * Where this app mounts the Rebase CMS, as a URL path.\n *\n * Copied from the app's declaration — see {@link RebaseStaticAppConfig.cms}\n * for why a client-side route has to be declared to be knowable at all.\n */\n cms?: string;\n}\n\n/**\n * A native module found in the dependency closure.\n *\n * Recorded rather than merely counted so a rejection can name the offending\n * package instead of saying \"something here is native\".\n */\nexport interface NativeDependency {\n name: string;\n /** Why it was flagged — a `.node` binary, a gyp build, or an install script. */\n reason: string;\n}\n\n/**\n * `manifest.json` — generated, and the document the runtime and control plane\n * both validate against.\n */\n/**\n * One custom function, as recorded in a built bundle.\n *\n * @see RebaseBundleManifest.functions\n */\nexport interface RebaseBundleFunction {\n /**\n * The filename without its extension — which is also the URL segment it\n * mounts at (`/api/functions/<name>`), the API-key permission that grants\n * it, and the name `REBASE_FUNCTIONS_ONLY` selects by. One identity, used\n * everywhere.\n */\n name: string;\n /** Path inside the bundle, so a host can point at the file. */\n file: string;\n /**\n * `false` when the function's own source imports a Node built-in or a\n * package that needs one.\n *\n * Descriptive, never a gate: nothing refuses to build or deploy on this. It\n * says where this function *could* run, not where it should.\n */\n portable: boolean;\n /**\n * Why it is not portable — one short phrase per reason, deduplicated.\n * Absent when it is.\n */\n requires?: string[];\n}\n\nexport interface RebaseBundleManifest {\n /** @see BUNDLE_FORMAT_VERSION */\n bundleFormat: number;\n runtime: {\n /** The `runtime` range copied from `rebase.json`. */\n range: string;\n /** Exact `@rebasepro/server` version this bundle was built against. */\n builtAgainst: string;\n /** Runtime contract major this bundle requires. */\n contract: number;\n };\n /**\n * Hash of the compiled collection definitions.\n *\n * This is the contract stamp. A generated SDK records the value it was built\n * from, a client sends it back, and a mismatch is what lets the platform say\n * \"this app was built against an older schema\" instead of failing mysteriously\n * at the first request. It covers collections only — a hook edit does not\n * change a client's contract, so it must not invalidate every SDK.\n */\n schemaVersion: string;\n /** Which app in `rebase.json` this bundle was built from. */\n app: string;\n /**\n * What the runtime does with this bundle.\n *\n * - `backend` — boot the full server: database, auth and the data API, plus\n * any static apps in `entry.static`.\n * - `static` — no backend at all: serve `entry.static` and nothing else. No\n * database, no auth, no data sources. This is how a static app runs on the\n * same image as the backend.\n *\n * Replaces an earlier `mode: \"cms\" | \"baas\" | \"static\"`. The cms/baas\n * distinction was never a third kind of thing — it is simply whether\n * `entry.config` is present, so it is derived rather than declared.\n */\n kind: \"backend\" | \"static\";\n entry: RebaseBundleEntrypoints;\n /** Collection slugs contained in the bundle, for quick inspection. */\n collections?: string[];\n /**\n * Every custom function in the bundle, named and classified.\n *\n * Two things are recorded per function, and both are answers a host would\n * otherwise have to get by importing user code:\n *\n * - **What it is called.** That name is the function's identity everywhere —\n * the URL segment it mounts at, the `functions/<name>` API-key\n * permission, the value `REBASE_FUNCTIONS_ONLY` selects by. A host that\n * wants to give one slow function its own replica count currently has to\n * boot the bundle to discover what is in it.\n * - **Whether it needs Node.** Purely descriptive: a function that opens a\n * file or runs raw SQL is a fine function, and every deployment today is\n * a Node process. It is recorded because the question \"which of these\n * could run somewhere else\" has to be answerable from the artifact, and\n * because answering it per-file after the fact — across a codebase\n * already written — is the expensive version of the same question.\n *\n * Absent on a bundle built before this field existed, which is why every\n * consumer must treat it as optional rather than as an empty list.\n */\n functions?: RebaseBundleFunction[];\n hooks: {\n /**\n * Whether the dependency closure contains native code.\n *\n * The managed runtime refuses these: a prebuilt binary cannot be run on\n * an image the platform did not build it for, and the honest failure is\n * at deploy time rather than at 3am in a crash loop.\n */\n native: boolean;\n nativeModules?: NativeDependency[];\n };\n /**\n * What the bundle's config says about storage access control.\n *\n * Storage is not under RLS and its keys share one flat namespace, so a\n * deployment with file storage enabled and no access model serves every\n * user's files to every signed-in user. The runtime refuses to boot in that\n * state — which, on a hosted platform that enables storage from the *console*\n * rather than from the bundle, surfaces as a crash loop the developer cannot\n * read.\n *\n * Recording it here lets a host reject the deploy with the reason instead.\n * Absent on bundles built before this field existed.\n */\n storage?: {\n /** Whether the config package exports a `storageAuthorize` hook. */\n authorize: boolean;\n /**\n * Buckets, on bundles built before {@link RebaseBundleManifest.resources}.\n *\n * No longer written. A host reads `resources`, which carries every kind\n * in one list; this stays declared so a control plane can keep reading\n * the bundles a project shipped before it was rebuilt.\n */\n sources?: StorageSourceDefinition[];\n };\n /**\n * Everything the project declares it needs — databases, buckets, topics,\n * and whatever kind is registered next.\n *\n * Recorded so a host can tell, from the artifact alone and before starting\n * anything, what a deploy will need provisioned. That question used to be\n * answerable for buckets and for nothing else, because buckets were the\n * only kind written into an artifact — which is how a project's databases\n * became invisible to the platform that runs them.\n *\n * Absent on bundles built before this field existed.\n */\n resources?: ResourceGraph;\n deps: {\n /** Runtime dependencies of user code, as declared. */\n declared: Record<string, string>;\n /**\n * The dependency tree ships *inside* the bundle, already installed.\n *\n * Absent or false means the tree is declared but not present, and\n * whoever boots the bundle has to install it. On the managed runtime that\n * install runs in an init container on **every** pod start — the bundle\n * lives on a volume that is wiped each time — and it is the single\n * largest cost in a managed pod's life: 35–55 seconds of a 40–60 second\n * cold start. Since a pod restarts on every eviction, node failure, OOM\n * and runtime rollout, that number is not a startup detail. It is what an\n * outage costs.\n *\n * Vendoring moves the install to build time, where it happens once. It is\n * skipped when the closure contains native code, because a prebuilt\n * binary is only valid for the platform it was built for — see\n * {@link vendorTarget} for what \"the platform\" means here.\n */\n vendored?: boolean;\n /**\n * What {@link vendored} was resolved for, recorded so a mismatch can be\n * refused rather than discovered at import time.\n *\n * Cross-platform vendoring is safe for pure JavaScript and unsafe for\n * anything compiled, and the boundary between them is not always visible\n * in a dependency list: `esbuild` is pure-JS with a *platform-specific\n * optional dependency* holding the actual binary, so an install run on a\n * developer's Mac silently produces a tree that cannot run on the Linux\n * image. The install therefore resolves optional dependencies for the\n * target explicitly rather than for the machine it runs on, and records\n * the answer here.\n */\n vendorTarget?: {\n /** npm `--os`, e.g. `linux`. */\n os: string;\n /** npm `--cpu`, e.g. `x64`. */\n cpu: string;\n /** Node major the tree was resolved for. */\n node: string;\n };\n };\n build: {\n /** `@rebasepro/cli` version that produced this bundle. */\n cli: string;\n /** Node major the bundle was compiled on. */\n node: string;\n /** ISO-8601. */\n createdAt: string;\n };\n}\n\n/** The contract a running backend serves at `GET /api/meta/contract`. */\nexport interface RebaseProjectContract {\n /** Matches {@link RebaseBundleManifest.schemaVersion}. */\n schemaVersion: string;\n runtime: {\n /** `@rebasepro/server` version currently running. */\n version: string;\n contract: number;\n };\n /** Full collection definitions, serialized — the input to SDK generation. */\n collections: unknown[];\n /** Collection slugs, for cheap inspection without parsing the definitions. */\n collectionSlugs: string[];\n generatedAt: string;\n}\n\n/** Header carrying the schema version an SDK was generated from. */\nexport const SCHEMA_VERSION_HEADER = \"x-rebase-schema\";\n","import type { CollectionConfig } from \"./collections\";\n\n/**\n * Serializing collections so they survive a network hop.\n *\n * A collection definition is not plain data. Relations point at their target\n * with a *function* (`target: () => usersCollection`) so two collections can\n * reference each other without an import cycle, and collections also carry\n * callbacks, custom views and component references. `JSON.stringify` silently\n * drops every one of those, which matters because the SDK generator *calls*\n * `relation.target()` to decide whether a foreign key is a string or a number.\n * Serialize naively and remote SDK generation produces subtly wrong types\n * instead of failing — the worst possible outcome.\n *\n * So relation targets are resolved to a slug reference on the way out and\n * rebuilt into functions on the way in. Everything else that cannot cross a wire\n * is dropped deliberately: an SDK is generated from the *shape* of the data, and\n * server-side behaviour is neither useful to a client nor safe to publish.\n */\n\n/** Marker replacing a relation's `target` function in serialized form. */\nexport interface SerializedCollectionRef {\n __collectionRef: string;\n}\n\nexport function isSerializedCollectionRef(value: unknown): value is SerializedCollectionRef {\n return typeof value === \"object\"\n && value !== null\n && typeof (value as SerializedCollectionRef).__collectionRef === \"string\";\n}\n\n/** Depth limit for the walk — deep enough for real configs, finite for cyclic ones. */\nconst MAX_DEPTH = 64;\n\n/**\n * Resolve whatever a `target` thunk returns down to a collection.\n *\n * A target may be the collection, a module namespace (when the authoring file\n * used `import * as`), or a default-export wrapper. All three appear in real\n * projects, and the SDK generator already unwraps them the same way.\n */\nfunction unwrapTarget(value: unknown): CollectionConfig | undefined {\n if (!value || typeof value !== \"object\") return undefined;\n const candidate = value as { default?: unknown; __esModule?: boolean; properties?: unknown };\n if (candidate.default || candidate.__esModule) {\n const inner = candidate.default;\n if (inner && typeof inner === \"object\") return inner as CollectionConfig;\n }\n if (candidate.properties) return value as CollectionConfig;\n return undefined;\n}\n\n/** The identity a serialized reference uses. Slug first — it is the routing key. */\nfunction refFor(collection: CollectionConfig | undefined): string | undefined {\n if (!collection) return undefined;\n const withPath = collection as CollectionConfig & { path?: string };\n return collection.slug || withPath.path || collection.name;\n}\n\n/**\n * Deep-copy a value into something JSON can carry.\n *\n * `target` keys are special-cased into refs. Other functions vanish, cycles are\n * cut, and everything else is copied structurally.\n */\n/** Shared walk state: the memo, plus a count of depth-cap hits. */\ninterface WalkState {\n memo: WeakMap<object, unknown>;\n /**\n * How many times the walk has truncated a subtree — by hitting the depth\n * cap, or by cutting a cycle.\n *\n * Either kind of truncation makes a result valid only at the *position* it\n * was produced at, so caching it and serving it elsewhere silently drops\n * content that would have been included. Comparing this counter before and\n * after a node's children tells us whether its result is position-\n * independent and therefore safe to memoize.\n *\n * The cycle case is the subtle one: with `a.b = b` and `b.a = a`, serializing\n * `{ first: b, second: a }` visits `a` beneath `b` — where the cycle back to\n * `b` is cut — and would then reuse that truncated `a` for `second`, where\n * nothing needed cutting.\n */\n truncations: number;\n}\n\nfunction toSerializable(\n value: unknown,\n seen: WeakSet<object>,\n depth: number,\n state: WalkState,\n key?: string\n): unknown {\n if (depth > MAX_DEPTH) {\n state.truncations++;\n return undefined;\n }\n\n if (typeof value === \"function\") {\n // Only a relation target carries information a client needs. Calling it\n // is safe here — this runs on the server, where the target module is\n // already loaded — and a throwing target simply yields no reference,\n // which degrades the generated FK type rather than failing the request.\n if (key === \"target\") {\n try {\n const resolved = unwrapTarget((value as () => unknown)());\n const ref = refFor(resolved);\n return ref ? { __collectionRef: ref } : undefined;\n } catch {\n return undefined;\n }\n }\n return undefined;\n }\n\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n if (value instanceof Date) return value.toISOString();\n if (value instanceof RegExp) return value.source;\n\n if (seen.has(value as object)) {\n state.truncations++;\n return undefined;\n }\n\n // A shared (non-cyclic) subgraph is reachable by many paths, and `seen` is a\n // *path* set — released in the `finally` below so a node referenced twice in\n // different branches is emitted twice rather than dropped as a false cycle.\n // Without memoization that makes the walk exponential in depth: a diamond\n // graph 20 levels deep took ~400ms, and each further level doubled it. The\n // result is a plain data tree, so handing back the same converted object for\n // a repeat visit is indistinguishable after JSON.stringify.\n const cached = state.memo.get(value as object);\n if (cached !== undefined) return cached;\n\n seen.add(value as object);\n const truncationsBefore = state.truncations;\n const memoize = (result: unknown): unknown => {\n // Only cache a result that nothing was cut from.\n if (result !== undefined && state.truncations === truncationsBefore) {\n state.memo.set(value as object, result);\n }\n return result;\n };\n\n try {\n if (Array.isArray(value)) {\n const items = value\n .map(item => toSerializable(item, seen, depth + 1, state))\n .filter(item => item !== undefined);\n // A container that had content, none of which can be represented, is\n // itself unrepresentable — see the note below.\n return memoize(value.length > 0 && items.length === 0 ? undefined : items);\n }\n\n // A React element or component reference has no meaning to a client and\n // will not survive JSON anyway.\n if (\"$$typeof\" in (value as Record<string, unknown>)) return undefined;\n\n const entries = Object.entries(value as Record<string, unknown>);\n const out: Record<string, unknown> = {};\n for (const [k, v] of entries) {\n const converted = toSerializable(v, seen, depth + 1, state, k);\n if (converted !== undefined) out[k] = converted;\n }\n\n // Drop a container whose entire content was dropped.\n //\n // `callbacks: { beforeSave() {…} }` would otherwise serialize to\n // `callbacks: {}` — an empty husk that carries no information but is not\n // *nothing*, so it lands in the payload and, worse, in the schema hash.\n // Editing a hook would then change every client's schema version and\n // report perfectly current SDKs as stale.\n //\n // A container that started empty stays empty: `properties: {}` is a\n // deliberate statement, not a casualty.\n if (entries.length > 0 && Object.keys(out).length === 0) return undefined;\n\n return memoize(out);\n } finally {\n // Released so a collection referenced twice in different branches is\n // emitted twice rather than being dropped as a false cycle.\n seen.delete(value as object);\n }\n}\n\n/**\n * Serialize collections for transport over the contract endpoint.\n *\n * Sorted by slug so the output — and therefore the schema hash computed from it\n * — does not depend on filesystem ordering.\n */\nexport function serializeCollections(collections: CollectionConfig[]): unknown[] {\n return [...collections]\n .sort((a, b) => String(a.slug ?? \"\").localeCompare(String(b.slug ?? \"\")))\n .map(collection => toSerializable(withoutAdminBlock(collection), new WeakSet(), 0, {\n memo: new WeakMap(),\n truncations: 0\n }))\n .filter((c): c is Record<string, unknown> => c !== undefined);\n}\n\n/**\n * Drop the admin block before the walk.\n *\n * Nothing downstream of serialization is an admin panel. The contract endpoint\n * feeds remote SDK generation, and `rebase build` writes the result into a bundle\n * manifest that only the backend runtime reads. The block would survive the walk\n * as a husk anyway — its React elements and component functions are dropped\n * individually — and that husk has two costs worth avoiding: it puts every custom\n * component's *file path* on an endpoint whose job is to describe data shapes, and\n * it grows a payload that is fetched and cached per project.\n *\n * Removing it here rather than at each call site means one chokepoint, so a future\n * consumer of `serializeCollections` cannot forget.\n *\n * Child collections carry their own block, so this recurses — stripping only the\n * top level was the mistake `stripNonClientFields` in the contract routes already\n * had to fix once for security rules.\n */\nfunction withoutAdminBlock(collection: CollectionConfig): CollectionConfig {\n const { admin: _admin, ...rest } = collection as CollectionConfig & Record<string, unknown>;\n const nested = rest as Record<string, unknown>;\n if (Array.isArray(nested.subcollections)) {\n nested.subcollections = nested.subcollections.map(\n (child) => withoutAdminBlock(child as CollectionConfig)\n );\n }\n return rest as CollectionConfig;\n}\n\n/**\n * Rebuild collections received from a contract endpoint.\n *\n * Relation refs become real thunks resolving through the returned set, so\n * downstream consumers — the SDK generator above all — see exactly the shape\n * they would have seen had the collections been imported from source.\n *\n * A ref naming a collection that is not in the payload resolves to `undefined`\n * rather than throwing: the generator already tolerates an unresolvable target\n * by falling back to a permissive key type, and a partial contract should still\n * produce a usable SDK.\n */\nexport function deserializeCollections(payload: unknown[]): CollectionConfig[] {\n const collections = payload\n .filter((c): c is Record<string, unknown> => typeof c === \"object\" && c !== null)\n .map(c => ({ ...c })) as unknown as CollectionConfig[];\n\n const bySlug = new Map<string, CollectionConfig>();\n for (const collection of collections) {\n const ref = refFor(collection);\n if (ref) bySlug.set(ref, collection);\n }\n\n const rehydrate = (value: unknown, depth: number): void => {\n if (depth > MAX_DEPTH || !value || typeof value !== \"object\") return;\n\n if (Array.isArray(value)) {\n for (const item of value) rehydrate(item, depth + 1);\n return;\n }\n\n const record = value as Record<string, unknown>;\n for (const [key, child] of Object.entries(record)) {\n if (key === \"target\" && isSerializedCollectionRef(child)) {\n const slug = child.__collectionRef;\n record.target = () => bySlug.get(slug);\n continue;\n }\n rehydrate(child, depth + 1);\n }\n };\n\n for (const collection of collections) rehydrate(collection, 0);\n return collections;\n}\n","import type { CollectionConfig } from \"./collections\";\nimport { serializeCollections } from \"./collection_contract\";\n\n/**\n * The schema version stamp.\n *\n * One function, used in three places that must agree or the whole drift-detection\n * story is noise: `rebase build` writes it into a bundle manifest, the runtime\n * serves it from the contract endpoint, and a generated SDK records the value it\n * was built from. If any two of those computed it differently, every client would\n * look permanently out of date.\n *\n * It covers **collections only** — the client's contract is the shape of the\n * data, so editing a hook or a server function must not invalidate every SDK in\n * every repository. That is a deliberate narrowing, not an oversight.\n */\n\n/** Stable stringify: object keys sorted at every level, so key order cannot alter the hash. */\nfunction canonicalize(value: unknown): string {\n if (value === null || typeof value !== \"object\") {\n return JSON.stringify(value) ?? \"null\";\n }\n if (Array.isArray(value)) {\n return `[${value.map(canonicalize).join(\",\")}]`;\n }\n const entries = Object.entries(value as Record<string, unknown>)\n .filter(([, v]) => v !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalize(v)}`).join(\",\")}}`;\n}\n\n/**\n * Reduce a collection to the parts a generated client is actually built from.\n *\n * The version answers one question — \"is this SDK stale?\" — so it must change\n * exactly when the generated types could change, and never otherwise. Hashing a\n * whole collection fails both halves of that:\n *\n * - Security rules, callbacks, icons, groups and UI settings do not appear in a\n * generated client, so including them reports perfectly current SDKs as stale.\n * - Worse, they are not stable *inputs*. The runtime applies default security\n * rules when it loads collections, so the same source hashed before and after\n * loading produced two different answers — a build-time stamp that could never\n * match the server that served it.\n *\n * Codegen reads the slug (for the `Database` key and type names), the properties,\n * and the relations. That is the projection.\n */\nfunction projectForCodegen(collection: CollectionConfig): Record<string, unknown> {\n const source = collection as CollectionConfig & {\n relations?: unknown;\n subcollections?: CollectionConfig[];\n path?: string;\n engine?: unknown;\n dataSource?: unknown;\n };\n\n return {\n slug: collection.slug ?? source.path,\n properties: collection.properties,\n relations: source.relations,\n // The engine decides whether relations are resolved at all: codegen asks\n // `getDataSourceCapabilities(collection.engine).supportsRelations`, and an\n // engine that answers no drops every foreign-key column from the\n // generated Row/Insert/Update types. Moving a collection to such an\n // engine is a real change to the generated types, so it has to move the\n // version. `dataSource` is what resolves to `engine`, so it counts too.\n engine: source.engine,\n dataSource: source.dataSource,\n subcollections: source.subcollections?.map(projectForCodegen)\n };\n}\n\n/**\n * Compute the canonical string a schema version hashes.\n *\n * Exposed separately so the hashing itself can differ by environment: Node has\n * `crypto`, and callers without it can still compare canonical forms directly.\n */\nexport function canonicalSchemaPayload(collections: CollectionConfig[]): string {\n const projected = serializeCollections(collections)\n .map(collection => projectForCodegen(collection as CollectionConfig));\n return canonicalize(projected);\n}\n\n/**\n * A short, non-cryptographic digest of the canonical payload.\n *\n * FNV-1a style, 64 bits, as two 32-bit halves. This is an identity, not a\n * security boundary: nothing trusts a schema version to prove anything, it only\n * answers \"is this the same schema as before\". A hand-rolled hash keeps this\n * module free of `node:crypto`, so the identical function runs in the browser,\n * in the CLI, and in the runtime — which is the property that actually matters.\n */\nexport function computeSchemaVersion(collections: CollectionConfig[]): string {\n const payload = canonicalSchemaPayload(collections);\n\n let h1 = 0x811c9dc5;\n let h2 = 0x01000193;\n\n for (let i = 0; i < payload.length; i++) {\n const code = payload.charCodeAt(i);\n h1 ^= code;\n // Multiply by the FNV prime using shifts to stay in 32-bit integer math.\n h1 = (h1 + ((h1 << 1) + (h1 << 4) + (h1 << 7) + (h1 << 8) + (h1 << 24))) >>> 0;\n h2 ^= code + i;\n h2 = (h2 + ((h2 << 1) + (h2 << 5) + (h2 << 9) + (h2 << 15) + (h2 << 24))) >>> 0;\n }\n\n const hex = (n: number): string => n.toString(16).padStart(8, \"0\");\n return `v1:${hex(h1)}${hex(h2)}`;\n}\n","import { Hono } from \"hono\";\nimport {\n RUNTIME_CONTRACT_VERSION,\n SCHEMA_VERSION_HEADER,\n computeSchemaVersion,\n serializeCollections,\n type CollectionConfig,\n type RebaseProjectContract\n} from \"@rebasepro/types\";\nimport type { HonoEnv } from \"./types\";\nimport { logger } from \"../utils/logger\";\n\n/**\n * The project contract endpoint.\n *\n * This is what makes a repository able to build against a project it does not\n * contain. Without it, a typed client can only be generated from local\n * collection *source*, which means every frontend must live in the same\n * repository as the backend. Serving the contract turns that around: an app\n * asks the project what its shape is, so a web app, a second web app and a\n * mobile app can each live wherever they like and none of them needs to know\n * about the others.\n *\n * Admin-gated. Collection definitions describe every table, column and relation\n * in the project, including ones no security rule would ever expose — that is a\n * map of the database, not public API documentation.\n */\n\nexport interface ContractRoutesConfig {\n collectionRegistry: { getRawCollections(): CollectionConfig[] };\n /**\n * The schema version recorded at build time.\n *\n * Preferred over recomputing, so that what a client is told matches exactly\n * what the bundle claims. It is recomputed only when a bundle did not record\n * one — a `baas`-mode project derives its collections from the live database\n * at boot, so there was nothing to hash when it was built.\n */\n schemaVersion?: string;\n /** Runtime package version, surfaced so a client can report what it built against. */\n runtimeVersion?: string;\n}\n\n/**\n * Strip everything a client does not need from a serialized collection.\n *\n * The generator reads the slug, the properties and the relations. It never reads\n * a security rule — but `securityRules` carries the raw SQL of every RLS\n * predicate guarding the project, which is a description of the authorization\n * model rather than of the data shape. Publishing it to anyone who can generate\n * an SDK gives away more than the endpoint is for, so it is removed here rather\n * than trusted not to matter.\n */\nfunction stripNonClientFields(collection: unknown): unknown {\n if (!collection || typeof collection !== \"object\") return collection;\n const {\n securityRules: _securityRules,\n callbacks: _callbacks,\n ...rest\n } = collection as Record<string, unknown>;\n\n // Subcollections are collections, and carry their own rules. Stripping only\n // the top level published every nested policy — the leak this exists to\n // prevent, just one level down.\n if (Array.isArray(rest.subcollections)) {\n rest.subcollections = rest.subcollections.map(stripNonClientFields);\n }\n\n return rest;\n}\n\nexport function createContractRoutes(config: ContractRoutesConfig): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n\n // Computing a version walks and canonicalizes every collection, and\n // `/schema-version` is deliberately unauthenticated and meant to be polled.\n // Recomputing per request would make a CI convenience into a CPU\n // amplification anyone could aim at the server. Collections do not change\n // after boot, so once is enough.\n let cachedVersion: string | undefined;\n const schemaVersionOf = (collections: CollectionConfig[]): string => {\n if (config.schemaVersion) return config.schemaVersion;\n if (cachedVersion === undefined) cachedVersion = computeSchemaVersion(collections);\n return cachedVersion;\n };\n\n router.get(\"/contract\", (c) => {\n const collections = config.collectionRegistry.getRawCollections();\n const serialized = serializeCollections(collections).map(stripNonClientFields);\n\n // In `baas` mode the collections are whatever introspection found at\n // boot, so the version has to be computed from them rather than taken\n // from a build that never saw them.\n const schemaVersion = schemaVersionOf(collections);\n\n const contract: RebaseProjectContract = {\n schemaVersion,\n runtime: {\n version: config.runtimeVersion ?? \"unknown\",\n contract: RUNTIME_CONTRACT_VERSION\n },\n collections: serialized,\n collectionSlugs: collections\n .map(collection => collection.slug)\n .filter((slug): slug is string => Boolean(slug))\n .sort(),\n generatedAt: new Date().toISOString()\n };\n\n c.header(SCHEMA_VERSION_HEADER, schemaVersion);\n return c.json(contract);\n });\n\n /**\n * Cheap drift check.\n *\n * Deliberately unauthenticated and deliberately tiny: two version stamps\n * and nothing else. A CI job that only wants to know whether its generated\n * SDK is stale should not need admin credentials, and a version stamp\n * reveals nothing about the schema it stands for.\n *\n * `runtime` is here as well as on `/contract` because the two answer\n * different questions and only one of them was reachable. Which runtime a\n * project is on decides whether a client's wire format is understood at\n * all, and it was published solely on the admin-gated route — so a CLI or\n * an SDK, the two callers that actually need to know, could not ask. That\n * is the same shape as the header this route echoes: a documented signal\n * with no reachable sender. `contract` is the number that matters for\n * compatibility; `version` names the release a human should quote.\n */\n router.get(\"/schema-version\", (c) => {\n const schemaVersion = schemaVersionOf(config.collectionRegistry.getRawCollections());\n c.header(SCHEMA_VERSION_HEADER, schemaVersion);\n return c.json({\n schemaVersion,\n runtime: {\n version: config.runtimeVersion ?? \"unknown\",\n contract: RUNTIME_CONTRACT_VERSION\n }\n });\n });\n\n logger.debug(\"Contract routes mounted\");\n return router;\n}\n"],"mappings":"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"}
package/dist/index.es.js CHANGED
@@ -4,7 +4,7 @@ globalThis.process ??= __rebaseProcess;
4
4
  __rebaseCreateRequire(import.meta.url);
5
5
  import { $ as isFieldOperation, A as createDataSourceRegistry, B as resolveCollectionRelations, D as restrictedFieldNames, E as effectiveAccess, F as securityRuleToConditions, G as suggestNearMiss, H as buildCompositeId, I as fieldKeyForColumn, K as toSnakeCase, L as findRelation, M as resolveDataSource, N as getEffectiveSecurityRules, O as defaultUsersCollection, Q as hasFieldOperation, R as getTableName, S as cursorToStartAfter, T as canWriteField, U as resolvePrimaryKeys, V as enumToObjectEntries, W as hydrateRegExp, X as BATCH_REF_KEY, Z as FIELD_OPERATORS, at as Vector, c as collectAllPages, ct as isUnsupported, g as topLevelIncludeNames, h as serializeInclude, it as GeoPoint, j as isRelationalCollection, k as CollectionRegistry, l as paginateFind, lt as unsupportedMethod, n as buildSdkData, nt as EntityReference, o as serializeFilter, ot as RebaseApiError, p as mergeIncludeSpecs, rt as EntityRelation, s as serializeLogicalCondition, st as RebaseClientError, t as buildRoutedRebaseData, u as resolveFindWindow, v as normalizeOrderBy, y as serializeOrderBy } from "./src-DqZ9YiGA.js";
6
6
  import { E as DEFAULT_DATA_SOURCE_KEY, F as sortKeyToString, I as toCanonicalOp, P as parseRelationAggregateSort, S as getCollectionDataPath, _ as resourceKinds, a as resourceToStorageSource, b as RLS_UID_SQL, c as DEFAULT_STORAGE_SOURCE_KEY, d as DEFAULT_RESOURCE_KEY, f as buildResourceGraph, g as resourceKind, h as resourceKeyOf, i as resourceToDataSource, l as findStorageSuffixCollision, m as resourceEnvSuffix, n as declaredQueueConsumers, o as setQueueRuntime, p as resolveResourceRefs, r as declaredSubscriptions, s as setTopicRuntime, u as storageEnvSuffix, v as RLS_JWT_SQL, w as isPostgresCollectionConfig, y as RLS_ROLES_SQL } from "./src-Br6ARbs6.js";
7
- import { n as ADMIN_PROPERTY_KEYS, t as ADMIN_COLLECTION_KEYS } from "./admin_block-0Xu0r6eZ.js";
7
+ import { n as ADMIN_PROPERTY_KEYS, t as ADMIN_COLLECTION_KEYS } from "./admin_block-DxKLmdiv.js";
8
8
  import { a as isDuplicateObjectRace, i as isConcurrentDdlRace, n as createDdlBootstrapper, o as isSQLAdmin, s as isSchemaEditingAdmin, t as CONCURRENT_DDL_SQLSTATES } from "./ddl-bootstrap-CfNvxMuK.js";
9
9
  import { i as SCHEMA_VERSION_HEADER, n as createContractRoutes, r as computeSchemaVersion } from "./contract-routes-eLxV0le1.js";
10
10
  import { $ as escapeHtml, A as MemoryRateLimitStore, B as resolveAuthHooks, C as providerVerifiedEmail, Ct as PUBLIC_STORAGE_PREFIX, D as DEFAULT_FUNCTIONS_ANONYMOUS_LIMIT, E as isBootstrapWindowOpen, F as registerDevEmailSink, G as getEmailOtpTemplate, H as validatePasswordStrength, I as SMTPEmailService, J as getPasswordResetTemplate, K as getEmailVerificationTemplate, L as createEmailService, M as clearActiveDevEmailSink, N as createDevEmailSink, O as createDataRateLimiter, P as extractLinks, Q as RawHtml, R as assertEmailLinkBases, S as pkceTokenParams, St as isOperationAllowed, T as createBuiltinAuthAdapter, U as verifyPassword, V as hashPassword, W as generateSecurePassword, X as getWelcomeEmailTemplate, Y as getUserInvitationTemplate, Z as resolveEmailBranding, _ as verifyOidcIdToken, _t as extractBearerToken, a as resolveRateLimitStoreKind, at as extractUserFromToken, b as createGoogleProvider, bt as scopeDataDriver, c as createSlackProvider, ct as publicObjectAuth, d as createDiscordProvider, dt as requireAuth, et as html, f as createTwitterProvider, ft as createApiKeyPreAuth, g as tryVerifyOidcIdToken, gt as validateApiKey, h as createMicrosoftProvider, ht as isApiKeyToken, i as createApiKeyStore, it as createRequireAuth, j as activeDevEmailSink, k as defaultAuthLimiter, l as createBitbucketProvider, lt as queryTokenAuth, m as createAppleProvider, mt as createStorageApiKeyGuard, n as createCustomAuthAdapter, nt as createAdapterAuthMiddleware, o as createSqlRateLimitStore, ot as fileTokenAuth, p as createFacebookProvider, pt as createFunctionApiKeyGuard, q as getMagicLinkTemplate, r as createApiKeyRoutes, rt as createAuthMiddleware, s as createSpotifyProvider, st as optionalAuth, tt as raw, u as createGitLabProvider, ut as requireAdmin, v as createGitHubProvider, vt as safeCompare, w as createJwksRoutes, wt as isPublicStoragePath, x as oauthCodeFlowSchema, xt as httpMethodToOperation, y as createLinkedinProvider, yt as SERVICE_IDENTITY, z as resolveEmailLinkBase } from "./auth-DJsLXsCR.js";
@@ -17057,7 +17057,7 @@ async function _initializeRebaseBackend(config) {
17057
17057
  let schemaEditorOff = schemaEditorUnavailable();
17058
17058
  let schemaEditorRoutes;
17059
17059
  if (!schemaEditorOff && config.collectionsDir) try {
17060
- schemaEditorRoutes = (await import("./schema-editor-routes-C3TLZqAC.js")).createSchemaEditorRoutes(config.collectionsDir);
17060
+ schemaEditorRoutes = (await import("./schema-editor-routes-BKOmdf4M.js")).createSchemaEditorRoutes(config.collectionsDir);
17061
17061
  } catch (err) {
17062
17062
  if (err?.code === "ERR_MODULE_NOT_FOUND") {
17063
17063
  schemaEditorOff = {
@@ -17124,7 +17124,7 @@ async function _initializeRebaseBackend(config) {
17124
17124
  },
17125
17125
  writeSource: schemaEditorRoutes ? async (change) => {
17126
17126
  const applyEdit = async (dir, collectionId, collection) => {
17127
- const { AstSchemaEditor } = await import("./ast-schema-editor-C6mDz0XN.js");
17127
+ const { AstSchemaEditor } = await import("./ast-schema-editor-CslO8Oje.js");
17128
17128
  await new AstSchemaEditor(dir).saveCollection(collectionId, collection, { partial: false });
17129
17129
  };
17130
17130
  if (remoteRepo) {
@@ -3,7 +3,7 @@ import __rebaseProcess from "process";
3
3
  globalThis.process ??= __rebaseProcess;
4
4
  __rebaseCreateRequire(import.meta.url);
5
5
  import { n as errorHandler, t as ApiError } from "./errors-DMImyqyR.js";
6
- import { AstSchemaEditor } from "./ast-schema-editor-C6mDz0XN.js";
6
+ import { AstSchemaEditor } from "./ast-schema-editor-CslO8Oje.js";
7
7
  import { Hono } from "hono";
8
8
  import { z } from "zod";
9
9
  //#region src/api/schema-editor-routes.ts
@@ -84,4 +84,4 @@ function createSchemaEditorRoutes(collectionsDir) {
84
84
  //#endregion
85
85
  export { createSchemaEditorRoutes };
86
86
 
87
- //# sourceMappingURL=schema-editor-routes-C3TLZqAC.js.map
87
+ //# sourceMappingURL=schema-editor-routes-BKOmdf4M.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema-editor-routes-C3TLZqAC.js","names":[],"sources":["../src/api/schema-editor-routes.ts"],"sourcesContent":["import { Hono } from \"hono\";\nimport { z } from \"zod\";\nimport { AstSchemaEditor } from \"./ast-schema-editor\";\nimport { ApiError, errorHandler } from \"./errors\";\nimport { HonoEnv } from \"./types\";\n\n/**\n * Rewriting collection source from the admin panel.\n *\n * Every refusal in `AstSchemaEditor` is written for the person who will read it\n * — \"Collection X has no file at …\", \"Relation Y has no target collection. Pick\n * one before saving.\" — and every one of them was a plain `Error`, which\n * `errorHandler` treats as an unexpected failure: 500, and a body that says\n * \"Internal Server Error\". The messages never left the server. They are 400s\n * now, so the panel can show what it was told.\n */\nfunction refusalsAsBadRequest<T>(run: () => Promise<T>): Promise<T> {\n return run().catch((error: unknown) => {\n if (error instanceof ApiError) throw error;\n throw ApiError.badRequest(\n error instanceof Error ? error.message : String(error),\n \"SCHEMA_EDIT_REFUSED\"\n );\n });\n}\n\n/** The identifier half of every payload here, checked once. */\nconst collectionIdSchema = z.string().min(1, \"`collectionId` is required\");\nconst propertyKeySchema = z.string().min(1, \"`propertyKey` is required\");\n\nconst propertySaveSchema = z.object({\n collectionId: collectionIdSchema,\n propertyKey: propertyKeySchema,\n propertyConfig: z.record(z.string(), z.unknown())\n});\nconst propertyDeleteSchema = z.object({\n collectionId: collectionIdSchema,\n propertyKey: propertyKeySchema\n});\nconst collectionSaveSchema = z.object({\n collectionId: collectionIdSchema,\n collectionData: z.record(z.string(), z.unknown()),\n partial: z.boolean().optional()\n});\nconst collectionDeleteSchema = z.object({\n collectionId: collectionIdSchema\n});\n\n/** Parse a body against a schema, or 400 naming the field. */\nasync function body<S extends z.ZodType>(c: { req: { json: () => Promise<unknown> } }, schema: S): Promise<z.infer<S>> {\n const raw = await c.req.json().catch(() => undefined);\n const parsed = schema.safeParse(raw);\n if (!parsed.success) {\n throw ApiError.badRequest(\n parsed.error.issues.map(i => `${i.path.join(\".\") || \"body\"}: ${i.message}`).join(\"; \"),\n \"INVALID_INPUT\"\n );\n }\n return parsed.data;\n}\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 { collectionId, propertyKey, propertyConfig } = await body(c, propertySaveSchema);\n await refusalsAsBadRequest(() => editor.saveProperty(collectionId, propertyKey, propertyConfig));\n return c.json({ success: true });\n });\n\n router.post(\"/property/delete\", async (c) => {\n const { collectionId, propertyKey } = await body(c, propertyDeleteSchema);\n await refusalsAsBadRequest(() => editor.deleteProperty(collectionId, propertyKey));\n return c.json({ success: true });\n });\n\n /**\n * `partial: true` means \"this payload is what changed\", not \"this is the\n * collection\". Without it a one-key patch — which is what adding a column\n * posts — is read as a whole-collection save and deletes everything it does\n * not mention, `securityRules` included. Absent, it defaults to a full save,\n * so an older panel keeps the behaviour it was written against.\n */\n router.post(\"/collection/save\", async (c) => {\n const { collectionId, collectionData, partial } = await body(c, collectionSaveSchema);\n await refusalsAsBadRequest(() =>\n editor.saveCollection(collectionId, collectionData, { partial: partial === true }));\n return c.json({ success: true });\n });\n\n router.post(\"/collection/delete\", async (c) => {\n const { collectionId } = await body(c, collectionDeleteSchema);\n await refusalsAsBadRequest(() => editor.deleteCollection(collectionId));\n return c.json({ success: true });\n });\n\n return router;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAgBA,SAAS,qBAAwB,KAAmC;CAChE,OAAO,IAAI,CAAC,CAAC,OAAO,UAAmB;EACnC,IAAI,iBAAiB,UAAU,MAAM;EACrC,MAAM,SAAS,WACX,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GACrD,qBACJ;CACJ,CAAC;AACL;;AAGA,IAAM,qBAAqB,EAAE,OAAO,CAAC,CAAC,IAAI,GAAG,4BAA4B;AACzE,IAAM,oBAAoB,EAAE,OAAO,CAAC,CAAC,IAAI,GAAG,2BAA2B;AAEvE,IAAM,qBAAqB,EAAE,OAAO;CAChC,cAAc;CACd,aAAa;CACb,gBAAgB,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC;AACpD,CAAC;AACD,IAAM,uBAAuB,EAAE,OAAO;CAClC,cAAc;CACd,aAAa;AACjB,CAAC;AACD,IAAM,uBAAuB,EAAE,OAAO;CAClC,cAAc;CACd,gBAAgB,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC;CAChD,SAAS,EAAE,QAAQ,CAAC,CAAC,SAAS;AAClC,CAAC;AACD,IAAM,yBAAyB,EAAE,OAAO,EACpC,cAAc,mBAClB,CAAC;;AAGD,eAAe,KAA0B,GAA8C,QAAgC;CACnH,MAAM,MAAM,MAAM,EAAE,IAAI,KAAK,CAAC,CAAC,YAAY,KAAA,CAAS;CACpD,MAAM,SAAS,OAAO,UAAU,GAAG;CACnC,IAAI,CAAC,OAAO,SACR,MAAM,SAAS,WACX,OAAO,MAAM,OAAO,KAAI,MAAK,GAAG,EAAE,KAAK,KAAK,GAAG,KAAK,OAAO,IAAI,EAAE,SAAS,CAAC,CAAC,KAAK,IAAI,GACrF,eACJ;CAEJ,OAAO,OAAO;AAClB;AAEA,SAAgB,yBAAyB,gBAAuC;CAC5E,MAAM,SAAS,IAAI,KAAc;CACjC,OAAO,QAAQ,YAAY;CAC3B,MAAM,SAAS,IAAI,gBAAgB,cAAc;CAEjD,OAAO,KAAK,kBAAkB,OAAO,MAAM;EACvC,MAAM,EAAE,cAAc,aAAa,mBAAmB,MAAM,KAAK,GAAG,kBAAkB;EACtF,MAAM,2BAA2B,OAAO,aAAa,cAAc,aAAa,cAAc,CAAC;EAC/F,OAAO,EAAE,KAAK,EAAE,SAAS,KAAK,CAAC;CACnC,CAAC;CAED,OAAO,KAAK,oBAAoB,OAAO,MAAM;EACzC,MAAM,EAAE,cAAc,gBAAgB,MAAM,KAAK,GAAG,oBAAoB;EACxE,MAAM,2BAA2B,OAAO,eAAe,cAAc,WAAW,CAAC;EACjF,OAAO,EAAE,KAAK,EAAE,SAAS,KAAK,CAAC;CACnC,CAAC;;;;;;;;CASD,OAAO,KAAK,oBAAoB,OAAO,MAAM;EACzC,MAAM,EAAE,cAAc,gBAAgB,YAAY,MAAM,KAAK,GAAG,oBAAoB;EACpF,MAAM,2BACF,OAAO,eAAe,cAAc,gBAAgB,EAAE,SAAS,YAAY,KAAK,CAAC,CAAC;EACtF,OAAO,EAAE,KAAK,EAAE,SAAS,KAAK,CAAC;CACnC,CAAC;CAED,OAAO,KAAK,sBAAsB,OAAO,MAAM;EAC3C,MAAM,EAAE,iBAAiB,MAAM,KAAK,GAAG,sBAAsB;EAC7D,MAAM,2BAA2B,OAAO,iBAAiB,YAAY,CAAC;EACtE,OAAO,EAAE,KAAK,EAAE,SAAS,KAAK,CAAC;CACnC,CAAC;CAED,OAAO;AACX"}
1
+ {"version":3,"file":"schema-editor-routes-BKOmdf4M.js","names":[],"sources":["../src/api/schema-editor-routes.ts"],"sourcesContent":["import { Hono } from \"hono\";\nimport { z } from \"zod\";\nimport { AstSchemaEditor } from \"./ast-schema-editor\";\nimport { ApiError, errorHandler } from \"./errors\";\nimport { HonoEnv } from \"./types\";\n\n/**\n * Rewriting collection source from the admin panel.\n *\n * Every refusal in `AstSchemaEditor` is written for the person who will read it\n * — \"Collection X has no file at …\", \"Relation Y has no target collection. Pick\n * one before saving.\" — and every one of them was a plain `Error`, which\n * `errorHandler` treats as an unexpected failure: 500, and a body that says\n * \"Internal Server Error\". The messages never left the server. They are 400s\n * now, so the panel can show what it was told.\n */\nfunction refusalsAsBadRequest<T>(run: () => Promise<T>): Promise<T> {\n return run().catch((error: unknown) => {\n if (error instanceof ApiError) throw error;\n throw ApiError.badRequest(\n error instanceof Error ? error.message : String(error),\n \"SCHEMA_EDIT_REFUSED\"\n );\n });\n}\n\n/** The identifier half of every payload here, checked once. */\nconst collectionIdSchema = z.string().min(1, \"`collectionId` is required\");\nconst propertyKeySchema = z.string().min(1, \"`propertyKey` is required\");\n\nconst propertySaveSchema = z.object({\n collectionId: collectionIdSchema,\n propertyKey: propertyKeySchema,\n propertyConfig: z.record(z.string(), z.unknown())\n});\nconst propertyDeleteSchema = z.object({\n collectionId: collectionIdSchema,\n propertyKey: propertyKeySchema\n});\nconst collectionSaveSchema = z.object({\n collectionId: collectionIdSchema,\n collectionData: z.record(z.string(), z.unknown()),\n partial: z.boolean().optional()\n});\nconst collectionDeleteSchema = z.object({\n collectionId: collectionIdSchema\n});\n\n/** Parse a body against a schema, or 400 naming the field. */\nasync function body<S extends z.ZodType>(c: { req: { json: () => Promise<unknown> } }, schema: S): Promise<z.infer<S>> {\n const raw = await c.req.json().catch(() => undefined);\n const parsed = schema.safeParse(raw);\n if (!parsed.success) {\n throw ApiError.badRequest(\n parsed.error.issues.map(i => `${i.path.join(\".\") || \"body\"}: ${i.message}`).join(\"; \"),\n \"INVALID_INPUT\"\n );\n }\n return parsed.data;\n}\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 { collectionId, propertyKey, propertyConfig } = await body(c, propertySaveSchema);\n await refusalsAsBadRequest(() => editor.saveProperty(collectionId, propertyKey, propertyConfig));\n return c.json({ success: true });\n });\n\n router.post(\"/property/delete\", async (c) => {\n const { collectionId, propertyKey } = await body(c, propertyDeleteSchema);\n await refusalsAsBadRequest(() => editor.deleteProperty(collectionId, propertyKey));\n return c.json({ success: true });\n });\n\n /**\n * `partial: true` means \"this payload is what changed\", not \"this is the\n * collection\". Without it a one-key patch — which is what adding a column\n * posts — is read as a whole-collection save and deletes everything it does\n * not mention, `securityRules` included. Absent, it defaults to a full save,\n * so an older panel keeps the behaviour it was written against.\n */\n router.post(\"/collection/save\", async (c) => {\n const { collectionId, collectionData, partial } = await body(c, collectionSaveSchema);\n await refusalsAsBadRequest(() =>\n editor.saveCollection(collectionId, collectionData, { partial: partial === true }));\n return c.json({ success: true });\n });\n\n router.post(\"/collection/delete\", async (c) => {\n const { collectionId } = await body(c, collectionDeleteSchema);\n await refusalsAsBadRequest(() => editor.deleteCollection(collectionId));\n return c.json({ success: true });\n });\n\n return router;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAgBA,SAAS,qBAAwB,KAAmC;CAChE,OAAO,IAAI,CAAC,CAAC,OAAO,UAAmB;EACnC,IAAI,iBAAiB,UAAU,MAAM;EACrC,MAAM,SAAS,WACX,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GACrD,qBACJ;CACJ,CAAC;AACL;;AAGA,IAAM,qBAAqB,EAAE,OAAO,CAAC,CAAC,IAAI,GAAG,4BAA4B;AACzE,IAAM,oBAAoB,EAAE,OAAO,CAAC,CAAC,IAAI,GAAG,2BAA2B;AAEvE,IAAM,qBAAqB,EAAE,OAAO;CAChC,cAAc;CACd,aAAa;CACb,gBAAgB,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC;AACpD,CAAC;AACD,IAAM,uBAAuB,EAAE,OAAO;CAClC,cAAc;CACd,aAAa;AACjB,CAAC;AACD,IAAM,uBAAuB,EAAE,OAAO;CAClC,cAAc;CACd,gBAAgB,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC;CAChD,SAAS,EAAE,QAAQ,CAAC,CAAC,SAAS;AAClC,CAAC;AACD,IAAM,yBAAyB,EAAE,OAAO,EACpC,cAAc,mBAClB,CAAC;;AAGD,eAAe,KAA0B,GAA8C,QAAgC;CACnH,MAAM,MAAM,MAAM,EAAE,IAAI,KAAK,CAAC,CAAC,YAAY,KAAA,CAAS;CACpD,MAAM,SAAS,OAAO,UAAU,GAAG;CACnC,IAAI,CAAC,OAAO,SACR,MAAM,SAAS,WACX,OAAO,MAAM,OAAO,KAAI,MAAK,GAAG,EAAE,KAAK,KAAK,GAAG,KAAK,OAAO,IAAI,EAAE,SAAS,CAAC,CAAC,KAAK,IAAI,GACrF,eACJ;CAEJ,OAAO,OAAO;AAClB;AAEA,SAAgB,yBAAyB,gBAAuC;CAC5E,MAAM,SAAS,IAAI,KAAc;CACjC,OAAO,QAAQ,YAAY;CAC3B,MAAM,SAAS,IAAI,gBAAgB,cAAc;CAEjD,OAAO,KAAK,kBAAkB,OAAO,MAAM;EACvC,MAAM,EAAE,cAAc,aAAa,mBAAmB,MAAM,KAAK,GAAG,kBAAkB;EACtF,MAAM,2BAA2B,OAAO,aAAa,cAAc,aAAa,cAAc,CAAC;EAC/F,OAAO,EAAE,KAAK,EAAE,SAAS,KAAK,CAAC;CACnC,CAAC;CAED,OAAO,KAAK,oBAAoB,OAAO,MAAM;EACzC,MAAM,EAAE,cAAc,gBAAgB,MAAM,KAAK,GAAG,oBAAoB;EACxE,MAAM,2BAA2B,OAAO,eAAe,cAAc,WAAW,CAAC;EACjF,OAAO,EAAE,KAAK,EAAE,SAAS,KAAK,CAAC;CACnC,CAAC;;;;;;;;CASD,OAAO,KAAK,oBAAoB,OAAO,MAAM;EACzC,MAAM,EAAE,cAAc,gBAAgB,YAAY,MAAM,KAAK,GAAG,oBAAoB;EACpF,MAAM,2BACF,OAAO,eAAe,cAAc,gBAAgB,EAAE,SAAS,YAAY,KAAK,CAAC,CAAC;EACtF,OAAO,EAAE,KAAK,EAAE,SAAS,KAAK,CAAC;CACnC,CAAC;CAED,OAAO,KAAK,sBAAsB,OAAO,MAAM;EAC3C,MAAM,EAAE,iBAAiB,MAAM,KAAK,GAAG,sBAAsB;EAC7D,MAAM,2BAA2B,OAAO,iBAAiB,YAAY,CAAC;EACtE,OAAO,EAAE,KAAK,EAAE,SAAS,KAAK,CAAC;CACnC,CAAC;CAED,OAAO;AACX"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/server",
3
- "version": "0.20.0",
3
+ "version": "0.20.1-canary.g4d882ca",
4
4
  "description": "Database-Agnostic Backend Core for Rebase",
5
5
  "keywords": [
6
6
  "rebase",
@@ -54,10 +54,10 @@
54
54
  "jsonwebtoken": "^9.0.3",
55
55
  "ws": "^8.21.1",
56
56
  "zod": "^4.4.3",
57
- "@rebasepro/client": "0.20.0",
58
- "@rebasepro/types": "0.20.0",
59
- "@rebasepro/common": "0.20.0",
60
- "@rebasepro/utils": "0.20.0"
57
+ "@rebasepro/client": "0.20.1-canary.g4d882ca",
58
+ "@rebasepro/common": "0.20.1-canary.g4d882ca",
59
+ "@rebasepro/types": "0.20.1-canary.g4d882ca",
60
+ "@rebasepro/utils": "0.20.1-canary.g4d882ca"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@jest/globals": "^30.4.1",
@@ -1 +0,0 @@
1
- {"version":3,"file":"ast-schema-editor-C6mDz0XN.js","names":[],"sources":["../src/api/ast-schema-editor.ts"],"sourcesContent":["import { Project, SyntaxKind, Node, ObjectLiteralExpression, ObjectLiteralElementLike, PropertyAssignment, SourceFile, IndentationText } from \"ts-morph\";\nimport { nestAdminCollectionKeys, nestAdminPropertyKeys } from \"@rebasepro/types\";\nimport * as path from \"path\";\nimport * as fs from \"fs\";\n\n/**\n * The helpers a collection file may be wrapped in.\n *\n * `rebase init` scaffolds every collection as\n * `const postsCollection = defineCollection({ … })` — a call expression, not the\n * bare object literal `rebase introspect` emits. An editor that only understood\n * the bare form found nothing to patch in any stock project, and then rewrote the\n * file from the panel's JSON: no wrapper, no imports, no relation thunks.\n */\nconst COLLECTION_FACTORIES = new Set([\"defineCollection\"]);\n\n/**\n * The only relation-target expression this editor will write through verbatim.\n *\n * A relation's `target` is emitted as SOURCE, not as a string literal, because\n * it has to be `() => otherCollection`. The test for \"is this already a thunk?\"\n * used to be \"does it contain an arrow\", which `() => { require(\"child_process\")\n * .execSync(\"…\") }` also satisfies — and `rebase dev` re-imports the file the\n * moment it changes, so the payload ran without anyone deploying anything.\n *\n * An arrow returning a single identifier is the whole grammar. Anything else is\n * read as a collection name and turned into a thunk by `targetThunk`, which\n * resolves it against the collections directory and refuses a name with no file.\n */\nconst ARROW_TO_IDENTIFIER = /^\\(\\s*\\)\\s*=>\\s*[A-Za-z_$][A-Za-z0-9_$]*$/;\n\n/**\n * A value that must be emitted as source code rather than as JSON.\n *\n * Everything reaching the writer has been through `JSON.stringify` on the wire,\n * so a function-valued key arrives either missing or as a string. A relation's\n * `target` is a thunk in the file and a slug in the payload; this is how the\n * thunk gets written back.\n */\nclass RawExpression {\n constructor(public readonly text: string) {\n }\n}\n\n/**\n * Move presentation keys into the `admin` block.\n *\n * The rule itself lives in `@rebasepro/types`, next to `ADMIN_COLLECTION_KEYS`,\n * because `@rebasepro/cms-types` has to apply the identical one on the panel's\n * side and this package cannot import that one. Two copies used to exist and\n * they disagreed about precedence, which decided whether a presentation edit was\n * saved or silently reverted to the value the user had just changed away from.\n */\nexport function nestAdminKeys(collectionData: Record<string, unknown>): Record<string, unknown> {\n return nestAdminCollectionKeys(collectionData);\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 /**\n * The variable name the generated file binds the collection to.\n *\n * `sanitizeCollectionId` guards the FILENAME, and permits hyphens and a\n * leading digit — both legal in a filename and neither legal in a\n * JavaScript identifier. So a collection created from the admin panel as\n * `my-notes` (the documented slug shape) or `2024 Archive` (auto-slugged to\n * `2024_archive`) wrote:\n *\n * const my-notesCollection: CollectionConfig = … \"',' expected\"\n * const 2024_archiveCollection: CollectionConfig = … \"Numeric separators\n * are not allowed here\"\n *\n * The panel reported success, and the next boot failed for EVERY collection\n * in the directory — the loader imports all of them — while the editor could\n * no longer parse the file it had just written, so it could not fix itself.\n *\n * Separators camel-case rather than vanish, so `my-notes` and `my_notes`\n * stay distinct; a leading digit is prefixed rather than stripped, so\n * `2024_archive` stays distinct from `archive`. Byte-identical for every\n * slug that already produced a valid identifier.\n */\n private static collectionVarName(safeId: string): string {\n const camel = safeId.replace(/[-_]+([a-zA-Z0-9])/g, (_, char: string) => char.toUpperCase());\n const identifier = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(camel) ? camel : `c${camel.replace(/^[0-9]/, (d) => d)}`;\n return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier) ? identifier : `c${identifier.replace(/[^A-Za-z0-9_$]/g, \"\")}`;\n }\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 /**\n * Find the object literal a collection is declared with, through whatever\n * wraps it.\n *\n * `defineCollection({ … })` is the shape `rebase init` writes and the one the\n * docs recommend; `satisfies` / `as` / parentheses are the other ways an\n * author can dress the same literal. Returning `null` for any of them meant\n * the three callers below each failed differently, and the worst of them\n * overwrote the file.\n */\n private unwrapCollectionObject(node: Node | undefined): ObjectLiteralExpression | null {\n if (!node) return null;\n if (Node.isObjectLiteralExpression(node)) return node;\n if (Node.isParenthesizedExpression(node) ||\n Node.isAsExpression(node) ||\n Node.isSatisfiesExpression(node) ||\n Node.isTypeAssertion(node) ||\n Node.isNonNullExpression(node)) {\n return this.unwrapCollectionObject(node.getExpression());\n }\n if (Node.isCallExpression(node)) {\n // `defineCollection`, `admin.defineCollection`, `defineCollection<Post>`\n const callee = node.getExpression().getText().split(\".\").pop();\n if (callee && COLLECTION_FACTORIES.has(callee)) {\n return this.unwrapCollectionObject(node.getArguments()[0]);\n }\n }\n return null;\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 const unwrapped = this.unwrapCollectionObject(varDecl?.getInitializer());\n if (unwrapped) return unwrapped;\n } else {\n // `export default defineCollection({ … })`\n const unwrapped = this.unwrapCollectionObject(expr);\n if (unwrapped) return unwrapped;\n }\n }\n }\n // Fallback: the first VariableDeclaration that holds a collection literal\n for (const varDecl of file.getVariableDeclarations()) {\n const init = this.unwrapCollectionObject(varDecl.getInitializer());\n if (init) return init;\n }\n return null;\n }\n\n /**\n * The collection's object literal, or a refusal that says what to do.\n *\n * Every caller needs this to be all-or-nothing: a missing object literal used\n * to mean \"throw\", \"report success and do nothing\" and \"recreate the file\n * from scratch\" depending on which method you called.\n */\n private requireCollectionObject(collectionId: string): ObjectLiteralExpression {\n const file = this.getCollectionFile(collectionId);\n if (!file) {\n throw new Error(`Collection \"${collectionId}\" has no file at ${path.join(this.collectionsDir, `${collectionId}.ts`)}.`);\n }\n const collectionObj = this.getCollectionObject(collectionId);\n if (!collectionObj) {\n throw new Error(this.unreadableFileMessage(collectionId, file));\n }\n return collectionObj;\n }\n\n private unreadableFileMessage(collectionId: string, file: SourceFile): string {\n return `Could not find the collection object in ${file.getFilePath()}. ` +\n \"The schema editor can only edit a collection declared as `const x = defineCollection({ … })` \" +\n \"or `const x: CollectionConfig = { … }` and exported as the file's default. \" +\n `Edit \"${collectionId}\" in code instead.`;\n }\n\n /** Look a key up on an object literal, quoted or not. */\n private findProperty(obj: ObjectLiteralExpression, name: string): ObjectLiteralElementLike | undefined {\n return obj.getProperty((p: ObjectLiteralElementLike) =>\n \"getName\" in p &&\n typeof (p as PropertyAssignment).getName === \"function\" &&\n ((p as PropertyAssignment).getName() === name ||\n (p as PropertyAssignment).getName() === `\"${name}\"` ||\n (p as PropertyAssignment).getName() === `'${name}'`));\n }\n\n private static quoteKey(key: string): string {\n return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);\n }\n\n private static isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value) && !(value instanceof RawExpression);\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 instanceof RawExpression) {\n return obj.text;\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 === \"browserCallbacks\" || name === \"permissions\" || name === \"securityRules\") {\n // Preserve this property exactly as it was\n preservedProps.push(`${AstSchemaEditor.quoteKey(name)}: ${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 = AstSchemaEditor.quoteKey(key);\n\n // If the value is an object, pass the old AST node to recurse\n let childAstNode: ObjectLiteralExpression | undefined;\n if (oldAstNode && AstSchemaEditor.isPlainObject(record[key])) {\n const oldProp = this.findProperty(oldAstNode, key);\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 /**\n * Write only the keys the patch names, leaving every sibling alone.\n *\n * A patch says what changed, not what the collection is. The panel sends one\n * — `{ propertiesOrder }` is what adding a column posts — and rewriting the\n * `admin` block from it deleted the collection's icon, group, list columns\n * and kanban config in the same write.\n */\n private mergeIntoObjectLiteral(target: ObjectLiteralExpression, data: Record<string, unknown>, indentLevel: number): void {\n for (const [key, value] of Object.entries(data)) {\n const existing = this.findProperty(target, key);\n const existingObj = existing && existing.isKind(SyntaxKind.PropertyAssignment)\n ? existing.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression)\n : undefined;\n\n if (existingObj && AstSchemaEditor.isPlainObject(value)) {\n this.mergeIntoObjectLiteral(existingObj, value, indentLevel + 1);\n continue;\n }\n\n const initializer = this.convertJsonToAstString(value, indentLevel, existingObj);\n if (existing && existing.isKind(SyntaxKind.PropertyAssignment)) {\n existing.setInitializer(initializer);\n } else {\n target.addPropertyAssignment({\n name: AstSchemaEditor.quoteKey(key),\n initializer\n });\n }\n }\n }\n\n public async saveProperty(collectionId: string, propertyKey: string, propertyConfig: Record<string, unknown>) {\n const collectionObj = this.requireCollectionObject(collectionId);\n\n // The panel's property forms bind to the flat names — `readOnly`,\n // `hideFromCollection` — while on disk they belong inside the property's\n // own `admin` block. Written flat they are not merely ignored: the boot\n // validator treats a moved key as fatal.\n const nestedConfig = nestAdminPropertyKeys(propertyConfig);\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 = this.findProperty(propsObj, propertyKey);\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(nestedConfig, 2, oldPropAstNode);\n\n if (existingProp) {\n if (existingProp.isKind(SyntaxKind.PropertyAssignment)) {\n existingProp.setInitializer(newInitializer);\n }\n } else {\n propsObj.addPropertyAssignment({\n name: AstSchemaEditor.quoteKey(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.requireCollectionObject(collectionId);\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 = this.findProperty(propsObj, propertyKey);\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 /**\n * Write a collection back to its file.\n *\n * `partial` is the difference between \"this is the collection\" and \"this is\n * what changed about it\". The panel sends both — a full save from the editor\n * dialog, and a one-key patch whenever a property is added, deleted or\n * reordered — and they cannot be told apart by looking at the payload. Read\n * as a full save, a patch deletes everything it does not mention, including\n * `securityRules`; the loader then hands the collection the directory\n * default, which in the scaffold is `access: \"public\"`.\n */\n public async saveCollection(collectionId: string, collectionData: Record<string, unknown>, options: { partial?: boolean } = {}) {\n const partial = options.partial === true;\n let file = this.getCollectionFile(collectionId);\n const collectionObj = file ? this.getCollectionObject(collectionId) : null;\n\n if (file && !collectionObj) {\n // The file is there and we could not read it. Recreating it from the\n // panel's JSON would drop the imports, the callbacks and the relation\n // thunks that JSON cannot carry.\n throw new Error(this.unreadableFileMessage(collectionId, file));\n }\n\n if (!file || !collectionObj) {\n if (partial) {\n throw new Error(`Cannot apply a partial update to \"${collectionId}\": it has no collection file yet.`);\n }\n // Create a new file\n const safeId = this.sanitizeCollectionId(collectionId);\n const newFilePath = this.safePath(`${safeId}.ts`);\n if (fs.existsSync(newFilePath)) {\n throw new Error(`Refusing to overwrite ${newFilePath}: a file for \"${collectionId}\" already exists but could not be parsed.`);\n }\n const varName = `${AstSchemaEditor.collectionVarName(safeId)}Collection`;\n file = this.project.createSourceFile(newFilePath, `import { CollectionConfig } from \"@rebasepro/types\";\\n\\nconst ${varName}: CollectionConfig = ${this.convertJsonToAstString(nestAdminKeys(collectionData))};\\n\\nexport default ${varName};\\n`);\n } else {\n // Update root level properties gracefully\n\n if (!partial) {\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\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\") {\n this.writeRelations(collectionId, file, collectionObj, collectionData[key]);\n continue;\n }\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 if (partial && oldAstNode && AstSchemaEditor.isPlainObject(collectionData[key])) {\n this.mergeIntoObjectLiteral(oldAstNode, collectionData[key] as Record<string, unknown>, 2);\n continue;\n }\n\n const newInit = this.convertJsonToAstString(collectionData[key], 1, oldAstNode);\n if (prop) {\n prop.setInitializer(newInit);\n } else {\n collectionObj.addPropertyAssignment({\n // `quoteKey`, like every other site that emits a key.\n // This one did not, and ts-morph writes a property name\n // out verbatim — so a top-level key of\n // `injected: (() => { … })(), tail` closed the property\n // and opened an expression, which `rebase dev` then\n // re-imported and ran. The rest of this file has always\n // quoted; this was the one that did not.\n name: AstSchemaEditor.quoteKey(key),\n initializer: newInit\n });\n }\n }\n }\n if (file) {\n file.formatText();\n }\n await this.project.save();\n }\n\n /**\n * Write the collection-level `relations` array.\n *\n * Relations are the one key whose values are not all data: `target` is a\n * thunk in the file, and the panel sends the target collection's slug (or,\n * for a relation it did not touch, nothing at all — `JSON.stringify` drops\n * the function). So each entry's target is resolved in that order: a slug\n * becomes `() => xCollection` plus the import it needs, and anything else\n * falls back to the thunk already in the file.\n *\n * This used to be a `continue` with a comment saying relations were handled\n * elsewhere. Nothing handled them; every edit in the Relations tab was\n * dropped without a word.\n */\n private writeRelations(collectionId: string, file: SourceFile, collectionObj: ObjectLiteralExpression, value: unknown): void {\n if (!Array.isArray(value)) return;\n\n const existing = this.findProperty(collectionObj, \"relations\");\n const oldArray = existing && existing.isKind(SyntaxKind.PropertyAssignment)\n ? existing.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression)\n : undefined;\n\n const oldElements = oldArray?.getElements() ?? [];\n const oldTargetsByName = new Map<string, string>();\n // Only for entries the file left unnamed — `relationName` is optional, and\n // those cannot be matched any other way. Positional matching is off the\n // moment the array's length changes, because then a position no longer\n // means the same relation.\n const anonymousTargetsByIndex: (string | undefined)[] = [];\n oldElements.forEach((element, index) => {\n const elementObj = element.asKind(SyntaxKind.ObjectLiteralExpression);\n if (!elementObj) return;\n const targetProp = this.findProperty(elementObj, \"target\");\n const targetInit = targetProp && targetProp.isKind(SyntaxKind.PropertyAssignment)\n ? targetProp.getInitializer()\n : undefined;\n if (!targetInit) return;\n\n const nameProp = this.findProperty(elementObj, \"relationName\");\n const nameInit = nameProp && nameProp.isKind(SyntaxKind.PropertyAssignment)\n ? nameProp.getInitializerIfKind(SyntaxKind.StringLiteral)\n : undefined;\n if (nameInit) oldTargetsByName.set(nameInit.getLiteralValue(), targetInit.getText());\n else anonymousTargetsByIndex[index] = targetInit.getText();\n });\n\n const items = value.map((entry, index) => {\n if (!AstSchemaEditor.isPlainObject(entry)) return entry;\n const relation: Record<string, unknown> = { ...entry };\n const relationName = typeof relation.relationName === \"string\" ? relation.relationName : undefined;\n const rawTarget = relation.target;\n\n let targetText: string | undefined;\n if (typeof rawTarget === \"string\" && rawTarget.trim().length > 0) {\n const trimmed = rawTarget.trim();\n // A target that already looks like a thunk is written into the\n // file as source, so \"contains an arrow\" is not a good enough\n // reason to trust it: `() => { require(\"child_process\")… }` also\n // contains one. Only the exact shape this emits is accepted —\n // an arrow returning one identifier — and anything else is\n // treated as a collection NAME and turned into a thunk here,\n // which is the path that was always safe.\n targetText = ARROW_TO_IDENTIFIER.test(trimmed)\n ? trimmed\n : this.targetThunk(file, trimmed);\n } else if (relationName && oldTargetsByName.has(relationName)) {\n targetText = oldTargetsByName.get(relationName);\n } else if (value.length === oldElements.length) {\n targetText = anonymousTargetsByIndex[index];\n }\n\n if (!targetText) {\n throw new Error(`Relation \"${relationName ?? `#${index}`}\" on collection \"${collectionId}\" has no target collection. Pick one before saving.`);\n }\n\n relation.target = new RawExpression(targetText);\n return relation;\n });\n\n const initializer = this.convertJsonToAstString(items, 1);\n if (existing && existing.isKind(SyntaxKind.PropertyAssignment)) {\n existing.setInitializer(initializer);\n } else {\n collectionObj.addPropertyAssignment({ name: \"relations\", initializer });\n }\n }\n\n /**\n * `() => targetCollection` for a target named by its slug, importing it if\n * the file does not already.\n */\n private targetThunk(file: SourceFile, targetSlug: string): string {\n const targetFile = this.getCollectionFile(targetSlug);\n if (!targetFile) {\n throw new Error(`Cannot link to collection \"${targetSlug}\": no file for it in ${this.collectionsDir}.`);\n }\n\n const identifier = this.getDefaultExportName(targetFile);\n if (!identifier) {\n throw new Error(`Cannot link to collection \"${targetSlug}\": ${targetFile.getFilePath()} has no default export to import.`);\n }\n\n if (targetFile.getFilePath() !== file.getFilePath() && !this.hasDefaultImport(file, identifier)) {\n const relative = path.relative(path.dirname(file.getFilePath()), targetFile.getFilePath())\n .split(path.sep)\n .join(\"/\")\n .replace(/\\.tsx?$/, \".js\");\n file.addImportDeclaration({\n defaultImport: identifier,\n moduleSpecifier: relative.startsWith(\".\") ? relative : `./${relative}`\n });\n }\n\n return `() => ${identifier}`;\n }\n\n private hasDefaultImport(file: SourceFile, identifier: string): boolean {\n return file.getImportDeclarations().some(decl => decl.getDefaultImport()?.getText() === identifier);\n }\n\n private getDefaultExportName(file: SourceFile): string | undefined {\n const declaration = file.getDefaultExportSymbol()?.getDeclarations()[0];\n const expr = declaration?.asKind(SyntaxKind.ExportAssignment)?.getExpression();\n if (expr && expr.getKind() === SyntaxKind.Identifier) return expr.getText();\n\n for (const varDecl of file.getVariableDeclarations()) {\n if (this.unwrapCollectionObject(varDecl.getInitializer())) return varDecl.getName();\n }\n return undefined;\n }\n\n public async deleteCollection(collectionId: string) {\n const file = this.getCollectionFile(collectionId);\n if (file) {\n file.deleteImmediatelySync();\n }\n }\n}\n"],"mappings":"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"}