@rebasepro/common 0.12.1-canary.gf5f1d39 → 0.13.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/index.es.js +170 -37
- package/dist/index.es.js.map +1 -1
- package/dist/util/auth-default-policies.d.ts +18 -0
- package/dist/util/builders.d.ts +1 -20
- package/dist/util/email.d.ts +29 -0
- package/dist/util/identity.d.ts +14 -0
- package/dist/util/index.d.ts +2 -0
- package/dist/util/policy/sqlToPolicy.d.ts +6 -1
- package/dist/util/string-column-length.d.ts +24 -0
- package/package.json +7 -7
- package/src/data/buildRebaseData.ts +1 -1
- package/src/util/auth-default-policies.ts +22 -0
- package/src/util/builders.ts +0 -50
- package/src/util/email.ts +32 -0
- package/src/util/identity.ts +36 -0
- package/src/util/index.ts +2 -0
- package/src/util/permissions.test.ts +17 -3
- package/src/util/pg-column-to-property.ts +14 -1
- package/src/util/policy/evaluatePolicy.ts +6 -2
- package/src/util/policy/policyToPostgres.ts +9 -3
- package/src/util/policy/sqlToPolicy.ts +10 -3
- package/src/util/resolve-relation.ts +74 -9
- package/src/util/string-column-length.ts +31 -0
package/dist/index.es.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ANONYMOUS_USER_ID, CANONICAL_TO_REST, DEFAULT_DATA_SOURCE_KEY, EntityReference, EntityRelation, NULL_OPS, REST_TO_CANONICAL, getDataSourceCapabilities, getDeclaredSubcollections, isManyToMany, isPostgresCollectionConfig, isRelationalCollectionConfig, policy, toCanonicalOp } from "@rebasepro/types";
|
|
2
|
-
import { deepClone, generateForeignKeyName, getIn, getPolicyOperations, isDefaultFieldConfigId, mergeDeep, prettifyIdentifier, randomString, removeFunctions, toSnakeCase } from "@rebasepro/utils";
|
|
1
|
+
import { ANONYMOUS_USER_ID, ANONYMOUS_USER_IDS, CANONICAL_TO_REST, DEFAULT_DATA_SOURCE_KEY, EntityReference, EntityRelation, NULL_OPS, REST_TO_CANONICAL, getDataSourceCapabilities, getDeclaredSubcollections, isAnonymousUid, isManyToMany, isPostgresCollectionConfig, isRelationalCollectionConfig, policy, toCanonicalOp } from "@rebasepro/types";
|
|
2
|
+
import { deepClone, generateForeignKeyName, getIn, getPolicyNamesForRules, getPolicyOperations, isDefaultFieldConfigId, mergeDeep, prettifyIdentifier, randomString, removeFunctions, toSnakeCase } from "@rebasepro/utils";
|
|
3
3
|
import jsonLogic from "json-logic-js";
|
|
4
4
|
import { deepEqual } from "fast-equals";
|
|
5
5
|
//#region src/util/common.ts
|
|
@@ -226,6 +226,34 @@ function getPrimaryKeys(collection) {
|
|
|
226
226
|
//#region src/util/identity.ts
|
|
227
227
|
/** Separator between the parts of a composite address. */
|
|
228
228
|
var COMPOSITE_ID_SEPARATOR = ":::";
|
|
229
|
+
/** The eight-four-four-four-twelve shape of a UUID, any version. */
|
|
230
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
231
|
+
/** Whether one address part can be a value of the column it addresses. */
|
|
232
|
+
function partIsAddressable(part, pk) {
|
|
233
|
+
if (pk.isUUID) return UUID_PATTERN.test(String(part));
|
|
234
|
+
if (pk.type === "number") return typeof part === "number" ? Number.isFinite(part) : !isNaN(parseInt(String(part), 10));
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Whether an address could name a row at all, before asking the database.
|
|
239
|
+
*
|
|
240
|
+
* A `uuid` column cannot hold `"new"`, and an `integer` column cannot hold
|
|
241
|
+
* `"abc"` — so the answer to "which row is this" is "none", and that is a 404,
|
|
242
|
+
* not a failure. Postgres cannot say so politely: the comparison never runs, it
|
|
243
|
+
* raises `22P02` and aborts the enclosing transaction, after which every
|
|
244
|
+
* further statement returns the far less helpful `25P02`.
|
|
245
|
+
*
|
|
246
|
+
* `isUUID` must come from the column, not from `isId: "uuid"` in a config: the
|
|
247
|
+
* config is a claim about a key, and a `text` column that holds ids of some
|
|
248
|
+
* other shape is a working app this must not start rejecting.
|
|
249
|
+
*/
|
|
250
|
+
function isAddressableId(idValue, primaryKeys) {
|
|
251
|
+
if (primaryKeys.length === 0) return false;
|
|
252
|
+
if (primaryKeys.length === 1) return partIsAddressable(idValue, primaryKeys[0]);
|
|
253
|
+
const parts = String(idValue).split(":::");
|
|
254
|
+
if (parts.length !== primaryKeys.length) return false;
|
|
255
|
+
return parts.every((part, i) => partIsAddressable(part, primaryKeys[i]));
|
|
256
|
+
}
|
|
229
257
|
/**
|
|
230
258
|
* Derive a row's address from its key columns.
|
|
231
259
|
*
|
|
@@ -330,6 +358,39 @@ function resolvePrimaryKeys(collection) {
|
|
|
330
358
|
return [];
|
|
331
359
|
}
|
|
332
360
|
//#endregion
|
|
361
|
+
//#region src/util/email.ts
|
|
362
|
+
/**
|
|
363
|
+
* Email normalization — one implementation, because the database enforces it.
|
|
364
|
+
*
|
|
365
|
+
* `ensureAuthTablesExist` puts a `UNIQUE INDEX ON users (lower(email))` on the
|
|
366
|
+
* auth table. That index decides what "the same address" means, and it does not
|
|
367
|
+
* trim: to Postgres, `' foo@bar.com'` and `'foo@bar.com'` are two addresses and
|
|
368
|
+
* both may exist. So every write that reaches the column has to agree with
|
|
369
|
+
* every read, exactly, or the two disagree in the one direction that matters —
|
|
370
|
+
* a row that exists and cannot be found.
|
|
371
|
+
*
|
|
372
|
+
* That is not hypothetical. The lookup path trimmed and the admin create paths
|
|
373
|
+
* did not, so a user created through `POST /api/data/users` or
|
|
374
|
+
* `POST /api/auth/admin/users` with a stray space was stored untrimmed,
|
|
375
|
+
* survived the unique index alongside the real address, and was unreachable by
|
|
376
|
+
* login forever after. The HTTP auth routes were unaffected only because Zod's
|
|
377
|
+
* `.email()` happens to reject surrounding whitespace — a guard on a different
|
|
378
|
+
* layer, for a different reason, that the admin paths do not sit behind.
|
|
379
|
+
*
|
|
380
|
+
* It lives in `common` because `server`, `server-postgres` and `server-mongo`
|
|
381
|
+
* all write this column and must agree exactly, and `common` is the only
|
|
382
|
+
* package all three already depend on.
|
|
383
|
+
*/
|
|
384
|
+
/**
|
|
385
|
+
* Canonical form of an email address: trimmed, lower-cased.
|
|
386
|
+
*
|
|
387
|
+
* Non-strings pass through untouched, so this is safe to apply to a value out
|
|
388
|
+
* of a partial update payload whose type is not known yet.
|
|
389
|
+
*/
|
|
390
|
+
function normalizeEmail(email) {
|
|
391
|
+
return typeof email === "string" ? email.trim().toLowerCase() : email;
|
|
392
|
+
}
|
|
393
|
+
//#endregion
|
|
333
394
|
//#region src/util/enums.ts
|
|
334
395
|
function enumToObjectEntries(enumValues) {
|
|
335
396
|
if (Array.isArray(enumValues)) return enumValues;
|
|
@@ -393,8 +454,7 @@ function fullPathToCollectionSegments(path) {
|
|
|
393
454
|
function resolveRelation(relation, sourceCollection, propertyKey) {
|
|
394
455
|
const target = relation.target;
|
|
395
456
|
if (typeof target !== "function") throw new Error(`Relation${relation.relationName ? ` '${relation.relationName}'` : ""} on '${sourceCollection.slug}' has no \`target\`. Give it a thunk: \`target: () => otherCollection\`.`);
|
|
396
|
-
const targetCollection = target
|
|
397
|
-
if (!targetCollection?.slug) throw new Error(`Relation${relation.relationName ? ` '${relation.relationName}'` : ""} on '${sourceCollection.slug}' has a \`target\` that did not resolve to a collection.`);
|
|
457
|
+
const targetCollection = callTarget(relation, sourceCollection, propertyKey, target);
|
|
398
458
|
const relationName = relation.relationName ?? propertyKey ?? toSnakeCase(targetCollection.slug);
|
|
399
459
|
const shared = {
|
|
400
460
|
relationName,
|
|
@@ -421,7 +481,8 @@ function resolveRelation(relation, sourceCollection, propertyKey) {
|
|
|
421
481
|
cardinality: "one",
|
|
422
482
|
writable: true,
|
|
423
483
|
shared: false,
|
|
424
|
-
foreignKeyOnTarget: relation.foreignKeyOnTarget ?? generateForeignKeyName(sourceName)
|
|
484
|
+
foreignKeyOnTarget: relation.foreignKeyOnTarget ?? generateForeignKeyName(sourceName),
|
|
485
|
+
sourceKey: relation.sourceKey
|
|
425
486
|
};
|
|
426
487
|
case "hasMany": return {
|
|
427
488
|
...shared,
|
|
@@ -429,7 +490,8 @@ function resolveRelation(relation, sourceCollection, propertyKey) {
|
|
|
429
490
|
cardinality: "many",
|
|
430
491
|
writable: true,
|
|
431
492
|
shared: false,
|
|
432
|
-
foreignKeyOnTarget: relation.foreignKeyOnTarget ?? generateForeignKeyName(sourceName)
|
|
493
|
+
foreignKeyOnTarget: relation.foreignKeyOnTarget ?? generateForeignKeyName(sourceName),
|
|
494
|
+
sourceKey: relation.sourceKey
|
|
433
495
|
};
|
|
434
496
|
case "manyToMany": {
|
|
435
497
|
const sourceTable = getTableName(sourceCollection);
|
|
@@ -458,6 +520,43 @@ function resolveRelation(relation, sourceCollection, propertyKey) {
|
|
|
458
520
|
default: throw new Error(`Unknown relation kind: ${JSON.stringify(relation)}`);
|
|
459
521
|
}
|
|
460
522
|
}
|
|
523
|
+
/** How this relation is addressed in an error message, before it has a resolved name. */
|
|
524
|
+
function describe(relation, sourceCollection, propertyKey) {
|
|
525
|
+
const name = relation.relationName ?? propertyKey;
|
|
526
|
+
return `Relation${name ? ` '${name}'` : ""} on '${sourceCollection.slug}'`;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Call the `target` thunk, and translate the two ways an import cycle breaks it
|
|
530
|
+
* into an error that names the cause.
|
|
531
|
+
*
|
|
532
|
+
* The thunk exists to defer the reference until every module has finished
|
|
533
|
+
* evaluating, and for a cycle that closes at import time it does. What it cannot
|
|
534
|
+
* defer is a cycle that leaves the binding permanently unusable, and there are
|
|
535
|
+
* two shapes of that:
|
|
536
|
+
*
|
|
537
|
+
* - **ESM/TDZ.** `const` and `class` bindings in a not-yet-evaluated module are
|
|
538
|
+
* in the temporal dead zone, so reading one throws `ReferenceError: x is not
|
|
539
|
+
* defined`. The stack points at the thunk — a one-line arrow function that is
|
|
540
|
+
* obviously fine — and says nothing about the cycle that made it throw.
|
|
541
|
+
* - **CJS interop.** The half-initialised module object has no `default` yet,
|
|
542
|
+
* the import resolves to `undefined`, and the thunk returns it without
|
|
543
|
+
* complaint. That one used to surface here as "did not resolve to a
|
|
544
|
+
* collection", which is true and unhelpful.
|
|
545
|
+
*
|
|
546
|
+
* Both mean the same thing, and the fix for both is the same: break the cycle,
|
|
547
|
+
* or move the relation into the collection that does not close it.
|
|
548
|
+
*/
|
|
549
|
+
function callTarget(relation, sourceCollection, propertyKey, target) {
|
|
550
|
+
let targetCollection;
|
|
551
|
+
try {
|
|
552
|
+
targetCollection = target();
|
|
553
|
+
} catch (error) {
|
|
554
|
+
if (error instanceof ReferenceError) throw new Error(`${describe(relation, sourceCollection, propertyKey)} targets a collection that is not initialized yet — almost always an import cycle between the two collection files. Break the cycle (move the shared piece into a third module, or import the target lazily) so the target's module finishes evaluating before the registry is built.`, { cause: error });
|
|
555
|
+
throw error;
|
|
556
|
+
}
|
|
557
|
+
if (!targetCollection?.slug) throw new Error(`${describe(relation, sourceCollection, propertyKey)} has a \`target\` that resolved to ${targetCollection === void 0 ? "`undefined`" : "something that is not a collection"}. ` + (targetCollection === void 0 ? "Under CommonJS interop an import cycle resolves the default import to `undefined`, so check whether this collection and its target import each other. Otherwise the thunk is returning the wrong value — it must return the collection itself, not a promise or a module." : "The thunk must return a collection config with a `slug`."));
|
|
558
|
+
return targetCollection;
|
|
559
|
+
}
|
|
461
560
|
//#endregion
|
|
462
561
|
//#region src/util/relations.ts
|
|
463
562
|
/**
|
|
@@ -957,7 +1056,12 @@ var UID_NOT_NULL = /auth\.uid\(\)\s+IS\s+NOT\s+NULL/i;
|
|
|
957
1056
|
* is how the trusted *server* context is recognised), so:
|
|
958
1057
|
*
|
|
959
1058
|
* - `auth.uid() IS NOT NULL` is a tautology on the user path, and
|
|
960
|
-
* - `auth.uid() != 'anon'`
|
|
1059
|
+
* - `auth.uid() != 'anon'` excludes one spelling of anonymous and admits the
|
|
1060
|
+
* other. This one is not hypothetical and was not only a foreign habit:
|
|
1061
|
+
* rebase's own request path reported `'anon'` while everything that compiled
|
|
1062
|
+
* or checked a policy used `'anonymous'`, so whichever literal an author
|
|
1063
|
+
* picked, half the anonymous callers walked through. See
|
|
1064
|
+
* {@link ANONYMOUS_USER_IDS}.
|
|
961
1065
|
*
|
|
962
1066
|
* Either one turns a lockdown into a full grant, and neither looks wrong. No
|
|
963
1067
|
* real user id is ever one of these literals, and a user-context request is
|
|
@@ -995,7 +1099,7 @@ function findAnonymousGrants(expr) {
|
|
|
995
1099
|
found.push({
|
|
996
1100
|
pattern: "foreign-uid-literal",
|
|
997
1101
|
detail: literal.value,
|
|
998
|
-
explanation: `'${literal.value}' is a ${platform} convention. Rebase reports an anonymous request as '${ANONYMOUS_USER_ID}', so comparing against '${literal.value}' passes for every caller. Use \`condition: policy.authenticated()\` to mean "signed in".`
|
|
1102
|
+
explanation: `'${literal.value}' is a ${platform} convention. Rebase reports an anonymous request as '${ANONYMOUS_USER_ID}', so comparing against '${literal.value}' passes for every caller. Use \`condition: policy.authenticated()\` to mean "signed in" — it compiles to NOT IN (${ANONYMOUS_USER_IDS.map((v) => `'${v}'`).join(", ")}), covering every spelling rebase has reported rather than whichever one you remember.`
|
|
999
1103
|
});
|
|
1000
1104
|
return;
|
|
1001
1105
|
}
|
|
@@ -1090,7 +1194,7 @@ function compile(expr, scope) {
|
|
|
1090
1194
|
}
|
|
1091
1195
|
case "rolesOverlap": return `string_to_array(auth.roles(), ',') && ${rolesArraySql(expr.roles)}`;
|
|
1092
1196
|
case "rolesContain": return `string_to_array(auth.roles(), ',') @> ${rolesArraySql(expr.roles)}`;
|
|
1093
|
-
case "authenticated": return `auth.uid() IS NOT NULL AND auth.uid()
|
|
1197
|
+
case "authenticated": return `auth.uid() IS NOT NULL AND auth.uid() NOT IN (${ANONYMOUS_USER_IDS.map(quoteLiteral).join(", ")})`;
|
|
1094
1198
|
case "serverContext": return "auth.uid() IS NULL";
|
|
1095
1199
|
case "existsIn": return compileExistsIn(expr, scope);
|
|
1096
1200
|
case "raw": return expr.sql.replace(/\{(\w+)\}/g, (_, col) => `${outerQualifier(scope)}${resolveColumnName(col, scope.outerCollection)}`);
|
|
@@ -1187,7 +1291,7 @@ function evaluatePolicy(expr, ctx) {
|
|
|
1187
1291
|
const userRoles = ctx.roles ?? [];
|
|
1188
1292
|
return expr.roles.every((r) => r === "public" || userRoles.includes(r));
|
|
1189
1293
|
}
|
|
1190
|
-
case "authenticated": return ctx.uid != null && ctx.uid
|
|
1294
|
+
case "authenticated": return ctx.uid != null && !isAnonymousUid(ctx.uid);
|
|
1191
1295
|
case "serverContext": return false;
|
|
1192
1296
|
case "existsIn": return "unknown";
|
|
1193
1297
|
case "raw": return "unknown";
|
|
@@ -1356,35 +1460,12 @@ function canDeleteEntity(collection, authContext, path, entity) {
|
|
|
1356
1460
|
//#endregion
|
|
1357
1461
|
//#region src/util/builders.ts
|
|
1358
1462
|
/**
|
|
1359
|
-
* @deprecated Use {@link defineCollection} instead — it infers property
|
|
1360
|
-
* types automatically (autocomplete on `titleProperty`, `sort`,
|
|
1361
|
-
* `propertiesOrder`, callbacks) without manual generics.
|
|
1362
|
-
* `buildCollection` is kept for FireCMS migration compatibility and will
|
|
1363
|
-
* be removed before 1.0.
|
|
1364
|
-
*
|
|
1365
|
-
* @group Builder
|
|
1366
|
-
*/
|
|
1367
|
-
function buildCollection(collection) {
|
|
1368
|
-
return collection;
|
|
1369
|
-
}
|
|
1370
|
-
/**
|
|
1371
1463
|
* Implementation — delegates to the correct overload at the type level.
|
|
1372
1464
|
* At runtime this is a plain identity function.
|
|
1373
1465
|
*/
|
|
1374
1466
|
function defineCollection(collection) {
|
|
1375
1467
|
return collection;
|
|
1376
1468
|
}
|
|
1377
|
-
/**
|
|
1378
|
-
* @deprecated Use plain typed property objects with {@link defineCollection}
|
|
1379
|
-
* instead — `defineCollection` infers property types automatically, making
|
|
1380
|
-
* this wrapper unnecessary. `buildProperty` is kept for FireCMS migration
|
|
1381
|
-
* compatibility and will be removed before 1.0.
|
|
1382
|
-
*
|
|
1383
|
-
* @group Builder
|
|
1384
|
-
*/
|
|
1385
|
-
function buildProperty(property) {
|
|
1386
|
-
return property;
|
|
1387
|
-
}
|
|
1388
1469
|
//#endregion
|
|
1389
1470
|
//#region src/util/storage.ts
|
|
1390
1471
|
/**
|
|
@@ -1653,6 +1734,26 @@ function getInjectedSecurityRules(collection) {
|
|
|
1653
1734
|
const explicitCount = (collection.securityRules ?? []).length;
|
|
1654
1735
|
return getEffectiveSecurityRules(collection).slice(explicitCount);
|
|
1655
1736
|
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Every policy name `rebase db push` would write for a collection.
|
|
1739
|
+
*
|
|
1740
|
+
* This is the answer to "did the codebase produce this live policy?", and it is
|
|
1741
|
+
* more than `securityRules.map(r => r.name)` for two reasons:
|
|
1742
|
+
*
|
|
1743
|
+
* - a rule without an explicit `name` compiles to `<table>_<op>_<hash>`, one
|
|
1744
|
+
* per operation, so comparing `rule.name` to `policyname` never matches it;
|
|
1745
|
+
* - the generator also injects the safe-by-default baseline
|
|
1746
|
+
* (`<table>_default_admin_*`), which is in no collection's `securityRules`.
|
|
1747
|
+
*
|
|
1748
|
+
* Every UI that flags drift has to get both right, and each one that derived it
|
|
1749
|
+
* by hand got a different subset — which is how four policies *Rebase itself
|
|
1750
|
+
* wrote* came to be badged as hand-written drift on every table in a project,
|
|
1751
|
+
* with a button offering to import them back into the codebase that produced
|
|
1752
|
+
* them. There is one derivation now, and this is it.
|
|
1753
|
+
*/
|
|
1754
|
+
function getGeneratedPolicyNames(collection) {
|
|
1755
|
+
return getPolicyNamesForRules(getEffectiveSecurityRules(collection), getTableName(collection));
|
|
1756
|
+
}
|
|
1656
1757
|
//#endregion
|
|
1657
1758
|
//#region src/util/junction-policies.ts
|
|
1658
1759
|
var SERVER_OR_ADMIN_EXPR = policy.or(policy.serverContext(), policy.rolesOverlap(["admin"]));
|
|
@@ -1952,7 +2053,7 @@ function buildConditionContext(params) {
|
|
|
1952
2053
|
* Maps a PostgreSQL column data type to a Rebase property type.
|
|
1953
2054
|
*/
|
|
1954
2055
|
function pgTypeToRebaseProperty(column) {
|
|
1955
|
-
const { column_name, data_type, udt_name, is_nullable, column_default, enum_values } = column;
|
|
2056
|
+
const { column_name, data_type, udt_name, is_nullable, column_default, character_maximum_length, enum_values } = column;
|
|
1956
2057
|
const required = is_nullable === "NO";
|
|
1957
2058
|
const prettifiedName = prettifyIdentifier(column_name);
|
|
1958
2059
|
const isAutoId = column_default != null && (column_default.includes("nextval") || column_default.includes("gen_random_uuid") || column_default.includes("uuid_generate") || column_default.includes("identity"));
|
|
@@ -1976,11 +2077,15 @@ function pgTypeToRebaseProperty(column) {
|
|
|
1976
2077
|
let colType = "varchar";
|
|
1977
2078
|
if (dt === "text" || dt === "citext") colType = "text";
|
|
1978
2079
|
if (dt === "char" || dt === "character") colType = "char";
|
|
2080
|
+
const declaredLength = colType === "text" ? null : character_maximum_length;
|
|
1979
2081
|
const prop = {
|
|
1980
2082
|
type: "string",
|
|
1981
2083
|
name: prettifiedName,
|
|
1982
2084
|
columnType: colType,
|
|
1983
|
-
validation: required ? {
|
|
2085
|
+
validation: required || declaredLength ? {
|
|
2086
|
+
...required ? { required: true } : {},
|
|
2087
|
+
...declaredLength ? { max: declaredLength } : {}
|
|
2088
|
+
} : void 0
|
|
1984
2089
|
};
|
|
1985
2090
|
if (isAutoId) prop.isId = "manual";
|
|
1986
2091
|
return prop;
|
|
@@ -2184,6 +2289,34 @@ function buildCollectionFromTableMetadata(tableName, metadata) {
|
|
|
2184
2289
|
};
|
|
2185
2290
|
}
|
|
2186
2291
|
//#endregion
|
|
2292
|
+
//#region src/util/string-column-length.ts
|
|
2293
|
+
/**
|
|
2294
|
+
* The length a bounded string column is declared with when the property does
|
|
2295
|
+
* not say. Historical: it is what the DDL generator hardcoded, kept so that
|
|
2296
|
+
* regenerating an existing schema does not silently redefine its columns.
|
|
2297
|
+
*/
|
|
2298
|
+
var DEFAULT_STRING_COLUMN_LENGTH = 255;
|
|
2299
|
+
/**
|
|
2300
|
+
* How wide a `varchar`/`char` column should be for a given property.
|
|
2301
|
+
*
|
|
2302
|
+
* One definition, three call sites, because they used to disagree. For the same
|
|
2303
|
+
* `columnType: "varchar"` property the DDL generator emitted `VARCHAR(255)`
|
|
2304
|
+
* while the Drizzle generator emitted a bare `varchar("col")` — which Postgres
|
|
2305
|
+
* reads as *unbounded* — so which of the two you ran decided whether the column
|
|
2306
|
+
* had a limit at all. Introspection then dropped the length entirely, so reading
|
|
2307
|
+
* an existing `character varying(500)` column back and regenerating it produced
|
|
2308
|
+
* a `VARCHAR(255)`: a silent narrowing of a column with data already in it.
|
|
2309
|
+
*
|
|
2310
|
+
* `validation.max` is the property's own statement about how long the value may
|
|
2311
|
+
* be, so it is the only sensible source for the column's width — and it keeps
|
|
2312
|
+
* the constraint the database enforces in step with the one the app enforces,
|
|
2313
|
+
* rather than inventing a second, different limit underneath it.
|
|
2314
|
+
*/
|
|
2315
|
+
function resolveStringColumnLength(prop) {
|
|
2316
|
+
const max = prop.validation?.max;
|
|
2317
|
+
return typeof max === "number" && Number.isInteger(max) && max > 0 ? max : 255;
|
|
2318
|
+
}
|
|
2319
|
+
//#endregion
|
|
2187
2320
|
//#region src/data/resolveDataSource.ts
|
|
2188
2321
|
/**
|
|
2189
2322
|
* Build a keyed registry from a list of {@link DataSourceDefinition}s.
|
|
@@ -3150,7 +3283,7 @@ function rowToEntity(row, slug, primaryKeys = []) {
|
|
|
3150
3283
|
};
|
|
3151
3284
|
}
|
|
3152
3285
|
/**
|
|
3153
|
-
* The relation envelope `
|
|
3286
|
+
* The relation envelope `toFlatRow` writes where a relation was:
|
|
3154
3287
|
* `{ id, path, __type: "relation", data: { id, path, values } }`. It is the
|
|
3155
3288
|
* admin's view-model, and the only pipeline that produces one is postgres'.
|
|
3156
3289
|
*/
|
|
@@ -3773,6 +3906,6 @@ async function detectJunctionTables(executeSql) {
|
|
|
3773
3906
|
return junctionTables;
|
|
3774
3907
|
}
|
|
3775
3908
|
//#endregion
|
|
3776
|
-
export { COLLECTION_PATH_SEPARATOR, COMPOSITE_ID_SEPARATOR, CollectionRegistry, DEFAULT_FIND_ALL_MAX_ROWS, DEFAULT_MAX_PAGES, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, DEFAULT_PAGE_SIZE, JUNCTION_TABLES_SQL, QueryBuilder, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, RebasePaginationError, and,
|
|
3909
|
+
export { COLLECTION_PATH_SEPARATOR, COMPOSITE_ID_SEPARATOR, CollectionRegistry, DEFAULT_FIND_ALL_MAX_ROWS, DEFAULT_MAX_PAGES, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, DEFAULT_PAGE_SIZE, DEFAULT_STRING_COLUMN_LENGTH, JUNCTION_TABLES_SQL, QueryBuilder, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, RebasePaginationError, and, buildCollectionFromTableMetadata, buildCompositeId, buildConditionContext, buildPropertyCallbacks, buildRebaseData, buildRoutedRebaseData, buildSdkData, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, checkOperation, classifyTable, collectAllPages, cond, createDataSourceRegistry, createPaginationHelpers, createRelationRef, createRelationRefWithData, defaultUsersCollection, defineCollection, deserializeFilter, deserializeLogicalCondition, deserializeOrderBy, detectJunctionTables, embedParentExpression, enumToObjectEntries, evaluateCondition, evaluatePolicy, findAnonymousGrants, findRelation, fullPathToCollectionSegments, getArrayResolvedProperties, getColumnName, getDeclaredPrimaryKeys, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEffectiveSecurityRules, getEntityChildViews, getEnumVarName, getGeneratedPolicyNames, getInjectedSecurityRules, getJunctionCollectionConfig, getJunctionSecurityRules, getLabelOrConfigFrom, getPrimaryKeys, getReferenceFrom, getRelationFrom, getSubcollections, getTableName, getTableVarName, isAddressableId, isJunctionBackedRelation, isPropertyBuilder, isRebaseInternalTable, normalizeEmail, normalizeToEntityRelation, or, paginateFind, parseIdValues, policyToPostgres, registerConditionOperations, resolveArrayProperties, resolveCollectionRelations, resolveDataSource, resolveEnumValues, resolveJunctionSpecs, resolvePrimaryKeys, resolveProperties, resolveProperty, resolvePropertyEnum, resolveRelation, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, resolveStringColumnLength, sanitizeData, securityRuleToConditions, segmentsToStrippedPath, serializeFilter, serializeLogicalCondition, serializeOrderBy, sortProperties, sqlToPolicy, stripCollectionPath, traverseValueProperty, traverseValuesProperties, updateDateAutoValues, wrapAsEntityData, wrapAsSdkData };
|
|
3777
3910
|
|
|
3778
3911
|
//# sourceMappingURL=index.es.js.map
|