@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,282 +0,0 @@
1
- import {
2
- DataType,
3
- Entity,
4
- EntityReference,
5
- EntityRelation,
6
- EntityStatus,
7
- EntityValues,
8
- Properties,
9
- Property
10
- } from "@rebasepro/types";
11
- import { DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE } from "./common";
12
- import { mergeDeep } from "@rebasepro/utils";
13
-
14
- export function isPropertyBuilder(property?: Property) {
15
- return typeof property?.dynamicProps === "function";
16
- }
17
-
18
- export function getDefaultValuesFor<M extends Record<string, unknown>>(properties: Properties): Partial<EntityValues<M>> {
19
- if (!properties) return {};
20
- return Object.entries(properties)
21
- .map(([key, property]) => {
22
- if (!property) return {};
23
- const value = getDefaultValueFor(property);
24
- return value === undefined ? {} : { [key]: value };
25
- })
26
- .reduce((a, b) => ({ ...a,
27
- ...b }), {}) as EntityValues<M>;
28
- }
29
-
30
- export function getDefaultValueFor(property?: Property): unknown {
31
- if (!property) return undefined;
32
- if (isPropertyBuilder(property)) return undefined;
33
- if (property.defaultValue || property.defaultValue === null) {
34
- return property.defaultValue;
35
- } else if (property.type === "map" && property.properties) {
36
- const defaultValuesFor = getDefaultValuesFor(property.properties as Properties);
37
- if (Object.keys(defaultValuesFor).length === 0) return undefined;
38
- return defaultValuesFor;
39
- } else {
40
- return getDefaultValueFortype(property.type);
41
- }
42
- }
43
-
44
- export function getDefaultValueFortype(type: DataType): unknown {
45
- if (type === "string") {
46
- return null;
47
- } else if (type === "number") {
48
- return null;
49
- } else if (type === "boolean") {
50
- return false;
51
- } else if (type === "date") {
52
- return null;
53
- } else if (type === "array") {
54
- return [];
55
- } else if (type === "map") {
56
- return {};
57
- } else if (type === "vector") {
58
- return null;
59
- } else if (type === "binary") {
60
- return null;
61
- } else {
62
- return null;
63
- }
64
- }
65
-
66
- /**
67
- * Update the automatic values in a entity before save
68
- * @group Driver
69
- */
70
- export function updateDateAutoValues<M extends Record<string, unknown>>({
71
- inputValues,
72
- properties,
73
- status,
74
- timestampNowValue
75
- }:
76
- {
77
- inputValues: Partial<EntityValues<M>>,
78
- properties: Properties,
79
- status: EntityStatus,
80
- timestampNowValue: unknown
81
- }): EntityValues<M> {
82
- return traverseValuesProperties(
83
- inputValues,
84
- properties,
85
- (inputValue, property) => {
86
- if (property.type === "date") {
87
- if (status === "existing" && property.autoValue === "on_update") {
88
- return timestampNowValue;
89
- } else if ((status === "new" || status === "copy") &&
90
- (property.autoValue === "on_update" || property.autoValue === "on_create")) {
91
- return timestampNowValue;
92
- } else {
93
- return inputValue;
94
- }
95
- } else {
96
- return inputValue;
97
- }
98
- }
99
- ) ?? {} as M;
100
- }
101
-
102
- /**
103
- * Add missing required fields, expected in the collection, to the values of a entity
104
- * @param values
105
- * @param properties
106
- * @group Driver
107
- */
108
- export function sanitizeData<M extends Record<string, unknown>>
109
- (
110
- values: EntityValues<M>,
111
- properties: Properties
112
- ) {
113
- const result = values as Record<string, unknown>;
114
- Object.entries(properties)
115
- .forEach(([key, property]) => {
116
- if (values && values[key] !== undefined) result[key] = values[key];
117
- else if ((property as Property).validation?.required) result[key] = null;
118
- });
119
- return result;
120
- }
121
-
122
- export function getReferenceFrom<M extends Record<string, unknown>>(entity: Entity<M>): EntityReference {
123
- if (typeof entity.id !== "string")
124
- throw new Error("Only string IDs are supported in references");
125
- return new EntityReference({
126
- id: entity.id,
127
- path: entity.path,
128
- driver: entity.driver,
129
- databaseId: entity.databaseId
130
- });
131
- }
132
-
133
- export function getRelationFrom<M extends Record<string, unknown>>(entity: Entity<M>): EntityRelation {
134
- return new EntityRelation(entity.id, entity.path, entity as unknown as Record<string, unknown>);
135
- }
136
-
137
- /**
138
- * Normalize a value into a proper EntityRelation instance.
139
- * Handles EntityRelation class instances, and plain objects
140
- * with `__type === "relation"` or an `isEntityRelation()` method.
141
- *
142
- * When `propertyType` is `"relation"`, also accepts plain objects that
143
- * have `id` and `path` fields — these are relation-shaped objects from
144
- * edge cases in the data pipeline (REST fallback, stale cache, custom data source).
145
- *
146
- * When `targetPath` is given, also accepts a bare id. A relation column is a
147
- * foreign key, and the REST layer returns it as the scalar it is; only some
148
- * fetch paths hydrate it into an object. Which form a caller sees therefore
149
- * depends on how the row was loaded, and a caller that only accepted objects
150
- * reported half of its own data as a type error. The declared target is the
151
- * missing half: with it, an id is a relation that has not been fetched yet.
152
- *
153
- * Returns null if the value cannot be coerced.
154
- */
155
- export function normalizeToEntityRelation(value: unknown, propertyType?: string, targetPath?: string): EntityRelation | null {
156
- if (value instanceof EntityRelation) return value;
157
-
158
- if (targetPath && (typeof value === "string" || typeof value === "number")) {
159
- // An empty string is an unset foreign key, not row "".
160
- if (value === "") return null;
161
- return new EntityRelation(value, targetPath);
162
- }
163
-
164
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
165
-
166
- const obj = value as Record<string, unknown>;
167
- const isRelationLike =
168
- obj.__type === "relation" ||
169
- obj.__type === "reference" ||
170
- (typeof obj.isEntityRelation === "function" && (obj.isEntityRelation as () => boolean)()) ||
171
- (typeof obj.isEntityReference === "function" && (obj.isEntityReference as () => boolean)()) ||
172
- (propertyType === "relation" && typeof obj.id !== "undefined" && typeof obj.path === "string");
173
-
174
- if (!isRelationLike) return null;
175
-
176
- return new EntityRelation(
177
- obj.id as string | number,
178
- obj.path as string,
179
- obj.data as Record<string, unknown> | undefined
180
- );
181
- }
182
-
183
- export function traverseValuesProperties<M extends Record<string, unknown>>(
184
- inputValues: Partial<EntityValues<M>>,
185
- properties: Properties,
186
- operation: (value: unknown, property: Property) => unknown
187
- ): EntityValues<M> | undefined {
188
- // Handle null/undefined inputValues - use empty object as base for mergeDeep
189
- const safeInputValues = inputValues ?? {};
190
-
191
- const updatedValues = Object.entries(properties)
192
- .map(([key, property]) => {
193
- const inputValue = safeInputValues && (safeInputValues)[key];
194
- const updatedValue = traverseValueProperty(inputValue, property as Property, operation);
195
- if (updatedValue === null) return null;
196
- if (updatedValue === undefined) return undefined;
197
- return ({ [key]: updatedValue });
198
- })
199
- .reduce((a, b) => ({ ...a,
200
- ...b }), {}) as EntityValues<M>;
201
- // Use mergeDeep to preserve class instances like EntityReference, GeoPoint
202
- const result = mergeDeep(safeInputValues, updatedValues);
203
- if (!result || Object.keys(result).length === 0) return undefined;
204
- return result;
205
- }
206
-
207
- export function traverseValueProperty(inputValue: unknown,
208
- property: Property,
209
- operation: (value: unknown, property: Property) => unknown): unknown {
210
-
211
- let value;
212
- if (property.type === "map" && property.properties) {
213
- value = traverseValuesProperties(inputValue as Partial<Record<string, unknown>>, property.properties, operation);
214
- } else if (property.type === "array") {
215
- const of = property.of;
216
- if (of && Array.isArray(inputValue) && !Array.isArray(of)) {
217
- value = inputValue.map((e) => traverseValueProperty(e, of, operation));
218
- } else if (of && Array.isArray(inputValue) && Array.isArray(of)) {
219
- value = inputValue.map((e, i) => {
220
- if (i < of.length)
221
- return traverseValueProperty(e, of[i], operation);
222
- return null
223
- }).filter(Boolean);
224
- } else if (property.oneOf && Array.isArray(inputValue)) {
225
- const typeField = property.oneOf?.typeField ?? DEFAULT_ONE_OF_TYPE;
226
- const valueField = property.oneOf?.valueField ?? DEFAULT_ONE_OF_VALUE;
227
- value = inputValue.map((e) => {
228
- if (e === null) return null;
229
- if (typeof e !== "object") return e;
230
- const rec = e as Record<string, unknown>;
231
- const type = rec[typeField] as string;
232
- const childProperty = property.oneOf?.properties[type];
233
- if (!type || !childProperty) return e;
234
- return {
235
- [typeField]: type,
236
- [valueField]: traverseValueProperty(rec[valueField], childProperty, operation)
237
- };
238
- });
239
- } else {
240
- value = inputValue;
241
- }
242
- } else {
243
- value = operation(inputValue, property);
244
- }
245
-
246
- return value;
247
- }
248
-
249
- /**
250
- * Relation reference types used throughout the server layer.
251
- * These replace the 50+ manual `{ id, path, __type: "relation" }` constructions.
252
- */
253
- export interface RelationRef {
254
- readonly id: string | number;
255
- readonly path: string;
256
- readonly __type: "relation";
257
- }
258
-
259
- export interface RelationRefWithData extends RelationRef {
260
- readonly data: Entity;
261
- }
262
-
263
- /**
264
- * Create a lightweight relation stub for admin views.
265
- * Replaces inline `{ id, path, __type: "relation" }` object literals.
266
- */
267
- export function createRelationRef(id: string | number, path: string): RelationRef {
268
- return { id,
269
- path,
270
- __type: "relation" };
271
- }
272
-
273
- /**
274
- * Create a hydrated relation reference that includes the full entity data.
275
- * Used when entity data has been pre-fetched (e.g., via batch loading or JOINs).
276
- */
277
- export function createRelationRefWithData(id: string | number, path: string, data: Entity): RelationRefWithData {
278
- return { id,
279
- path,
280
- __type: "relation",
281
- data };
282
- }
package/src/util/enums.ts DELETED
@@ -1,26 +0,0 @@
1
- import { EnumValueConfig, EnumValues } from "@rebasepro/types";
2
-
3
- export function enumToObjectEntries(enumValues: EnumValues): EnumValueConfig[] {
4
- if (Array.isArray(enumValues)) {
5
- return enumValues;
6
- } else {
7
- return Object.entries(enumValues).map(([id, value]) => {
8
- if (typeof value === "string") {
9
- return {
10
- id,
11
- label: value
12
- }
13
- } else {
14
- return {
15
- ...value,
16
- id
17
- }
18
- }
19
- });
20
- }
21
- }
22
-
23
- export function getLabelOrConfigFrom(enumValues: EnumValueConfig[], key?: string | number): EnumValueConfig | undefined {
24
- if (key === null || key === undefined) return undefined;
25
- return enumValues.find((entry) => String(entry.id) === String(key));
26
- }
@@ -1,202 +0,0 @@
1
- /**
2
- * Row identity: the address of a row, and how to derive it.
3
- *
4
- * Postgres has no `id`. A row is identified by its primary key — one or more
5
- * columns, with any names and any types. `id` is something we synthesize on top
6
- * of that: a single string token, because the admin needs *one* value it can put
7
- * in a URL (`/products/1:::2`), use as a cache key, and hang a relation ref off.
8
- *
9
- * That token is an address, not data. It is derived from the row's columns and
10
- * never stored in them — a row is exactly its columns, with their real types.
11
- * Writing the address back into the row is what used to rename primary keys
12
- * (`sku` → `id`) and restringify them (`42` → `"42"`) on the way out.
13
- *
14
- * These live in `common` because both sides need them and must agree exactly:
15
- * the driver parses an incoming address back into key columns, and the admin
16
- * derives the address from a row it was served.
17
- */
18
-
19
- /**
20
- * A primary-key column: its name, the type it round-trips as, and whether it is
21
- * a UUID (which is a string despite sometimes being described as an id "number").
22
- */
23
- export interface PrimaryKeyInfo {
24
- fieldName: string;
25
- type: "string" | "number";
26
- isUUID?: boolean;
27
- }
28
-
29
- /** Separator between the parts of a composite address. */
30
- export const COMPOSITE_ID_SEPARATOR = ":::";
31
-
32
- /** The eight-four-four-four-twelve shape of a UUID, any version. */
33
- const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
34
-
35
- /** Whether one address part can be a value of the column it addresses. */
36
- function partIsAddressable(part: string | number, pk: PrimaryKeyInfo): boolean {
37
- if (pk.isUUID) return UUID_PATTERN.test(String(part));
38
- if (pk.type === "number") {
39
- return typeof part === "number"
40
- ? Number.isFinite(part)
41
- : !isNaN(parseInt(String(part), 10));
42
- }
43
- return true;
44
- }
45
-
46
- /**
47
- * Whether an address could name a row at all, before asking the database.
48
- *
49
- * A `uuid` column cannot hold `"new"`, and an `integer` column cannot hold
50
- * `"abc"` — so the answer to "which row is this" is "none", and that is a 404,
51
- * not a failure. Postgres cannot say so politely: the comparison never runs, it
52
- * raises `22P02` and aborts the enclosing transaction, after which every
53
- * further statement returns the far less helpful `25P02`.
54
- *
55
- * `isUUID` must come from the column, not from `isId: "uuid"` in a config: the
56
- * config is a claim about a key, and a `text` column that holds ids of some
57
- * other shape is a working app this must not start rejecting.
58
- */
59
- export function isAddressableId(idValue: string | number, primaryKeys: PrimaryKeyInfo[]): boolean {
60
- if (primaryKeys.length === 0) return false;
61
- if (primaryKeys.length === 1) return partIsAddressable(idValue, primaryKeys[0]);
62
-
63
- const parts = String(idValue).split(COMPOSITE_ID_SEPARATOR);
64
- if (parts.length !== primaryKeys.length) return false;
65
- return parts.every((part, i) => partIsAddressable(part, primaryKeys[i]));
66
- }
67
-
68
- /**
69
- * Derive a row's address from its key columns.
70
- *
71
- * Single key → the value as a string. Composite → each part joined by
72
- * {@link COMPOSITE_ID_SEPARATOR}, in primary-key order, which is what
73
- * {@link parseIdValues} expects to invert.
74
- */
75
- export function buildCompositeId(values: Record<string, unknown>, primaryKeys: PrimaryKeyInfo[]): string {
76
- if (primaryKeys.length === 0) {
77
- return "";
78
- }
79
- if (primaryKeys.length === 1) {
80
- return String(values[primaryKeys[0].fieldName] ?? "");
81
- }
82
- return primaryKeys.map(pk => String(values[pk.fieldName] ?? "")).join(COMPOSITE_ID_SEPARATOR);
83
- }
84
-
85
- /**
86
- * Invert {@link buildCompositeId}: turn an address back into key columns, each
87
- * coerced to the type its column actually round-trips as.
88
- *
89
- * This is the boundary where a URL segment becomes a query parameter, so a
90
- * malformed address must throw rather than silently produce a query that
91
- * matches the wrong row (or none).
92
- */
93
- export function parseIdValues(idValue: string | number, primaryKeys: PrimaryKeyInfo[]): Record<string, string | number> {
94
- const result: Record<string, string | number> = {};
95
-
96
- if (primaryKeys.length === 0) {
97
- return result;
98
- }
99
-
100
- if (primaryKeys.length === 1) {
101
- const pk = primaryKeys[0];
102
- if (pk.type === "number" && !pk.isUUID) {
103
- const parsed = typeof idValue === "number" ? idValue : parseInt(String(idValue), 10);
104
- if (isNaN(parsed)) {
105
- throw new Error(`Invalid numeric ID: ${idValue}`);
106
- }
107
- result[pk.fieldName] = parsed;
108
- } else {
109
- result[pk.fieldName] = String(idValue);
110
- }
111
- return result;
112
- }
113
-
114
- // Composite key
115
- const parts = String(idValue).split(COMPOSITE_ID_SEPARATOR);
116
- if (parts.length !== primaryKeys.length) {
117
- throw new Error(`Composite ID parts mismatch. Expected ${primaryKeys.length}, got ${parts.length} for ID: ${idValue}`);
118
- }
119
-
120
- for (let i = 0; i < primaryKeys.length; i++) {
121
- const pk = primaryKeys[i];
122
- const val = parts[i];
123
- if (pk.type === "number" && !pk.isUUID) {
124
- const parsed = parseInt(val, 10);
125
- if (isNaN(parsed)) {
126
- throw new Error(`Invalid numeric ID component: ${val}`);
127
- }
128
- result[pk.fieldName] = parsed;
129
- } else {
130
- result[pk.fieldName] = val;
131
- }
132
- }
133
-
134
- return result;
135
- }
136
-
137
- /**
138
- * The primary keys of a collection, as declared by its properties.
139
- *
140
- * This is the only tier both sides can read, because it is the only one written
141
- * in the config: the postgres driver can also infer keys from the Drizzle
142
- * schema, which the browser never sees and is never sent — the admin compiles
143
- * the collection files into its own bundle rather than being served them. A key
144
- * that lives only in the Drizzle schema is therefore invisible here, and the
145
- * server says so at boot (`warnOnKeysTheAdminCannotResolve`) naming the `isId`
146
- * to add.
147
- *
148
- * Returns an empty array when a collection declares none, which callers must
149
- * treat as "not addressable" rather than defaulting to `id`: guessing a key
150
- * that is not the real one produces confidently wrong addresses.
151
- */
152
- export function getDeclaredPrimaryKeys(collection: {
153
- properties?: Record<string, unknown>;
154
- }): PrimaryKeyInfo[] {
155
- const properties = collection.properties;
156
- if (!properties) return [];
157
-
158
- const keys: PrimaryKeyInfo[] = [];
159
- for (const [fieldName, propRaw] of Object.entries(properties)) {
160
- const prop = propRaw as { type?: string; isId?: unknown } | undefined;
161
- if (!prop || typeof prop !== "object") continue;
162
- if (!("isId" in prop) || !prop.isId) continue;
163
- keys.push({
164
- fieldName,
165
- type: prop.type === "number" ? "number" : "string",
166
- isUUID: prop.isId === "uuid"
167
- });
168
- }
169
- return keys;
170
- }
171
-
172
- /**
173
- * The keys to address a collection's rows with, resolved the way the driver
174
- * resolves them — minus the tier the browser cannot reach.
175
- *
176
- * The postgres driver tries, in order: properties marked `isId`; the primary
177
- * keys of the Drizzle schema; and finally a column literally named `id`. Only
178
- * the first and last are visible in a `CollectionConfig`, which is what both
179
- * sides share.
180
- *
181
- * So the two agree except on a collection that declares no `isId` and whose key
182
- * is known only to Drizzle. There, the driver reads the real key, and this
183
- * either resolves nothing (reported to the console by the caller) or — if the
184
- * table happens to have an unrelated `id` property — resolves `id`, which is
185
- * the wrong key and cannot be detected from here: the addresses look right and
186
- * route wrong. Only the config can settle it, so the server names both cases
187
- * at boot (`warnOnKeysTheAdminCannotResolve`) with the `isId` to add.
188
- */
189
- export function resolvePrimaryKeys(collection: {
190
- properties?: Record<string, unknown>;
191
- }): PrimaryKeyInfo[] {
192
- const declared = getDeclaredPrimaryKeys(collection);
193
- if (declared.length > 0) return declared;
194
-
195
- const idProp = collection.properties?.id as { type?: string } | undefined;
196
- if (idProp && typeof idProp === "object") {
197
- return [{ fieldName: "id",
198
- type: idProp.type === "number" ? "number" : "string" }];
199
- }
200
-
201
- return [];
202
- }
package/src/util/index.ts DELETED
@@ -1,21 +0,0 @@
1
- export * from "./collections";
2
- export * from "./common";
3
- export * from "./entities";
4
- export * from "./identity";
5
- export * from "./email";
6
- export * from "./enums";
7
- export * from "./paths";
8
- export * from "./resolutions";
9
- export * from "./policy";
10
- export * from "./permissions";
11
- export * from "./builders";
12
- export * from "./storage";
13
- export * from "./callbacks";
14
- export * from "./relations";
15
- export * from "./resolve-relation";
16
- export * from "./auth-default-policies";
17
- export * from "./junction-policies";
18
- export * from "./conditions";
19
- export * from "./pg-column-to-property";
20
- export * from "./string-column-length";
21
- export * from "./internal-tables";