@rebasepro/common 0.25.1 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/util/conditions.d.ts +4 -3
- package/dist/util/conditions.es.js +4 -3
- package/dist/util/conditions.es.js.map +1 -1
- package/dist/util/copy.d.ts +3 -2
- package/dist/util/copy.es.js +3 -2
- package/dist/util/copy.es.js.map +1 -1
- package/dist/util/resolve-relation.es.js +23 -1
- package/dist/util/resolve-relation.es.js.map +1 -1
- package/package.json +3 -3
|
@@ -8,10 +8,11 @@ export declare function registerConditionOperations(): void;
|
|
|
8
8
|
* Evaluate a condition against the given context.
|
|
9
9
|
*
|
|
10
10
|
* A condition may be stated as a literal instead of a rule — `hidden: true`
|
|
11
|
-
* rather than `hidden: { "==": [1, 1] }` — and a literal
|
|
12
|
-
* answer, so it is returned rather than handed to the
|
|
11
|
+
* rather than `hidden: { "==": [1, 1] }`, a bound as `min: 0` — and a literal
|
|
12
|
+
* is already its own answer, so it is returned rather than handed to the
|
|
13
|
+
* evaluator.
|
|
13
14
|
*/
|
|
14
|
-
export declare function evaluateCondition(rule: ConditionRule, context: ConditionContext): unknown;
|
|
15
|
+
export declare function evaluateCondition(rule: ConditionRule | number, context: ConditionContext): unknown;
|
|
15
16
|
/**
|
|
16
17
|
* Build a ConditionContext from the current property resolution context.
|
|
17
18
|
*/
|
|
@@ -41,11 +41,12 @@ function registerConditionOperations() {
|
|
|
41
41
|
* Evaluate a condition against the given context.
|
|
42
42
|
*
|
|
43
43
|
* A condition may be stated as a literal instead of a rule — `hidden: true`
|
|
44
|
-
* rather than `hidden: { "==": [1, 1] }` — and a literal
|
|
45
|
-
* answer, so it is returned rather than handed to the
|
|
44
|
+
* rather than `hidden: { "==": [1, 1] }`, a bound as `min: 0` — and a literal
|
|
45
|
+
* is already its own answer, so it is returned rather than handed to the
|
|
46
|
+
* evaluator.
|
|
46
47
|
*/
|
|
47
48
|
function evaluateCondition(rule, context) {
|
|
48
|
-
if (typeof rule === "boolean") return rule;
|
|
49
|
+
if (typeof rule === "boolean" || typeof rule === "number") return rule;
|
|
49
50
|
registerConditionOperations();
|
|
50
51
|
return jsonLogic.apply(rule, context);
|
|
51
52
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conditions.es.js","names":[],"sources":["../../src/util/conditions.ts"],"sourcesContent":["import jsonLogic from \"json-logic-js\";\nimport {\n ArrayProperty,\n AuthState,\n ConditionContext,\n ConditionRule,\n EnumValueConfig,\n NumberProperty,\n PropertyConditions,\n Property,\n ReferenceProperty,\n StringProperty\n} from \"@rebasepro/types\";\n\n/**\n * Access a nested property from an object via dot notation.\n */\nfunction getIn(obj: Record<string, unknown> | unknown, path: string): unknown {\n if (!obj || !path) return undefined;\n return path.split(\".\").reduce((acc: unknown, part: string) => acc && (acc as Record<string, unknown>)[part], obj);\n}\n\nlet operationsRegistered = false;\n\n/**\n * Register custom JSON Logic operations for Rebase.\n * Call this once at app initialization.\n */\nexport function registerConditionOperations(): void {\n if (operationsRegistered) return;\n\n // Check if user has a specific role by ID\n jsonLogic.add_operation(\"hasRole\", function (this: ConditionContext, roleId: string) {\n return this?.user?.roles?.includes(roleId) ?? false;\n });\n\n // Check if user has any of the specified roles\n jsonLogic.add_operation(\"hasAnyRole\", function (this: ConditionContext, roleIds: string[]) {\n if (!this?.user?.roles || !Array.isArray(roleIds)) return false;\n return roleIds.some(role => this.user.roles.includes(role));\n });\n\n // Check if a timestamp is today\n jsonLogic.add_operation(\"isToday\", (timestamp: number) => {\n if (!timestamp) return false;\n const date = new Date(timestamp);\n const today = new Date();\n return date.getFullYear() === today.getFullYear() &&\n date.getMonth() === today.getMonth() &&\n date.getDate() === today.getDate();\n });\n\n // Check if a timestamp is in the past\n jsonLogic.add_operation(\"isPast\", (timestamp: number) => {\n if (!timestamp) return false;\n return timestamp < Date.now();\n });\n\n // Check if a timestamp is in the future\n jsonLogic.add_operation(\"isFuture\", (timestamp: number) => {\n if (!timestamp) return false;\n return timestamp > Date.now();\n });\n\n operationsRegistered = true;\n}\n\n/**\n * Evaluate a condition against the given context.\n *\n * A condition may be stated as a literal instead of a rule — `hidden: true`\n * rather than `hidden: { \"==\": [1, 1] }` — and a literal is already its own
|
|
1
|
+
{"version":3,"file":"conditions.es.js","names":[],"sources":["../../src/util/conditions.ts"],"sourcesContent":["import jsonLogic from \"json-logic-js\";\nimport {\n ArrayProperty,\n AuthState,\n ConditionContext,\n ConditionRule,\n EnumValueConfig,\n NumberProperty,\n PropertyConditions,\n Property,\n ReferenceProperty,\n StringProperty\n} from \"@rebasepro/types\";\n\n/**\n * Access a nested property from an object via dot notation.\n */\nfunction getIn(obj: Record<string, unknown> | unknown, path: string): unknown {\n if (!obj || !path) return undefined;\n return path.split(\".\").reduce((acc: unknown, part: string) => acc && (acc as Record<string, unknown>)[part], obj);\n}\n\nlet operationsRegistered = false;\n\n/**\n * Register custom JSON Logic operations for Rebase.\n * Call this once at app initialization.\n */\nexport function registerConditionOperations(): void {\n if (operationsRegistered) return;\n\n // Check if user has a specific role by ID\n jsonLogic.add_operation(\"hasRole\", function (this: ConditionContext, roleId: string) {\n return this?.user?.roles?.includes(roleId) ?? false;\n });\n\n // Check if user has any of the specified roles\n jsonLogic.add_operation(\"hasAnyRole\", function (this: ConditionContext, roleIds: string[]) {\n if (!this?.user?.roles || !Array.isArray(roleIds)) return false;\n return roleIds.some(role => this.user.roles.includes(role));\n });\n\n // Check if a timestamp is today\n jsonLogic.add_operation(\"isToday\", (timestamp: number) => {\n if (!timestamp) return false;\n const date = new Date(timestamp);\n const today = new Date();\n return date.getFullYear() === today.getFullYear() &&\n date.getMonth() === today.getMonth() &&\n date.getDate() === today.getDate();\n });\n\n // Check if a timestamp is in the past\n jsonLogic.add_operation(\"isPast\", (timestamp: number) => {\n if (!timestamp) return false;\n return timestamp < Date.now();\n });\n\n // Check if a timestamp is in the future\n jsonLogic.add_operation(\"isFuture\", (timestamp: number) => {\n if (!timestamp) return false;\n return timestamp > Date.now();\n });\n\n operationsRegistered = true;\n}\n\n/**\n * Evaluate a condition against the given context.\n *\n * A condition may be stated as a literal instead of a rule — `hidden: true`\n * rather than `hidden: { \"==\": [1, 1] }`, a bound as `min: 0` — and a literal\n * is already its own answer, so it is returned rather than handed to the\n * evaluator.\n */\nexport function evaluateCondition(rule: ConditionRule | number, context: ConditionContext): unknown {\n if (typeof rule === \"boolean\" || typeof rule === \"number\") return rule;\n // Ensure operations are registered\n registerConditionOperations();\n return jsonLogic.apply(rule, context);\n}\n\n/**\n * Convert a value to a format suitable for JSON Logic evaluation.\n * Specifically handles Date objects by converting them to Unix timestamps.\n */\nfunction serializeValueForConditions(value: unknown): unknown {\n if (value === null || value === undefined) {\n return value;\n }\n\n // Handle Date objects\n if (value instanceof Date) {\n return value.getTime();\n }\n\n // Handle Firestore Timestamp-like objects (have toDate or toMillis)\n if (typeof (value as { toMillis?: () => number })?.toMillis === \"function\") {\n return (value as { toMillis: () => number }).toMillis();\n }\n if (typeof (value as { toDate?: () => Date })?.toDate === \"function\") {\n return (value as { toDate: () => Date }).toDate().getTime();\n }\n\n // Handle arrays recursively\n if (Array.isArray(value)) {\n return value.map(serializeValueForConditions);\n }\n\n // Handle plain objects recursively\n if (typeof value === \"object\") {\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(value as Record<string, unknown>)) {\n result[key] = serializeValueForConditions((value as Record<string, unknown>)[key]);\n }\n return result;\n }\n\n return value;\n}\n\n/**\n * Build a ConditionContext from the current property resolution context.\n */\nexport function buildConditionContext(params: {\n propertyKey?: string;\n values?: Record<string, unknown>;\n previousValues?: Record<string, unknown>;\n path: string;\n entityId?: string;\n index?: number;\n authController: AuthState;\n}): ConditionContext {\n const {\n propertyKey,\n values,\n previousValues,\n path,\n entityId,\n index,\n authController\n } = params;\n\n const user = authController.user;\n const serializedValues = serializeValueForConditions(values ?? {});\n const serializedPreviousValues = serializeValueForConditions(previousValues ?? values ?? {});\n\n return {\n values: serializedValues as Record<string, unknown>,\n previousValues: serializedPreviousValues as Record<string, unknown>,\n propertyValue: propertyKey ? getIn(serializedValues, propertyKey) : undefined,\n path,\n entityId,\n isNew: !entityId,\n index,\n user: {\n uid: user?.uid ?? \"\",\n email: user?.email ?? null,\n displayName: user?.displayName ?? null,\n photoURL: user?.photoURL ?? null,\n roles: (user?.roles ?? []).map((r: unknown) => typeof r === \"string\" ? r : (r as { id: string }).id)\n },\n now: Date.now()\n };\n}\n\n/**\n * Apply PropertyConditions to a resolved property, evaluating all JSON Logic rules.\n */\n"],"mappings":";;;;;AAiBA,SAAS,MAAM,KAAwC,MAAuB;CAC1E,IAAI,CAAC,OAAO,CAAC,MAAM,OAAO,KAAA;CAC1B,OAAO,KAAK,MAAM,GAAG,CAAC,CAAC,QAAQ,KAAc,SAAiB,OAAQ,IAAgC,OAAO,GAAG;AACpH;AAEA,IAAI,uBAAuB;;;;;AAM3B,SAAgB,8BAAoC;CAChD,IAAI,sBAAsB;CAG1B,UAAU,cAAc,WAAW,SAAkC,QAAgB;EACjF,OAAO,MAAM,MAAM,OAAO,SAAS,MAAM,KAAK;CAClD,CAAC;CAGD,UAAU,cAAc,cAAc,SAAkC,SAAmB;EACvF,IAAI,CAAC,MAAM,MAAM,SAAS,CAAC,MAAM,QAAQ,OAAO,GAAG,OAAO;EAC1D,OAAO,QAAQ,MAAK,SAAQ,KAAK,KAAK,MAAM,SAAS,IAAI,CAAC;CAC9D,CAAC;CAGD,UAAU,cAAc,YAAY,cAAsB;EACtD,IAAI,CAAC,WAAW,OAAO;EACvB,MAAM,OAAO,IAAI,KAAK,SAAS;EAC/B,MAAM,wBAAQ,IAAI,KAAK;EACvB,OAAO,KAAK,YAAY,MAAM,MAAM,YAAY,KAC5C,KAAK,SAAS,MAAM,MAAM,SAAS,KACnC,KAAK,QAAQ,MAAM,MAAM,QAAQ;CACzC,CAAC;CAGD,UAAU,cAAc,WAAW,cAAsB;EACrD,IAAI,CAAC,WAAW,OAAO;EACvB,OAAO,YAAY,KAAK,IAAI;CAChC,CAAC;CAGD,UAAU,cAAc,aAAa,cAAsB;EACvD,IAAI,CAAC,WAAW,OAAO;EACvB,OAAO,YAAY,KAAK,IAAI;CAChC,CAAC;CAED,uBAAuB;AAC3B;;;;;;;;;AAUA,SAAgB,kBAAkB,MAA8B,SAAoC;CAChG,IAAI,OAAO,SAAS,aAAa,OAAO,SAAS,UAAU,OAAO;CAElE,4BAA4B;CAC5B,OAAO,UAAU,MAAM,MAAM,OAAO;AACxC;;;;;AAMA,SAAS,4BAA4B,OAAyB;CAC1D,IAAI,UAAU,QAAQ,UAAU,KAAA,GAC5B,OAAO;CAIX,IAAI,iBAAiB,MACjB,OAAO,MAAM,QAAQ;CAIzB,IAAI,OAAQ,OAAuC,aAAa,YAC5D,OAAQ,MAAqC,SAAS;CAE1D,IAAI,OAAQ,OAAmC,WAAW,YACtD,OAAQ,MAAiC,OAAO,CAAC,CAAC,QAAQ;CAI9D,IAAI,MAAM,QAAQ,KAAK,GACnB,OAAO,MAAM,IAAI,2BAA2B;CAIhD,IAAI,OAAO,UAAU,UAAU;EAC3B,MAAM,SAAkC,CAAC;EACzC,KAAK,MAAM,OAAO,OAAO,KAAK,KAAgC,GAC1D,OAAO,OAAO,4BAA6B,MAAkC,IAAI;EAErF,OAAO;CACX;CAEA,OAAO;AACX;;;;AAKA,SAAgB,sBAAsB,QAQjB;CACjB,MAAM,EACF,aACA,QACA,gBACA,MACA,UACA,OACA,mBACA;CAEJ,MAAM,OAAO,eAAe;CAC5B,MAAM,mBAAmB,4BAA4B,UAAU,CAAC,CAAC;CAGjE,OAAO;EACH,QAAQ;EACR,gBAJ6B,4BAA4B,kBAAkB,UAAU,CAAC,CAItE;EAChB,eAAe,cAAc,MAAM,kBAAkB,WAAW,IAAI,KAAA;EACpE;EACA;EACA,OAAO,CAAC;EACR;EACA,MAAM;GACF,KAAK,MAAM,OAAO;GAClB,OAAO,MAAM,SAAS;GACtB,aAAa,MAAM,eAAe;GAClC,UAAU,MAAM,YAAY;GAC5B,QAAQ,MAAM,SAAS,CAAC,EAAA,CAAG,KAAK,MAAe,OAAO,MAAM,WAAW,IAAK,EAAqB,EAAE;EACvG;EACA,KAAK,KAAK,IAAI;CAClB;AACJ"}
|
package/dist/util/copy.d.ts
CHANGED
|
@@ -9,8 +9,9 @@ import type { CollectionConfig, ResolvedRelation } from "@rebasepro/types";
|
|
|
9
9
|
* lives on the other row. Writing a `hasMany` or a `hasOne` points the
|
|
10
10
|
* children's foreign key at the copy, which takes them away from the record
|
|
11
11
|
* that was copied: duplicating an author moved every one of their posts to the
|
|
12
|
-
* duplicate and left the original with none. A `via` is
|
|
13
|
-
*
|
|
12
|
+
* duplicate and left the original with none. A `via` is left out too: a
|
|
13
|
+
* single-hop one's one-to-one writer re-points the target's key the same way,
|
|
14
|
+
* and a multi-hop one is read-only.
|
|
14
15
|
*
|
|
15
16
|
* Exhaustive over the kinds, so a new one is a compile error here rather than
|
|
16
17
|
* a copy that quietly carries it.
|
package/dist/util/copy.es.js
CHANGED
|
@@ -11,8 +11,9 @@ import { resolveRelationProperty } from "./resolutions.es.js";
|
|
|
11
11
|
* lives on the other row. Writing a `hasMany` or a `hasOne` points the
|
|
12
12
|
* children's foreign key at the copy, which takes them away from the record
|
|
13
13
|
* that was copied: duplicating an author moved every one of their posts to the
|
|
14
|
-
* duplicate and left the original with none. A `via` is
|
|
15
|
-
*
|
|
14
|
+
* duplicate and left the original with none. A `via` is left out too: a
|
|
15
|
+
* single-hop one's one-to-one writer re-points the target's key the same way,
|
|
16
|
+
* and a multi-hop one is read-only.
|
|
16
17
|
*
|
|
17
18
|
* Exhaustive over the kinds, so a new one is a compile error here rather than
|
|
18
19
|
* a copy that quietly carries it.
|
package/dist/util/copy.es.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"copy.es.js","names":[],"sources":["../../src/util/copy.ts"],"sourcesContent":["import type { CollectionConfig, Property, RelationProperty, ResolvedRelation } from \"@rebasepro/types\";\nimport { resolveCollectionRelations } from \"./relations\";\nimport { resolveRelationProperty } from \"./resolutions\";\n\n/**\n * Whether a duplicate of a record may carry this relation's value.\n *\n * A duplicate is saved as a create, and a create writes every relation it\n * carries. That is right where the link is the copy's own: a `belongsTo` key on\n * the copy's row, or `manyToMany` links, which are new junction rows beside the\n * original's — the targets are shared by design. It is wrong wherever the key\n * lives on the other row. Writing a `hasMany` or a `hasOne` points the\n * children's foreign key at the copy, which takes them away from the record\n * that was copied: duplicating an author moved every one of their posts to the\n * duplicate and left the original with none. A `via` is
|
|
1
|
+
{"version":3,"file":"copy.es.js","names":[],"sources":["../../src/util/copy.ts"],"sourcesContent":["import type { CollectionConfig, Property, RelationProperty, ResolvedRelation } from \"@rebasepro/types\";\nimport { resolveCollectionRelations } from \"./relations\";\nimport { resolveRelationProperty } from \"./resolutions\";\n\n/**\n * Whether a duplicate of a record may carry this relation's value.\n *\n * A duplicate is saved as a create, and a create writes every relation it\n * carries. That is right where the link is the copy's own: a `belongsTo` key on\n * the copy's row, or `manyToMany` links, which are new junction rows beside the\n * original's — the targets are shared by design. It is wrong wherever the key\n * lives on the other row. Writing a `hasMany` or a `hasOne` points the\n * children's foreign key at the copy, which takes them away from the record\n * that was copied: duplicating an author moved every one of their posts to the\n * duplicate and left the original with none. A `via` is left out too: a\n * single-hop one's one-to-one writer re-points the target's key the same way,\n * and a multi-hop one is read-only.\n *\n * Exhaustive over the kinds, so a new one is a compile error here rather than\n * a copy that quietly carries it.\n */\nexport function copyCarriesRelation(relation: ResolvedRelation): boolean {\n switch (relation.kind) {\n case \"belongsTo\":\n case \"manyToMany\":\n return true;\n case \"hasOne\":\n case \"hasMany\":\n case \"via\":\n return false;\n default: {\n const exhaustive: never = relation;\n throw new Error(`Unknown relation kind: ${JSON.stringify(exhaustive)}`);\n }\n }\n}\n\n/**\n * The values a duplicate of a record is created with: the original's, less\n * its key and less every relation the copy cannot take without changing\n * another row (see {@link copyCarriesRelation}).\n *\n * The key goes so the database, or the user, gives the copy its own. The\n * relations go because the copy is written as a create, and a create writes\n * them — for a child-held key that is a re-parent of rows the user never\n * touched, with nothing on screen to say so.\n *\n * A relation property that does not resolve is left out as well: what writing\n * it would do cannot be known here, and leaving a value out of a new record\n * changes nothing that exists.\n */\nexport function getCopyValues<M extends Record<string, unknown>>(\n collection: CollectionConfig,\n values: Partial<M>\n): Partial<M> {\n const result: Record<string, unknown> = { ...values };\n\n for (const [key, raw] of Object.entries(collection.properties ?? {})) {\n const property = raw as Property | undefined;\n if (!property) continue;\n if (\"isId\" in property && property.isId) {\n delete result[key];\n continue;\n }\n if (property.type !== \"relation\") continue;\n let relation: ResolvedRelation | undefined;\n try {\n relation = resolveRelationProperty(property as RelationProperty, collection, key);\n } catch {\n relation = undefined;\n }\n if (!relation || !copyCarriesRelation(relation)) delete result[key];\n }\n\n // A relation declared in `relations`, with no property, can still come back\n // on the row under its name. One whose target does not resolve cannot be\n // written by the server either, so the copy's save reports it; failing\n // here would only stop the form from opening.\n let declared: Record<string, ResolvedRelation> = {};\n try {\n declared = resolveCollectionRelations(collection);\n } catch {\n declared = {};\n }\n for (const [name, relation] of Object.entries(declared)) {\n if (!copyCarriesRelation(relation)) delete result[name];\n }\n\n return result as Partial<M>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,oBAAoB,UAAqC;CACrE,QAAQ,SAAS,MAAjB;EACI,KAAK;EACL,KAAK,cACD,OAAO;EACX,KAAK;EACL,KAAK;EACL,KAAK,OACD,OAAO;EACX,SAEI,MAAM,IAAI,MAAM,0BAA0B,KAAK,UAAU,QAAU,GAAG;CAE9E;AACJ;;;;;;;;;;;;;;;AAgBA,SAAgB,cACZ,YACA,QACU;CACV,MAAM,SAAkC,EAAE,GAAG,OAAO;CAEpD,KAAK,MAAM,CAAC,KAAK,QAAQ,OAAO,QAAQ,WAAW,cAAc,CAAC,CAAC,GAAG;EAClE,MAAM,WAAW;EACjB,IAAI,CAAC,UAAU;EACf,IAAI,UAAU,YAAY,SAAS,MAAM;GACrC,OAAO,OAAO;GACd;EACJ;EACA,IAAI,SAAS,SAAS,YAAY;EAClC,IAAI;EACJ,IAAI;GACA,WAAW,wBAAwB,UAA8B,YAAY,GAAG;EACpF,QAAQ;GACJ,WAAW,KAAA;EACf;EACA,IAAI,CAAC,YAAY,CAAC,oBAAoB,QAAQ,GAAG,OAAO,OAAO;CACnE;CAMA,IAAI,WAA6C,CAAC;CAClD,IAAI;EACA,WAAW,2BAA2B,UAAU;CACpD,QAAQ;EACJ,WAAW,CAAC;CAChB;CACA,KAAK,MAAM,CAAC,MAAM,aAAa,OAAO,QAAQ,QAAQ,GAClD,IAAI,CAAC,oBAAoB,QAAQ,GAAG,OAAO,OAAO;CAGtD,OAAO;AACX"}
|
|
@@ -81,13 +81,35 @@ function resolveRelation(relation, sourceCollection, propertyKey) {
|
|
|
81
81
|
...shared,
|
|
82
82
|
kind: "via",
|
|
83
83
|
cardinality: relation.cardinality,
|
|
84
|
-
writable:
|
|
84
|
+
writable: isSingleHopVia(relation.cardinality, relation.joinPath),
|
|
85
85
|
shared: true,
|
|
86
86
|
joinPath: relation.joinPath
|
|
87
87
|
};
|
|
88
88
|
default: throw new Error(`Unknown relation kind: ${JSON.stringify(relation)}`);
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Whether a `via` chain has a single link for a write to set — the chains
|
|
93
|
+
* Rebase writes through a create or update:
|
|
94
|
+
*
|
|
95
|
+
* - **to-one, one step** (`{ table: target, on: { from, to } }`): the target
|
|
96
|
+
* row holds the key, and the write points it at this row, as a `hasOne`
|
|
97
|
+
* write does;
|
|
98
|
+
* - **to-many, two steps through one table between the ends**: that table
|
|
99
|
+
* holds the links, and the write sets them as junction rows, as a
|
|
100
|
+
* `manyToMany` write does.
|
|
101
|
+
*
|
|
102
|
+
* Both shipped writable (0.24.0 fixed the one-to-one writer). A longer chain
|
|
103
|
+
* has no single link to maintain: a to-many one found no junction connecting
|
|
104
|
+
* both ends and failed as a 500, and a to-one one re-pointed the target at
|
|
105
|
+
* whatever column the first step read. So those are read-only, and every door
|
|
106
|
+
* reads this answer through `writable` rather than re-deriving it.
|
|
107
|
+
*/
|
|
108
|
+
function isSingleHopVia(cardinality, joinPath) {
|
|
109
|
+
if (!Array.isArray(joinPath)) return false;
|
|
110
|
+
if (cardinality === "one") return joinPath.length === 1;
|
|
111
|
+
return joinPath.length === 2 && joinPath[0].table !== joinPath[1].table;
|
|
112
|
+
}
|
|
91
113
|
/** How this relation is addressed in an error message, before it has a resolved name. */
|
|
92
114
|
function describe(relation, sourceCollection, propertyKey) {
|
|
93
115
|
const name = relation.relationName ?? propertyKey;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-relation.es.js","names":[],"sources":["../../src/util/resolve-relation.ts"],"sourcesContent":["import {\n CollectionConfig,\n Relation,\n ResolvedRelation\n} from \"@rebasepro/types\";\nimport { generateForeignKeyName, toSnakeCase } from \"@rebasepro/utils\";\n\nimport { getTableName } from \"./relations\";\n\n/**\n * Fill in a relation's defaults.\n *\n * This replaces `sanitizeRelation`, which had to work out *which kind of link\n * you meant* from whichever optional fields happened to be set — 194 lines of\n * it, including a pass that inspected the target collection's own relations to\n * decide whether a `many`/`inverse` pair was a one-to-many or the far side of a\n * many-to-many, wrapped in a `try/catch` that fell through to the wrong answer\n * when it could not tell. Two consumers running that logic at different moments\n * could reach different conclusions about the same relation.\n *\n * With the kind declared there is nothing to work out. What remains is\n * defaulting — a table name, a column name — which is deterministic, depends\n * only on the relation and its two endpoints, and cannot fail. That is why this\n * function returns rather than throws, and why it needs no cache to be\n * consistent.\n */\nexport function resolveRelation(\n relation: Relation,\n sourceCollection: CollectionConfig,\n propertyKey?: string\n): ResolvedRelation {\n const target = relation.target;\n if (typeof target !== \"function\") {\n throw new Error(\n `Relation${relation.relationName ? ` '${relation.relationName}'` : \"\"} on ` +\n `'${sourceCollection.slug}' has no \\`target\\`. Give it a thunk: \\`target: () => otherCollection\\`.`\n );\n }\n\n const targetCollection = callTarget(relation, sourceCollection, propertyKey, target);\n\n // The name is the address: the `include` key, the admin tab, and the\n // segment of a nested path. Declared name wins, then the declaring\n // property's key, then the target's slug.\n const relationName = relation.relationName ?? propertyKey ?? toSnakeCase(targetCollection.slug);\n\n const shared: Pick<ResolvedRelation, \"relationName\" | \"target\" | \"targetSlug\" | \"onUpdate\" | \"onDelete\" | \"overrides\"> = {\n relationName,\n // Normalised, not the thunk as written. Resolution reads the target once\n // and every later consumer calls it again — the driver building a join,\n // the DDL and policy generators, the admin's relation fields — so\n // handing back the raw thunk would give all of them the module namespace\n // `callTarget` just looked past, and the fix would hold only for the\n // fields resolution happens to read here. Still lazy: same call at the\n // same moment, one unwrap on the way out.\n target: () => unwrapModuleNamespace(target()) as CollectionConfig,\n targetSlug: targetCollection.slug,\n onUpdate: relation.onUpdate,\n onDelete: relation.onDelete,\n overrides: relation.overrides\n // No `validation`. Whether the link is required is a fact about the\n // *property*, and copying it onto the resolved relation gave the\n // question two answers that were free to disagree. Ask\n // `isRelationRequired(collection, relation)`, which reads the one.\n };\n\n const sourceName = toSnakeCase(sourceCollection.slug ?? sourceCollection.name);\n\n switch (relation.kind) {\n case \"belongsTo\":\n return {\n ...shared,\n kind: \"belongsTo\",\n cardinality: \"one\",\n writable: true,\n shared: false,\n localKey: relation.localKey ?? generateForeignKeyName(relationName),\n index: relation.index !== false\n };\n\n case \"hasOne\":\n return {\n ...shared,\n kind: \"hasOne\",\n cardinality: \"one\",\n writable: true,\n shared: false,\n foreignKeyOnTarget: relation.foreignKeyOnTarget ?? generateForeignKeyName(sourceName),\n sourceKey: relation.sourceKey\n };\n\n case \"hasMany\":\n return {\n ...shared,\n kind: \"hasMany\",\n cardinality: \"many\",\n writable: true,\n shared: false,\n foreignKeyOnTarget: relation.foreignKeyOnTarget ?? generateForeignKeyName(sourceName),\n // Not defaulted: the source's primary key needs the driver's\n // schema to resolve, which resolution does not have. `undefined`\n // means \"the primary key\" — see `ResolvedHasMany.sourceKey`.\n sourceKey: relation.sourceKey\n };\n\n case \"manyToMany\": {\n const sourceTable = getTableName(sourceCollection);\n const targetTable = getTableName(targetCollection);\n return {\n ...shared,\n kind: \"manyToMany\",\n cardinality: \"many\",\n writable: true,\n shared: true,\n through: {\n // Sorted so both sides of the same link derive the same\n // table without having to agree in advance.\n table: relation.through?.table ?? [sourceTable, targetTable].sort().join(\"_\"),\n sourceColumn: relation.through?.sourceColumn ?? generateForeignKeyName(sourceName),\n targetColumn: relation.through?.targetColumn ?? generateForeignKeyName(relationName),\n // `{}` rather than `undefined`, for the reason every other\n // field here is filled in: a consumer reads one shape and\n // does not have to decide what an absent payload means.\n properties: relation.through?.properties ?? {}\n }\n };\n }\n\n case \"via\":\n return {\n ...shared,\n kind: \"via\",\n cardinality: relation.cardinality,\n writable: false,\n // A join chain reaches rows that other parents reach too, and\n // Rebase does not know which hop, if any, is a link it owns.\n shared: true,\n joinPath: relation.joinPath\n };\n\n default: {\n // Exhaustive: a new kind is a compile error here, not a silent\n // fall-through to whatever shape happened to match first.\n const exhaustive: never = relation;\n throw new Error(`Unknown relation kind: ${JSON.stringify(exhaustive)}`);\n }\n }\n}\n\n/** How this relation is addressed in an error message, before it has a resolved name. */\nfunction describe(relation: Relation, sourceCollection: CollectionConfig, propertyKey?: string): string {\n const name = relation.relationName ?? propertyKey;\n return `Relation${name ? ` '${name}'` : \"\"} on '${sourceCollection.slug}'`;\n}\n\n/**\n * A module namespace, unwrapped to the collection it exports.\n *\n * A cycle transpiled to CommonJS does not hand the importing module the\n * *default export* — it hands it the module object, `{ __esModule: true,\n * default: … }`, captured before the exporting module finished evaluating. The\n * `default` slot fills in later, so by the time a lazy `target` thunk runs the\n * collection is sitting right there, one level down. Returning the namespace is\n * never a thing a thunk means to do, and there is exactly one reading of it.\n *\n * Only unwrapped when the inner value is itself a collection: a `default` that\n * is not one is a genuinely wrong thunk, and it should reach the error below\n * rather than be quietly swapped in.\n */\nfunction unwrapModuleNamespace(value: unknown): unknown {\n if (!value || typeof value !== \"object\") return value;\n if ((value as { slug?: unknown }).slug) return value;\n const inner = (value as { default?: unknown }).default;\n return inner && typeof inner === \"object\" && (inner as { slug?: unknown }).slug ? inner : value;\n}\n\n/**\n * Call the `target` thunk, and translate the ways an import cycle breaks it into\n * an error that names the cause — or, where the value is recoverable, into the\n * collection the thunk meant.\n *\n * The thunk exists to defer the reference until every module has finished\n * evaluating, and for a cycle that closes at import time it does. Two cycles\n * leave the binding permanently unusable:\n *\n * - **ESM/TDZ.** `const` and `class` bindings in a not-yet-evaluated module are\n * in the temporal dead zone, so reading one throws `ReferenceError: x is not\n * defined`. The stack points at the thunk — a one-line arrow function that is\n * obviously fine — and says nothing about the cycle that made it throw.\n * - **CJS interop, unresolved.** The half-initialised module object has no\n * `default` yet, the import resolves to `undefined`, and the thunk returns it\n * without complaint. That one used to surface here as \"did not resolve to a\n * collection\", which is true and unhelpful.\n *\n * Both mean the same thing, and the fix for both is the same: break the cycle,\n * or move the relation into the collection that does not close it.\n *\n * A third shape is *not* an error, and used to be reported as one. A loader that\n * transpiles ESM to CJS — jiti, which is what `rebase generate-sdk` and\n * `rebase build` load collections with — gives the module entered second in a\n * cycle a namespace object rather than the default export, and never replaces it\n * with a live binding. The thunk then returns `{ __esModule: true, default: … }`\n * holding the fully-initialised collection. Native ESM resolves the same thunk\n * to the collection directly, so this was a loader artefact reported as an\n * authoring mistake, and the advice it gave — make the target a lazy thunk — was\n * already satisfied by the code it was rejecting. Bidirectional relations make\n * these cycles unavoidable, and the lazy thunk is this framework's own answer to\n * them, so {@link unwrapModuleNamespace} takes the collection and moves on.\n */\nfunction callTarget(\n relation: Relation,\n sourceCollection: CollectionConfig,\n propertyKey: string | undefined,\n target: Relation[\"target\"]\n): ReturnType<Relation[\"target\"]> {\n let targetCollection: ReturnType<Relation[\"target\"]> | undefined;\n try {\n targetCollection = unwrapModuleNamespace(target()) as ReturnType<Relation[\"target\"]>;\n } catch (error) {\n // A ReferenceError from inside the thunk is a binding that was never\n // initialised — nothing else in a one-expression arrow can raise one.\n if (error instanceof ReferenceError) {\n throw new Error(\n `${describe(relation, sourceCollection, propertyKey)} targets a collection that is not ` +\n `initialized yet — almost always an import cycle between the two collection files. ` +\n `Break the cycle (move the shared piece into a third module, or import the target ` +\n `lazily) so the target's module finishes evaluating before the registry is built.`,\n { cause: error }\n );\n }\n throw error;\n }\n\n if (!targetCollection?.slug) {\n throw new Error(\n `${describe(relation, sourceCollection, propertyKey)} has a \\`target\\` that resolved to ` +\n `${targetCollection === undefined ? \"`undefined`\" : \"something that is not a collection\"}. ` +\n (targetCollection === undefined\n ? \"Under CommonJS interop an import cycle resolves the default import to `undefined`, \" +\n \"so check whether this collection and its target import each other. Otherwise the thunk \" +\n \"is returning the wrong value — it must return the collection itself, not a promise or a module.\"\n : typeof (targetCollection as { then?: unknown }).then === \"function\"\n ? \"The thunk returned a promise — `target: () => import(\\\"./other\\\")` is asynchronous. \" +\n \"Import the collection at the top of the file and return the binding: \" +\n \"`target: () => otherCollection`.\"\n : \"The thunk must return a collection config with a `slug`.\")\n );\n }\n\n return targetCollection;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"resolve-relation.es.js","names":[],"sources":["../../src/util/resolve-relation.ts"],"sourcesContent":["import {\n CollectionConfig,\n Relation,\n ResolvedRelation\n} from \"@rebasepro/types\";\nimport { generateForeignKeyName, toSnakeCase } from \"@rebasepro/utils\";\n\nimport { getTableName } from \"./relations\";\n\n/**\n * Fill in a relation's defaults.\n *\n * This replaces `sanitizeRelation`, which had to work out *which kind of link\n * you meant* from whichever optional fields happened to be set — 194 lines of\n * it, including a pass that inspected the target collection's own relations to\n * decide whether a `many`/`inverse` pair was a one-to-many or the far side of a\n * many-to-many, wrapped in a `try/catch` that fell through to the wrong answer\n * when it could not tell. Two consumers running that logic at different moments\n * could reach different conclusions about the same relation.\n *\n * With the kind declared there is nothing to work out. What remains is\n * defaulting — a table name, a column name — which is deterministic, depends\n * only on the relation and its two endpoints, and cannot fail. That is why this\n * function returns rather than throws, and why it needs no cache to be\n * consistent.\n */\nexport function resolveRelation(\n relation: Relation,\n sourceCollection: CollectionConfig,\n propertyKey?: string\n): ResolvedRelation {\n const target = relation.target;\n if (typeof target !== \"function\") {\n throw new Error(\n `Relation${relation.relationName ? ` '${relation.relationName}'` : \"\"} on ` +\n `'${sourceCollection.slug}' has no \\`target\\`. Give it a thunk: \\`target: () => otherCollection\\`.`\n );\n }\n\n const targetCollection = callTarget(relation, sourceCollection, propertyKey, target);\n\n // The name is the address: the `include` key, the admin tab, and the\n // segment of a nested path. Declared name wins, then the declaring\n // property's key, then the target's slug.\n const relationName = relation.relationName ?? propertyKey ?? toSnakeCase(targetCollection.slug);\n\n const shared: Pick<ResolvedRelation, \"relationName\" | \"target\" | \"targetSlug\" | \"onUpdate\" | \"onDelete\" | \"overrides\"> = {\n relationName,\n // Normalised, not the thunk as written. Resolution reads the target once\n // and every later consumer calls it again — the driver building a join,\n // the DDL and policy generators, the admin's relation fields — so\n // handing back the raw thunk would give all of them the module namespace\n // `callTarget` just looked past, and the fix would hold only for the\n // fields resolution happens to read here. Still lazy: same call at the\n // same moment, one unwrap on the way out.\n target: () => unwrapModuleNamespace(target()) as CollectionConfig,\n targetSlug: targetCollection.slug,\n onUpdate: relation.onUpdate,\n onDelete: relation.onDelete,\n overrides: relation.overrides\n // No `validation`. Whether the link is required is a fact about the\n // *property*, and copying it onto the resolved relation gave the\n // question two answers that were free to disagree. Ask\n // `isRelationRequired(collection, relation)`, which reads the one.\n };\n\n const sourceName = toSnakeCase(sourceCollection.slug ?? sourceCollection.name);\n\n switch (relation.kind) {\n case \"belongsTo\":\n return {\n ...shared,\n kind: \"belongsTo\",\n cardinality: \"one\",\n writable: true,\n shared: false,\n localKey: relation.localKey ?? generateForeignKeyName(relationName),\n index: relation.index !== false\n };\n\n case \"hasOne\":\n return {\n ...shared,\n kind: \"hasOne\",\n cardinality: \"one\",\n writable: true,\n shared: false,\n foreignKeyOnTarget: relation.foreignKeyOnTarget ?? generateForeignKeyName(sourceName),\n sourceKey: relation.sourceKey\n };\n\n case \"hasMany\":\n return {\n ...shared,\n kind: \"hasMany\",\n cardinality: \"many\",\n writable: true,\n shared: false,\n foreignKeyOnTarget: relation.foreignKeyOnTarget ?? generateForeignKeyName(sourceName),\n // Not defaulted: the source's primary key needs the driver's\n // schema to resolve, which resolution does not have. `undefined`\n // means \"the primary key\" — see `ResolvedHasMany.sourceKey`.\n sourceKey: relation.sourceKey\n };\n\n case \"manyToMany\": {\n const sourceTable = getTableName(sourceCollection);\n const targetTable = getTableName(targetCollection);\n return {\n ...shared,\n kind: \"manyToMany\",\n cardinality: \"many\",\n writable: true,\n shared: true,\n through: {\n // Sorted so both sides of the same link derive the same\n // table without having to agree in advance.\n table: relation.through?.table ?? [sourceTable, targetTable].sort().join(\"_\"),\n sourceColumn: relation.through?.sourceColumn ?? generateForeignKeyName(sourceName),\n targetColumn: relation.through?.targetColumn ?? generateForeignKeyName(relationName),\n // `{}` rather than `undefined`, for the reason every other\n // field here is filled in: a consumer reads one shape and\n // does not have to decide what an absent payload means.\n properties: relation.through?.properties ?? {}\n }\n };\n }\n\n case \"via\":\n return {\n ...shared,\n kind: \"via\",\n cardinality: relation.cardinality,\n writable: isSingleHopVia(relation.cardinality, relation.joinPath),\n // A join chain reaches rows that other parents reach too, and\n // Rebase does not know which hop, if any, is a link it owns.\n shared: true,\n joinPath: relation.joinPath\n };\n\n default: {\n // Exhaustive: a new kind is a compile error here, not a silent\n // fall-through to whatever shape happened to match first.\n const exhaustive: never = relation;\n throw new Error(`Unknown relation kind: ${JSON.stringify(exhaustive)}`);\n }\n }\n}\n\n/**\n * Whether a `via` chain has a single link for a write to set — the chains\n * Rebase writes through a create or update:\n *\n * - **to-one, one step** (`{ table: target, on: { from, to } }`): the target\n * row holds the key, and the write points it at this row, as a `hasOne`\n * write does;\n * - **to-many, two steps through one table between the ends**: that table\n * holds the links, and the write sets them as junction rows, as a\n * `manyToMany` write does.\n *\n * Both shipped writable (0.24.0 fixed the one-to-one writer). A longer chain\n * has no single link to maintain: a to-many one found no junction connecting\n * both ends and failed as a 500, and a to-one one re-pointed the target at\n * whatever column the first step read. So those are read-only, and every door\n * reads this answer through `writable` rather than re-deriving it.\n */\nfunction isSingleHopVia(cardinality: \"one\" | \"many\", joinPath: readonly { table: string }[] | undefined): boolean {\n // A chain declared without its steps resolves — resolution cannot fail;\n // the relation validator names the missing `joinPath` — and is not written.\n if (!Array.isArray(joinPath)) return false;\n if (cardinality === \"one\") return joinPath.length === 1;\n return joinPath.length === 2 && joinPath[0].table !== joinPath[1].table;\n}\n\n/** How this relation is addressed in an error message, before it has a resolved name. */\nfunction describe(relation: Relation, sourceCollection: CollectionConfig, propertyKey?: string): string {\n const name = relation.relationName ?? propertyKey;\n return `Relation${name ? ` '${name}'` : \"\"} on '${sourceCollection.slug}'`;\n}\n\n/**\n * A module namespace, unwrapped to the collection it exports.\n *\n * A cycle transpiled to CommonJS does not hand the importing module the\n * *default export* — it hands it the module object, `{ __esModule: true,\n * default: … }`, captured before the exporting module finished evaluating. The\n * `default` slot fills in later, so by the time a lazy `target` thunk runs the\n * collection is sitting right there, one level down. Returning the namespace is\n * never a thing a thunk means to do, and there is exactly one reading of it.\n *\n * Only unwrapped when the inner value is itself a collection: a `default` that\n * is not one is a genuinely wrong thunk, and it should reach the error below\n * rather than be quietly swapped in.\n */\nfunction unwrapModuleNamespace(value: unknown): unknown {\n if (!value || typeof value !== \"object\") return value;\n if ((value as { slug?: unknown }).slug) return value;\n const inner = (value as { default?: unknown }).default;\n return inner && typeof inner === \"object\" && (inner as { slug?: unknown }).slug ? inner : value;\n}\n\n/**\n * Call the `target` thunk, and translate the ways an import cycle breaks it into\n * an error that names the cause — or, where the value is recoverable, into the\n * collection the thunk meant.\n *\n * The thunk exists to defer the reference until every module has finished\n * evaluating, and for a cycle that closes at import time it does. Two cycles\n * leave the binding permanently unusable:\n *\n * - **ESM/TDZ.** `const` and `class` bindings in a not-yet-evaluated module are\n * in the temporal dead zone, so reading one throws `ReferenceError: x is not\n * defined`. The stack points at the thunk — a one-line arrow function that is\n * obviously fine — and says nothing about the cycle that made it throw.\n * - **CJS interop, unresolved.** The half-initialised module object has no\n * `default` yet, the import resolves to `undefined`, and the thunk returns it\n * without complaint. That one used to surface here as \"did not resolve to a\n * collection\", which is true and unhelpful.\n *\n * Both mean the same thing, and the fix for both is the same: break the cycle,\n * or move the relation into the collection that does not close it.\n *\n * A third shape is *not* an error, and used to be reported as one. A loader that\n * transpiles ESM to CJS — jiti, which is what `rebase generate-sdk` and\n * `rebase build` load collections with — gives the module entered second in a\n * cycle a namespace object rather than the default export, and never replaces it\n * with a live binding. The thunk then returns `{ __esModule: true, default: … }`\n * holding the fully-initialised collection. Native ESM resolves the same thunk\n * to the collection directly, so this was a loader artefact reported as an\n * authoring mistake, and the advice it gave — make the target a lazy thunk — was\n * already satisfied by the code it was rejecting. Bidirectional relations make\n * these cycles unavoidable, and the lazy thunk is this framework's own answer to\n * them, so {@link unwrapModuleNamespace} takes the collection and moves on.\n */\nfunction callTarget(\n relation: Relation,\n sourceCollection: CollectionConfig,\n propertyKey: string | undefined,\n target: Relation[\"target\"]\n): ReturnType<Relation[\"target\"]> {\n let targetCollection: ReturnType<Relation[\"target\"]> | undefined;\n try {\n targetCollection = unwrapModuleNamespace(target()) as ReturnType<Relation[\"target\"]>;\n } catch (error) {\n // A ReferenceError from inside the thunk is a binding that was never\n // initialised — nothing else in a one-expression arrow can raise one.\n if (error instanceof ReferenceError) {\n throw new Error(\n `${describe(relation, sourceCollection, propertyKey)} targets a collection that is not ` +\n `initialized yet — almost always an import cycle between the two collection files. ` +\n `Break the cycle (move the shared piece into a third module, or import the target ` +\n `lazily) so the target's module finishes evaluating before the registry is built.`,\n { cause: error }\n );\n }\n throw error;\n }\n\n if (!targetCollection?.slug) {\n throw new Error(\n `${describe(relation, sourceCollection, propertyKey)} has a \\`target\\` that resolved to ` +\n `${targetCollection === undefined ? \"`undefined`\" : \"something that is not a collection\"}. ` +\n (targetCollection === undefined\n ? \"Under CommonJS interop an import cycle resolves the default import to `undefined`, \" +\n \"so check whether this collection and its target import each other. Otherwise the thunk \" +\n \"is returning the wrong value — it must return the collection itself, not a promise or a module.\"\n : typeof (targetCollection as { then?: unknown }).then === \"function\"\n ? \"The thunk returned a promise — `target: () => import(\\\"./other\\\")` is asynchronous. \" +\n \"Import the collection at the top of the file and return the binding: \" +\n \"`target: () => otherCollection`.\"\n : \"The thunk must return a collection config with a `slug`.\")\n );\n }\n\n return targetCollection;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,gBACZ,UACA,kBACA,aACgB;CAChB,MAAM,SAAS,SAAS;CACxB,IAAI,OAAO,WAAW,YAClB,MAAM,IAAI,MACN,WAAW,SAAS,eAAe,KAAK,SAAS,aAAa,KAAK,GAAG,OAClE,iBAAiB,KAAK,yEAC9B;CAGJ,MAAM,mBAAmB,WAAW,UAAU,kBAAkB,aAAa,MAAM;CAKnF,MAAM,eAAe,SAAS,gBAAgB,eAAe,YAAY,iBAAiB,IAAI;CAE9F,MAAM,SAAmH;EACrH;EAQA,cAAc,sBAAsB,OAAO,CAAC;EAC5C,YAAY,iBAAiB;EAC7B,UAAU,SAAS;EACnB,UAAU,SAAS;EACnB,WAAW,SAAS;CAKxB;CAEA,MAAM,aAAa,YAAY,iBAAiB,QAAQ,iBAAiB,IAAI;CAE7E,QAAQ,SAAS,MAAjB;EACI,KAAK,aACD,OAAO;GACH,GAAG;GACH,MAAM;GACN,aAAa;GACb,UAAU;GACV,QAAQ;GACR,UAAU,SAAS,YAAY,uBAAuB,YAAY;GAClE,OAAO,SAAS,UAAU;EAC9B;EAEJ,KAAK,UACD,OAAO;GACH,GAAG;GACH,MAAM;GACN,aAAa;GACb,UAAU;GACV,QAAQ;GACR,oBAAoB,SAAS,sBAAsB,uBAAuB,UAAU;GACpF,WAAW,SAAS;EACxB;EAEJ,KAAK,WACD,OAAO;GACH,GAAG;GACH,MAAM;GACN,aAAa;GACb,UAAU;GACV,QAAQ;GACR,oBAAoB,SAAS,sBAAsB,uBAAuB,UAAU;GAIpF,WAAW,SAAS;EACxB;EAEJ,KAAK,cAAc;GACf,MAAM,cAAc,aAAa,gBAAgB;GACjD,MAAM,cAAc,aAAa,gBAAgB;GACjD,OAAO;IACH,GAAG;IACH,MAAM;IACN,aAAa;IACb,UAAU;IACV,QAAQ;IACR,SAAS;KAGL,OAAO,SAAS,SAAS,SAAS,CAAC,aAAa,WAAW,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG;KAC5E,cAAc,SAAS,SAAS,gBAAgB,uBAAuB,UAAU;KACjF,cAAc,SAAS,SAAS,gBAAgB,uBAAuB,YAAY;KAInF,YAAY,SAAS,SAAS,cAAc,CAAC;IACjD;GACJ;EACJ;EAEA,KAAK,OACD,OAAO;GACH,GAAG;GACH,MAAM;GACN,aAAa,SAAS;GACtB,UAAU,eAAe,SAAS,aAAa,SAAS,QAAQ;GAGhE,QAAQ;GACR,UAAU,SAAS;EACvB;EAEJ,SAII,MAAM,IAAI,MAAM,0BAA0B,KAAK,UAAU,QAAU,GAAG;CAE9E;AACJ;;;;;;;;;;;;;;;;;;AAmBA,SAAS,eAAe,aAA6B,UAA6D;CAG9G,IAAI,CAAC,MAAM,QAAQ,QAAQ,GAAG,OAAO;CACrC,IAAI,gBAAgB,OAAO,OAAO,SAAS,WAAW;CACtD,OAAO,SAAS,WAAW,KAAK,SAAS,EAAE,CAAC,UAAU,SAAS,EAAE,CAAC;AACtE;;AAGA,SAAS,SAAS,UAAoB,kBAAoC,aAA8B;CACpG,MAAM,OAAO,SAAS,gBAAgB;CACtC,OAAO,WAAW,OAAO,KAAK,KAAK,KAAK,GAAG,OAAO,iBAAiB,KAAK;AAC5E;;;;;;;;;;;;;;;AAgBA,SAAS,sBAAsB,OAAyB;CACpD,IAAI,CAAC,SAAS,OAAO,UAAU,UAAU,OAAO;CAChD,IAAK,MAA6B,MAAM,OAAO;CAC/C,MAAM,QAAS,MAAgC;CAC/C,OAAO,SAAS,OAAO,UAAU,YAAa,MAA6B,OAAO,QAAQ;AAC9F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAS,WACL,UACA,kBACA,aACA,QAC8B;CAC9B,IAAI;CACJ,IAAI;EACA,mBAAmB,sBAAsB,OAAO,CAAC;CACrD,SAAS,OAAO;EAGZ,IAAI,iBAAiB,gBACjB,MAAM,IAAI,MACN,GAAG,SAAS,UAAU,kBAAkB,WAAW,EAAE,wRAIrD,EAAE,OAAO,MAAM,CACnB;EAEJ,MAAM;CACV;CAEA,IAAI,CAAC,kBAAkB,MACnB,MAAM,IAAI,MACN,GAAG,SAAS,UAAU,kBAAkB,WAAW,EAAE,qCAClD,qBAAqB,KAAA,IAAY,gBAAgB,qCAAqC,OACxF,qBAAqB,KAAA,IAChB,8QAGA,OAAQ,iBAAwC,SAAS,aACrD,8LAGA,2DACd;CAGJ,OAAO;AACX"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/common",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Rebase shared core — collection registry, data driver adapter and fluent query builder. No React dependency.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rebase",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"fast-equals": "6.0.2",
|
|
48
48
|
"json-logic-js": "^2.0.5",
|
|
49
|
-
"@rebasepro/
|
|
50
|
-
"@rebasepro/
|
|
49
|
+
"@rebasepro/types": "0.27.0",
|
|
50
|
+
"@rebasepro/utils": "0.27.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@jest/globals": "^30.4.1",
|