@rebasepro/common 0.17.3 → 0.18.1

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.
Files changed (58) hide show
  1. package/README.md +4 -0
  2. package/dist/collections/CollectionRegistry.d.ts +1 -1
  3. package/dist/collections/default-collections.d.ts +15 -84
  4. package/dist/data/buildRebaseData.d.ts +1 -1
  5. package/dist/data/filter-dialect.d.ts +11 -0
  6. package/dist/data/sort-dialect.d.ts +15 -3
  7. package/dist/index.es.js +375 -63
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/util/builders.d.ts +69 -24
  10. package/dist/util/callback-errors.d.ts +77 -0
  11. package/dist/util/callback-errors.test.d.ts +1 -0
  12. package/dist/util/index.d.ts +1 -0
  13. package/dist/util/policy/evaluatePolicy.d.ts +6 -0
  14. package/dist/util/relations.d.ts +41 -0
  15. package/dist/util/table-name.test.d.ts +1 -0
  16. package/package.json +26 -22
  17. package/src/collections/CollectionRegistry.ts +0 -485
  18. package/src/collections/default-collections.ts +0 -109
  19. package/src/collections/index.ts +0 -2
  20. package/src/data/buildRebaseData.ts +0 -816
  21. package/src/data/buildRoutedRebaseData.ts +0 -103
  22. package/src/data/filter-conditions.ts +0 -46
  23. package/src/data/filter-dialect.ts +0 -737
  24. package/src/data/paginate.ts +0 -334
  25. package/src/data/query_builder.ts +0 -176
  26. package/src/data/resolveDataSource.ts +0 -135
  27. package/src/data/sort-dialect.ts +0 -237
  28. package/src/index.ts +0 -11
  29. package/src/table-classification.ts +0 -109
  30. package/src/types/json-logic-js.d.ts +0 -8
  31. package/src/util/auth-default-policies.ts +0 -215
  32. package/src/util/builders.ts +0 -82
  33. package/src/util/callbacks.ts +0 -122
  34. package/src/util/collections.ts +0 -117
  35. package/src/util/common.ts +0 -2
  36. package/src/util/conditions.ts +0 -168
  37. package/src/util/email.ts +0 -32
  38. package/src/util/entities.ts +0 -282
  39. package/src/util/enums.ts +0 -26
  40. package/src/util/identity.ts +0 -202
  41. package/src/util/index.ts +0 -21
  42. package/src/util/internal-tables.test.ts +0 -188
  43. package/src/util/internal-tables.ts +0 -197
  44. package/src/util/junction-policies.ts +0 -355
  45. package/src/util/paths.ts +0 -27
  46. package/src/util/permissions.test.ts +0 -866
  47. package/src/util/permissions.ts +0 -206
  48. package/src/util/pg-column-to-property.ts +0 -377
  49. package/src/util/policy/evaluatePolicy.ts +0 -194
  50. package/src/util/policy/index.ts +0 -4
  51. package/src/util/policy/policyToPostgres.ts +0 -263
  52. package/src/util/policy/securityRuleToConditions.ts +0 -67
  53. package/src/util/policy/sqlToPolicy.ts +0 -422
  54. package/src/util/relations.ts +0 -236
  55. package/src/util/resolutions.ts +0 -534
  56. package/src/util/resolve-relation.ts +0 -243
  57. package/src/util/storage.ts +0 -177
  58. package/src/util/string-column-length.ts +0 -31
@@ -1,534 +0,0 @@
1
- import {
2
- ArrayProperty,
3
- AuthState,
4
- CollectionConfig,
5
- EnumValueConfig,
6
- EnumValues,
7
- NumberProperty,
8
- Properties,
9
- Property,
10
- RelationProperty,
11
- ResolvedRelation,
12
- StringProperty,
13
- getDataSourceCapabilities,
14
- getDeclaredSubcollections,
15
- type EntityChildView
16
- } from "@rebasepro/types";
17
-
18
- type PropertyConfig = { property: unknown; [key: string]: unknown };
19
- import { isPropertyBuilder } from "./entities";
20
- import { enumToObjectEntries } from "./enums";
21
- import { DEFAULT_ONE_OF_TYPE } from "./common";
22
- import { isDefaultFieldConfigId } from "@rebasepro/utils";
23
- import { getIn, mergeDeep } from "@rebasepro/utils";
24
- import { isJunctionBackedRelation, resolveCollectionRelations } from "./relations";
25
- import { resolveRelation } from "./resolve-relation";
26
-
27
- /**
28
- * Resolve property builders, enums and arrays.
29
- */
30
-
31
- export type ResolvePropertyProps<M extends Record<string, unknown> = Record<string, unknown>> = {
32
- property: Property
33
- propertyKey?: string,
34
- values?: Partial<M>,
35
- previousValues?: Partial<M>,
36
- path?: string,
37
- entityId?: string | number,
38
- index?: number,
39
- propertyConfigs?: Record<string, PropertyConfig>;
40
- ignoreMissingFields?: boolean;
41
- authController: AuthState;
42
- }
43
-
44
- export function resolveProperty<M extends Record<string, unknown> = Record<string, unknown>>(props: ResolvePropertyProps<M>): Property | null {
45
-
46
- const {
47
- property,
48
- ignoreMissingFields = false,
49
- ...rest
50
- } = props;
51
-
52
- let resultProperty: Property;
53
-
54
- if (isPropertyBuilder(property)) {
55
- const path = rest.path;
56
- if (!path) {
57
- // When path is not available (e.g. in preview contexts), skip dynamic
58
- // resolution and use the property as-is without dynamic modifications.
59
- resultProperty = property as Property;
60
- } else {
61
- const usedPropertyValue = rest.propertyKey ? getIn(rest.values, rest.propertyKey) : undefined;
62
- const dynamicProps = property.dynamicProps?.({
63
- ...rest,
64
- path,
65
- propertyValue: usedPropertyValue,
66
- values: rest.values ?? {},
67
- previousValues: rest.previousValues ?? rest.values ?? {}
68
- });
69
- resultProperty = mergeDeep(property, dynamicProps ?? {});
70
- }
71
- } else {
72
- resultProperty = property as Property;
73
- }
74
-
75
- // Apply dynamic properties if they exist
76
- if (resultProperty?.dynamicProps && rest.path) {
77
- const path = rest.path;
78
- const usedPropertyValue = rest.propertyKey ? getIn(rest.values, rest.propertyKey) : undefined;
79
- const dynamicPropsResult = resultProperty.dynamicProps({
80
- ...rest,
81
- path,
82
- propertyValue: usedPropertyValue,
83
- values: rest.values ?? {},
84
- previousValues: rest.previousValues ?? rest.values ?? {}
85
- });
86
-
87
- if (dynamicPropsResult) {
88
- resultProperty = mergeDeep(resultProperty, dynamicPropsResult);
89
- }
90
- }
91
-
92
- let resolvedProperty: Property | null;
93
-
94
- if (resultProperty?.type === "map" && resultProperty.properties) {
95
- const properties = resolveProperties({
96
- ignoreMissingFields,
97
- ...rest,
98
- properties: resultProperty.properties
99
- });
100
- resolvedProperty = {
101
- ...resultProperty,
102
- properties
103
- } as Property;
104
- } else if (resultProperty?.type === "array") {
105
- resolvedProperty = resultProperty;
106
- } else if ((resultProperty?.type === "string" || resultProperty?.type === "number") && resultProperty.enum) {
107
- resolvedProperty = resolvePropertyEnum(resultProperty);
108
- } else {
109
- resolvedProperty = resultProperty;
110
- }
111
-
112
- if (resolvedProperty?.propertyConfig && !isDefaultFieldConfigId(resolvedProperty.propertyConfig)) {
113
- const cmsFields = rest.propertyConfigs;
114
- if (!cmsFields && !ignoreMissingFields) {
115
- throw Error(`Trying to resolve a property with key '${resolvedProperty.propertyConfig}' that inherits from a custom property config but no custom property configs were provided. Use the property 'propertyConfigs' in your app config to provide them`);
116
- }
117
- const customField: PropertyConfig | undefined = cmsFields?.[resolvedProperty.propertyConfig];
118
- if (!customField) {
119
- console.warn(`Trying to resolve a property with key '${resolvedProperty.propertyConfig}' that inherits from a custom property config but no custom property config with that key was found. Check the 'propertyConfigs' in your app config`)
120
- return resolvedProperty;
121
- }
122
- if (customField.property) {
123
- const restConfigProperty = { ...customField.property } as Record<string, unknown>;
124
- delete restConfigProperty.propertyConfig;
125
- const customFieldProperty = resolveProperty({
126
- property: { name: "",
127
- ...restConfigProperty } as Property,
128
- ignoreMissingFields,
129
- ...rest
130
- });
131
- if (customFieldProperty) {
132
- resolvedProperty = mergeDeep(customFieldProperty, resolvedProperty);
133
- }
134
- }
135
-
136
- }
137
-
138
- return resolvedProperty;
139
- }
140
-
141
- /**
142
- * The resolved relation a relation property refers to.
143
- *
144
- * Normalization stamps `resolvedRelation` onto the property, so this is usually
145
- * a field read. It falls back to resolving from the collection for properties
146
- * that never went through the registry — a preview, or a form rendered straight
147
- * from an authored config.
148
- */
149
- export function resolveRelationProperty(
150
- property: RelationProperty,
151
- collection: CollectionConfig,
152
- propertyKey?: string
153
- ): ResolvedRelation {
154
- if (property.resolvedRelation) return property.resolvedRelation;
155
-
156
- if (property.relation) {
157
- return resolveRelation(property.relation, collection, propertyKey);
158
- }
159
-
160
- const name = propertyKey ?? "";
161
- const declared = resolveCollectionRelations(collection)[name];
162
- if (!declared) {
163
- throw Error(
164
- `Relation property '${name || "(unnamed)"}' on '${collection.slug}' declares no \`relation\`, ` +
165
- "and the collection has no relation of that name."
166
- );
167
- }
168
- return declared;
169
- }
170
-
171
- /**
172
- * Resolve enum aliases for a string or number property
173
- * @param property
174
- */
175
- export function resolvePropertyEnum(property: StringProperty | NumberProperty): StringProperty | NumberProperty {
176
- if (typeof property.enum === "object") {
177
- return {
178
- ...property,
179
- enum: enumToObjectEntries(property.enum)?.filter((value) => value && (value.id || value.id === 0) && value.label) ?? []
180
- };
181
- }
182
- return property as StringProperty | NumberProperty;
183
- }
184
-
185
- /**
186
- * Resolve enums and arrays for properties
187
- * @param properties
188
- * @param value
189
- */
190
- export function resolveProperties<M extends Record<string, unknown>>({
191
- propertyKey,
192
- properties,
193
- ignoreMissingFields,
194
- ...props
195
- }: {
196
- propertyKey?: string,
197
- properties: Properties,
198
- values?: Partial<M>,
199
- previousValues?: Partial<M>,
200
- path?: string,
201
- entityId?: string | number,
202
- index?: number,
203
- propertyConfigs?: Record<string, PropertyConfig>;
204
- ignoreMissingFields?: boolean;
205
- authController: AuthState;
206
- }): Properties {
207
- return Object.entries<Property>(properties as Record<string, Property>)
208
- .map(([key, property]) => {
209
- const childResolvedProperty = resolveProperty({
210
- propertyKey: propertyKey ? `${propertyKey}.${key}` : undefined,
211
- property: property,
212
- ignoreMissingFields,
213
- ...props
214
- });
215
- if (!childResolvedProperty) return {};
216
- return {
217
- [key]: childResolvedProperty
218
- };
219
- })
220
- .filter((a) => a !== null)
221
- .reduce((a, b) => ({ ...a,
222
- ...b }), {}) as Properties;
223
- }
224
-
225
- export function resolveArrayProperties<M>({
226
- propertyKey,
227
- property,
228
- ignoreMissingFields = false,
229
- ...props
230
- }: {
231
- propertyKey?: string,
232
- property: ArrayProperty,
233
- values?: Partial<M>,
234
- previousValues?: Partial<M>,
235
- path?: string,
236
- entityId?: string | number,
237
- index?: number,
238
- propertyConfigs?: Record<string, PropertyConfig>;
239
- ignoreMissingFields?: boolean;
240
- authController: AuthState;
241
- }): Property[] {
242
- const propertyValue = propertyKey ? getIn(props.values, propertyKey) : undefined;
243
-
244
- if (property.of) {
245
- if (Array.isArray(property.of)) {
246
- return property.of.map((p, index) => {
247
- return resolveProperty({
248
- propertyKey: `${propertyKey}.${index}`,
249
- property: p as Property,
250
- ignoreMissingFields,
251
- ...props,
252
- index
253
- });
254
- }) as Property[];
255
- } else {
256
- const of = property.of;
257
- const resolvedProperties = getArrayResolvedProperties({
258
- propertyValue,
259
- propertyKey,
260
- property,
261
- ignoreMissingFields,
262
- ...props
263
- });
264
- // Destructured to be *excluded* from `...rest`, not to be used —
265
- // see the comment below. Said explicitly so the discarded-value
266
- // ratchet does not carry a finding that is working as intended.
267
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
268
- const { values, previousValues, ...rest } = props;
269
- const ofProperty = resolveProperty({ // we don't want to pass the values of the parent entity
270
- property: of,
271
- ignoreMissingFields,
272
- ...rest
273
- });
274
- if (!ofProperty && !ignoreMissingFields)
275
- throw Error("When using a property builder as the 'of' prop of an ArrayProperty, you must return a valid child property")
276
- return resolvedProperties;
277
- }
278
- } else if (property.oneOf) {
279
- const typeField = property.oneOf?.typeField ?? DEFAULT_ONE_OF_TYPE;
280
- const resolvedProperties: Property[] = Array.isArray(propertyValue)
281
- ? propertyValue.map((v, index) => {
282
- const type = v && v[typeField];
283
- const childProperty = property.oneOf?.properties[type];
284
- if (!type || !childProperty) return null;
285
- return resolveProperty({
286
- propertyKey: `${propertyKey}.${index}`,
287
- property: childProperty,
288
- ignoreMissingFields,
289
- ...props
290
- });
291
- }).filter(e => Boolean(e)) as Property[]
292
- : [];
293
- return resolvedProperties;
294
- } else if (!property.columnType) {
295
- // An array with neither `of`/`oneOf` nor a `columnType` describes no element
296
- // type, so nothing can be generated or rendered from it.
297
- //
298
- // The escape hatch used to be `ui.Field` — "a custom component can render
299
- // anything" — which made a *presentation* field decide whether a schema was
300
- // valid, in code the Postgres generator runs. `columnType` is the same escape
301
- // hatch stated as data: `columnType: "text[]"` says what the column holds,
302
- // which is what both the generator and the form actually need.
303
- throw Error(`The array property (${propertyKey}) needs to declare an 'of' or a 'oneOf' property, or a \`columnType\` such as "text[]"`);
304
- } else {
305
- return [];
306
- }
307
-
308
- }
309
-
310
- export function getArrayResolvedProperties({
311
- propertyKey,
312
- propertyValue,
313
- property,
314
- ...props
315
- }: {
316
- propertyValue: unknown,
317
- propertyKey?: string,
318
- property: ArrayProperty,
319
- ignoreMissingFields: boolean,
320
- values?: object;
321
- previousValues?: object;
322
- path?: string;
323
- entityId?: string | number;
324
- index?: number;
325
- propertyConfigs?: Record<string, PropertyConfig>;
326
- authController: AuthState;
327
- }) {
328
-
329
- const of = property.of;
330
- if (!of)
331
- throw Error(
332
- `Trying to resolve an array property (${propertyKey}) without providing an 'of' property`
333
- )
334
- return Array.isArray(propertyValue)
335
- ? propertyValue.map((v: unknown, index: number) => {
336
- return resolveProperty({
337
- propertyKey: `${propertyKey}.${index}`,
338
- property: Array.isArray(of) ? of[index] : of,
339
- ...props,
340
- index
341
- });
342
- }).filter(e => Boolean(e)) as Property[]
343
- : [];
344
- }
345
-
346
- export function resolveEnumValues(input: EnumValues): EnumValueConfig[] | undefined {
347
- if (typeof input === "object") {
348
- return Object.entries(input).map(([id, value]) =>
349
- (typeof value === "string"
350
- ? {
351
- id,
352
- label: value
353
- }
354
- : value));
355
- } else if (Array.isArray(input)) {
356
- return input as EnumValueConfig[];
357
- } else {
358
- return undefined;
359
- }
360
- }
361
-
362
-
363
- /**
364
- * The lists rendered inside an entity view of `collection` — its tabs.
365
- *
366
- * The single derivation. There used to be two that disagreed: this one, and a
367
- * copy in `CollectionRegistry.normalizeCollection` that stamped each child with
368
- * the *target collection's* slug instead of the relation key. Since the
369
- * registry ran first and cached its answer onto `childCollections`, its version
370
- * was the one that won, and the frontend addressed child listings by a segment
371
- * the backend could not resolve.
372
- *
373
- * Order of precedence:
374
- * 1. `childCollections` — the explicit escape hatch for custom drivers.
375
- * 2. `subcollections` on an engine that has real containment (Firestore).
376
- * 3. many-relations on an engine that has relations (SQL).
377
- */
378
- export function getEntityChildViews<M extends Record<string, unknown> = Record<string, unknown>>(
379
- collection: CollectionConfig<M>
380
- ): EntityChildView[] {
381
- const asSubcollections = (collections: CollectionConfig<Record<string, unknown>>[]): EntityChildView[] =>
382
- collections.filter(Boolean).map(child => ({
383
- key: child.slug,
384
- collection: child,
385
- source: { kind: "subcollection" as const }
386
- }));
387
-
388
- if (collection.childCollections) {
389
- return asSubcollections(collection.childCollections() ?? []);
390
- }
391
-
392
- const capabilities = getDataSourceCapabilities(collection.engine);
393
-
394
- const declaredSubcollections = getDeclaredSubcollections(collection);
395
- if (capabilities.supportsSubcollections && declaredSubcollections) {
396
- return asSubcollections(declaredSubcollections() ?? []);
397
- }
398
-
399
- if (!capabilities.supportsRelations) return [];
400
-
401
- const resolvedRelations = resolveCollectionRelations(collection);
402
- const views: EntityChildView[] = [];
403
- const seen = new Set<string>();
404
-
405
- // Keyed by the map key, not by `relationName`: the map key is what
406
- // `findRelation` matches a path segment against, so it is the only one that
407
- // addresses the same relation on both sides of the wire. The map registers
408
- // some relations twice — once canonically, once under the declaring
409
- // property key — so dedupe on the underlying relation.
410
- for (const [relationKey, relation] of Object.entries(resolvedRelations)) {
411
- if (relation.cardinality !== "many") continue;
412
-
413
- const identity = relation.relationName ?? relationKey;
414
- if (seen.has(identity)) continue;
415
-
416
- let target: CollectionConfig | undefined;
417
- try {
418
- target = relation.target();
419
- } catch {
420
- continue;
421
- }
422
- if (!target) continue;
423
- seen.add(identity);
424
-
425
- // A name given to the declaring property is the author naming the tab.
426
- const declaringProperty = Object.entries((collection.properties ?? {}) as Record<string, Property>)
427
- .find(([propKey, p]) => p.type === "relation" && ((p as RelationProperty).relation?.relationName ?? propKey) === identity);
428
- const customName = declaringProperty?.[1]?.name;
429
-
430
- const base: CollectionConfig<Record<string, unknown>> = {
431
- ...target,
432
- slug: relationKey,
433
- ...(customName ? { name: customName,
434
- singularName: customName } : {})
435
- } as CollectionConfig<Record<string, unknown>>;
436
-
437
- views.push({
438
- key: relationKey,
439
- collection: (relation.overrides ? mergeDeep(base, relation.overrides) : base) as CollectionConfig<Record<string, unknown>>,
440
- source: {
441
- kind: "relation",
442
- relationKey,
443
- mode: isJunctionBackedRelation(relation) ? "linked" : "owned",
444
- targetSlug: target.slug
445
- }
446
- });
447
- }
448
-
449
- return views;
450
- }
451
-
452
- /**
453
- * Each of `collection`'s tabs paired with the property that declared it, when a
454
- * property declared it: child view key → property key.
455
- *
456
- * A many-relation can only be declared as a property — that is the documented
457
- * and only mechanism — and {@link getEntityChildViews} promotes it to a tab. So
458
- * one declaration reaches the panel twice, and neither surface knew about the
459
- * other. The form rendered a relation picker beside the tab, and the collection
460
- * table rendered *two* columns under one heading: the relation's own column,
461
- * showing the child rows, and a jump-to-tab button carrying the same name.
462
- *
463
- * The pairing is what lets each surface decide which half is redundant, and it
464
- * has to be a pairing rather than two sets because the two keys differ whenever
465
- * a relation is named. The match is on the resolved `relationName` — the
466
- * identity `getEntityChildViews` itself dedupes on — so a relation declared in
467
- * `relations` and pointed at by a differently-named property is recognised too.
468
- *
469
- * A relation with no property of its own is absent here, which is the point: it
470
- * has exactly one surface already, and nothing to weigh it against.
471
- *
472
- * Only top-level properties: a relation nested inside a `map` gets no tab.
473
- */
474
- export function getChildViewDeclaringProperties<M extends Record<string, unknown> = Record<string, unknown>>(
475
- collection: CollectionConfig<M>
476
- ): Map<string, string> {
477
- const pairs = new Map<string, string>();
478
-
479
- const relationProperties = Object.entries((collection.properties ?? {}) as Record<string, Property>)
480
- .filter(([, property]) => property?.type === "relation");
481
- if (relationProperties.length === 0) return pairs;
482
-
483
- const relationViews = getEntityChildViews(collection)
484
- .filter(view => view.source.kind === "relation");
485
- if (relationViews.length === 0) return pairs;
486
-
487
- const resolvedRelations = resolveCollectionRelations(collection);
488
- const identityOf = (relationKey: string): string =>
489
- resolvedRelations[relationKey]?.relationName ?? relationKey;
490
-
491
- const declaringPropertyByIdentity = new Map<string, string>();
492
- for (const [propertyKey, property] of relationProperties) {
493
- const relation = (property as RelationProperty).resolvedRelation ?? resolvedRelations[propertyKey];
494
- // A to-one relation is a foreign key the author edits, never a tab. No
495
- // view will match it — the views here are many-relations only — but
496
- // reading the cardinality says so where someone is looking.
497
- if (relation?.cardinality !== "many") continue;
498
- const identity = relation.relationName ?? propertyKey;
499
- if (!declaringPropertyByIdentity.has(identity)) declaringPropertyByIdentity.set(identity, propertyKey);
500
- }
501
-
502
- for (const view of relationViews) {
503
- const propertyKey = declaringPropertyByIdentity.get(
504
- identityOf((view.source as { relationKey: string }).relationKey));
505
- if (propertyKey) pairs.set(view.key, propertyKey);
506
- }
507
-
508
- return pairs;
509
- }
510
-
511
- /**
512
- * The property keys of `collection` whose relation is already one of its tabs.
513
- *
514
- * What a form asks: the tab is the treatment for a list of child rows, so the
515
- * picker beside it is the redundant half. See
516
- * {@link getChildViewDeclaringProperties}.
517
- */
518
- export function getChildViewRelationPropertyKeys<M extends Record<string, unknown> = Record<string, unknown>>(
519
- collection: CollectionConfig<M>
520
- ): Set<string> {
521
- return new Set(getChildViewDeclaringProperties(collection).values());
522
- }
523
-
524
- /**
525
- * The child views of `collection` as bare collections.
526
- *
527
- * The flattened view of {@link getEntityChildViews}, for navigation code that
528
- * only needs to match a path segment against a slug. Anything that cares *what
529
- * kind* of list it is showing — chiefly the admin, which must not offer a
530
- * global delete on a shared row — should read the views instead.
531
- */
532
- export function getSubcollections<M extends Record<string, unknown> = Record<string, unknown>>(collection: CollectionConfig<M>): CollectionConfig<Record<string, unknown>>[] {
533
- return getEntityChildViews(collection).map(view => view.collection);
534
- }