@rebasepro/common 0.12.1-canary.g4e7bcbf → 0.12.1-canary.g52d71ee

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 CHANGED
@@ -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
  /**
@@ -1361,35 +1460,12 @@ function canDeleteEntity(collection, authContext, path, entity) {
1361
1460
  //#endregion
1362
1461
  //#region src/util/builders.ts
1363
1462
  /**
1364
- * @deprecated Use {@link defineCollection} instead — it infers property
1365
- * types automatically (autocomplete on `titleProperty`, `sort`,
1366
- * `propertiesOrder`, callbacks) without manual generics.
1367
- * `buildCollection` is kept for FireCMS migration compatibility and will
1368
- * be removed before 1.0.
1369
- *
1370
- * @group Builder
1371
- */
1372
- function buildCollection(collection) {
1373
- return collection;
1374
- }
1375
- /**
1376
1463
  * Implementation — delegates to the correct overload at the type level.
1377
1464
  * At runtime this is a plain identity function.
1378
1465
  */
1379
1466
  function defineCollection(collection) {
1380
1467
  return collection;
1381
1468
  }
1382
- /**
1383
- * @deprecated Use plain typed property objects with {@link defineCollection}
1384
- * instead — `defineCollection` infers property types automatically, making
1385
- * this wrapper unnecessary. `buildProperty` is kept for FireCMS migration
1386
- * compatibility and will be removed before 1.0.
1387
- *
1388
- * @group Builder
1389
- */
1390
- function buildProperty(property) {
1391
- return property;
1392
- }
1393
1469
  //#endregion
1394
1470
  //#region src/util/storage.ts
1395
1471
  /**
@@ -3830,6 +3906,6 @@ async function detectJunctionTables(executeSql) {
3830
3906
  return junctionTables;
3831
3907
  }
3832
3908
  //#endregion
3833
- 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, buildCollection, buildCollectionFromTableMetadata, buildCompositeId, buildConditionContext, buildProperty, 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, isJunctionBackedRelation, isPropertyBuilder, isRebaseInternalTable, 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 };
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 };
3834
3910
 
3835
3911
  //# sourceMappingURL=index.es.js.map