@rebasepro/common 0.17.3 → 0.18.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/README.md +4 -0
- package/dist/collections/CollectionRegistry.d.ts +1 -1
- package/dist/collections/default-collections.d.ts +15 -84
- package/dist/data/buildRebaseData.d.ts +1 -1
- package/dist/data/filter-dialect.d.ts +11 -0
- package/dist/data/sort-dialect.d.ts +15 -3
- package/dist/index.es.js +375 -63
- package/dist/index.es.js.map +1 -1
- package/dist/util/builders.d.ts +69 -24
- package/dist/util/callback-errors.d.ts +77 -0
- package/dist/util/callback-errors.test.d.ts +1 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/policy/evaluatePolicy.d.ts +6 -0
- package/dist/util/relations.d.ts +41 -0
- package/dist/util/table-name.test.d.ts +1 -0
- package/package.json +26 -22
- package/src/collections/CollectionRegistry.ts +0 -485
- package/src/collections/default-collections.ts +0 -109
- package/src/collections/index.ts +0 -2
- package/src/data/buildRebaseData.ts +0 -816
- package/src/data/buildRoutedRebaseData.ts +0 -103
- package/src/data/filter-conditions.ts +0 -46
- package/src/data/filter-dialect.ts +0 -737
- package/src/data/paginate.ts +0 -334
- package/src/data/query_builder.ts +0 -176
- package/src/data/resolveDataSource.ts +0 -135
- package/src/data/sort-dialect.ts +0 -237
- package/src/index.ts +0 -11
- package/src/table-classification.ts +0 -109
- package/src/types/json-logic-js.d.ts +0 -8
- package/src/util/auth-default-policies.ts +0 -215
- package/src/util/builders.ts +0 -82
- package/src/util/callbacks.ts +0 -122
- package/src/util/collections.ts +0 -117
- package/src/util/common.ts +0 -2
- package/src/util/conditions.ts +0 -168
- package/src/util/email.ts +0 -32
- package/src/util/entities.ts +0 -282
- package/src/util/enums.ts +0 -26
- package/src/util/identity.ts +0 -202
- package/src/util/index.ts +0 -21
- package/src/util/internal-tables.test.ts +0 -188
- package/src/util/internal-tables.ts +0 -197
- package/src/util/junction-policies.ts +0 -355
- package/src/util/paths.ts +0 -27
- package/src/util/permissions.test.ts +0 -866
- package/src/util/permissions.ts +0 -206
- package/src/util/pg-column-to-property.ts +0 -377
- package/src/util/policy/evaluatePolicy.ts +0 -194
- package/src/util/policy/index.ts +0 -4
- package/src/util/policy/policyToPostgres.ts +0 -263
- package/src/util/policy/securityRuleToConditions.ts +0 -67
- package/src/util/policy/sqlToPolicy.ts +0 -422
- package/src/util/relations.ts +0 -236
- package/src/util/resolutions.ts +0 -534
- package/src/util/resolve-relation.ts +0 -243
- package/src/util/storage.ts +0 -177
- package/src/util/string-column-length.ts +0 -31
package/src/util/entities.ts
DELETED
|
@@ -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
|
-
}
|
package/src/util/identity.ts
DELETED
|
@@ -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";
|