@rebasepro/common 0.6.1 → 0.7.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.
@@ -1,5 +1,12 @@
1
1
  import { EntityCollection } from "@rebasepro/types";
2
+ import { DataSourceRegistry } from "../data/resolveDataSource";
2
3
  export declare class CollectionRegistry {
4
+ /**
5
+ * Declared data sources, used during normalization to resolve each
6
+ * collection's engine (so `dataSource`-only collections get the right
7
+ * capabilities). Empty by default → behaviour keys off `driver` as before.
8
+ */
9
+ private dataSources;
3
10
  private collectionsByTableName;
4
11
  private collectionsBySlug;
5
12
  private rootCollections;
@@ -9,7 +16,13 @@ export declare class CollectionRegistry {
9
16
  private rawRootCollections;
10
17
  private cachedRawCollectionsList;
11
18
  private lastRawInputSnapshot;
12
- constructor(collections?: EntityCollection[]);
19
+ constructor(collections?: EntityCollection[], dataSources?: DataSourceRegistry);
20
+ /**
21
+ * Provide the declared data sources used to resolve each collection's
22
+ * engine during normalization. Set this before registering collections.
23
+ * Returns true if the registry changed (callers may re-register).
24
+ */
25
+ setDataSources(dataSources: DataSourceRegistry): boolean;
13
26
  reset(): void;
14
27
  /**
15
28
  * Registers a collection and its subcollections recursively.
@@ -0,0 +1,53 @@
1
+ import { RebaseData } from "@rebasepro/types";
2
+ /**
3
+ * Parameters for {@link buildRoutedRebaseData}.
4
+ */
5
+ export interface RoutedRebaseDataParams {
6
+ /**
7
+ * The default data source. Handles every collection that does not
8
+ * resolve to an entry in `sources` (i.e. server-transport collections,
9
+ * which ride the Rebase client).
10
+ */
11
+ defaultData: RebaseData;
12
+ /**
13
+ * Per-data-source {@link RebaseData} instances for direct and custom
14
+ * transports, keyed by data-source key (e.g. `"analytics"`). Server-
15
+ * mediated sources are not listed here — they fall through to
16
+ * `defaultData`.
17
+ */
18
+ sources: Record<string, RebaseData>;
19
+ /**
20
+ * Resolve the data-source key for a given collection slug or path.
21
+ * Typically backed by the collection registry + `resolveDataSource`
22
+ * (`resolveDataSource(registry.getCollection(path), defs).key`).
23
+ *
24
+ * Return `undefined` (or a key absent from `sources`) to route to the
25
+ * default data source.
26
+ */
27
+ resolveKey: (slugOrPath: string) => string | undefined;
28
+ }
29
+ /**
30
+ * Build a {@link RebaseData} that routes each collection to the right
31
+ * backend based on its resolved data source.
32
+ *
33
+ * `.collection(path)` (and dynamic `data.products`-style access) resolves the
34
+ * collection's data-source key via `resolveKey` and delegates to the matching
35
+ * entry in `sources`, falling back to `defaultData` when there is no match.
36
+ * Because routing keys off the *path being accessed*, a reference widget
37
+ * inside a Firestore form that points at a Postgres collection is still
38
+ * served by Postgres — routing follows the target, not the ancestor.
39
+ *
40
+ * When `sources` is empty this returns `defaultData` untouched, so the
41
+ * single-driver setup keeps identical behaviour and identity (important for
42
+ * effect dependencies that key off the data instance).
43
+ *
44
+ * @example
45
+ * const data = buildRoutedRebaseData({
46
+ * defaultData: client.data,
47
+ * sources: { analytics: buildRebaseData(firestoreDriver) },
48
+ * resolveKey: (path) => resolveDataSource(registry.getCollection(path), defs).key
49
+ * });
50
+ * await data.products.find(); // → default (server / Postgres)
51
+ * await data.events.find(); // → Firestore, if `events.dataSource === "analytics"`
52
+ */
53
+ export declare function buildRoutedRebaseData({ defaultData, sources, resolveKey }: RoutedRebaseDataParams): RebaseData;
@@ -0,0 +1,43 @@
1
+ import { DataSourceDefinition, ResolvedDataSource } from "@rebasepro/types";
2
+ /**
3
+ * The subset of a collection needed to resolve its data source. Accepting a
4
+ * structural type (rather than the full `EntityCollection`) keeps this usable
5
+ * from anywhere — frontend router, backend registry, editor — without coupling
6
+ * to the collection union.
7
+ */
8
+ export interface DataSourceResolvable {
9
+ /** Preferred routing key. */
10
+ dataSource?: string;
11
+ /** Legacy engine hint / fallback routing key. */
12
+ driver?: string;
13
+ /** Within-engine instance. */
14
+ databaseId?: string;
15
+ }
16
+ /** A lookup of data-source definitions by key. */
17
+ export type DataSourceRegistry = Record<string, DataSourceDefinition>;
18
+ /**
19
+ * Build a keyed registry from a list of {@link DataSourceDefinition}s.
20
+ * Later entries win on key collision.
21
+ */
22
+ export declare function createDataSourceRegistry(definitions?: DataSourceDefinition[]): DataSourceRegistry;
23
+ /**
24
+ * Resolve the effective data source for a collection — the single source of
25
+ * truth shared by the frontend router, the backend driver registry, and the
26
+ * editor's capability lookups.
27
+ *
28
+ * Resolution order:
29
+ * 1. The routing **key** is `collection.dataSource`, else the legacy
30
+ * `collection.driver`, else {@link DEFAULT_DATA_SOURCE_KEY}.
31
+ * 2. If a definition is registered for that key, it provides `engine`,
32
+ * `transport`, and `databaseId`.
33
+ * 3. Otherwise values are synthesized for backward compatibility: `engine`
34
+ * from the legacy `driver` (or the key, or `"postgres"`), `transport`
35
+ * defaults to `"server"`, and `databaseId` from the collection.
36
+ *
37
+ * `capabilities` are always derived from the resolved `engine`, so two
38
+ * data sources sharing an engine share capabilities.
39
+ *
40
+ * @param collection the collection (or any object carrying the routing fields)
41
+ * @param registry optional registry of declared data sources
42
+ */
43
+ export declare function resolveDataSource(collection: DataSourceResolvable | undefined, registry?: DataSourceRegistry): ResolvedDataSource;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  export * from "./util";
2
2
  export * from "./collections";
3
3
  export * from "./data/buildRebaseData";
4
+ export * from "./data/buildRoutedRebaseData";
5
+ export * from "./data/resolveDataSource";
4
6
  export * from "./data/query_builder";
7
+ export * from "./table-classification";
package/dist/index.es.js CHANGED
@@ -1,4 +1,4 @@
1
- import { EntityReference, EntityRelation, getDataSourceCapabilities } from "@rebasepro/types";
1
+ import { DEFAULT_DATA_SOURCE_KEY, EntityReference, EntityRelation, getDataSourceCapabilities } from "@rebasepro/types";
2
2
  import { deepClone, generateForeignKeyName, getIn, isDefaultFieldConfigId, mergeDeep, randomString, removeFunctions, toSnakeCase } from "@rebasepro/utils";
3
3
  import jsonLogic from "json-logic-js";
4
4
  import { deepEqual } from "fast-equals";
@@ -1411,8 +1411,57 @@ function applyEnumConditions(enumValues, conditions, context) {
1411
1411
  return result;
1412
1412
  }
1413
1413
  //#endregion
1414
+ //#region src/data/resolveDataSource.ts
1415
+ /**
1416
+ * Build a keyed registry from a list of {@link DataSourceDefinition}s.
1417
+ * Later entries win on key collision.
1418
+ */
1419
+ function createDataSourceRegistry(definitions) {
1420
+ const registry = {};
1421
+ for (const def of definitions ?? []) registry[def.key] = def;
1422
+ return registry;
1423
+ }
1424
+ /**
1425
+ * Resolve the effective data source for a collection — the single source of
1426
+ * truth shared by the frontend router, the backend driver registry, and the
1427
+ * editor's capability lookups.
1428
+ *
1429
+ * Resolution order:
1430
+ * 1. The routing **key** is `collection.dataSource`, else the legacy
1431
+ * `collection.driver`, else {@link DEFAULT_DATA_SOURCE_KEY}.
1432
+ * 2. If a definition is registered for that key, it provides `engine`,
1433
+ * `transport`, and `databaseId`.
1434
+ * 3. Otherwise values are synthesized for backward compatibility: `engine`
1435
+ * from the legacy `driver` (or the key, or `"postgres"`), `transport`
1436
+ * defaults to `"server"`, and `databaseId` from the collection.
1437
+ *
1438
+ * `capabilities` are always derived from the resolved `engine`, so two
1439
+ * data sources sharing an engine share capabilities.
1440
+ *
1441
+ * @param collection the collection (or any object carrying the routing fields)
1442
+ * @param registry optional registry of declared data sources
1443
+ */
1444
+ function resolveDataSource(collection, registry) {
1445
+ const key = collection?.dataSource ?? collection?.driver ?? DEFAULT_DATA_SOURCE_KEY;
1446
+ const def = registry?.[key];
1447
+ const engine = def?.engine ?? collection?.driver ?? (key !== DEFAULT_DATA_SOURCE_KEY ? key : "postgres");
1448
+ return {
1449
+ key,
1450
+ engine,
1451
+ transport: def?.transport ?? "server",
1452
+ databaseId: collection?.databaseId ?? def?.databaseId,
1453
+ capabilities: getDataSourceCapabilities(engine)
1454
+ };
1455
+ }
1456
+ //#endregion
1414
1457
  //#region src/collections/CollectionRegistry.ts
1415
1458
  var CollectionRegistry = class {
1459
+ /**
1460
+ * Declared data sources, used during normalization to resolve each
1461
+ * collection's engine (so `dataSource`-only collections get the right
1462
+ * capabilities). Empty by default → behaviour keys off `driver` as before.
1463
+ */
1464
+ dataSources = {};
1416
1465
  collectionsByTableName = /* @__PURE__ */ new Map();
1417
1466
  collectionsBySlug = /* @__PURE__ */ new Map();
1418
1467
  rootCollections = [];
@@ -1422,9 +1471,20 @@ var CollectionRegistry = class {
1422
1471
  rawRootCollections = [];
1423
1472
  cachedRawCollectionsList = null;
1424
1473
  lastRawInputSnapshot = null;
1425
- constructor(collections) {
1474
+ constructor(collections, dataSources) {
1475
+ if (dataSources) this.dataSources = dataSources;
1426
1476
  if (collections) this.registerMultiple(collections);
1427
1477
  }
1478
+ /**
1479
+ * Provide the declared data sources used to resolve each collection's
1480
+ * engine during normalization. Set this before registering collections.
1481
+ * Returns true if the registry changed (callers may re-register).
1482
+ */
1483
+ setDataSources(dataSources) {
1484
+ if (deepEqual(this.dataSources, dataSources)) return false;
1485
+ this.dataSources = dataSources ?? {};
1486
+ return true;
1487
+ }
1428
1488
  reset() {
1429
1489
  this.collectionsByTableName.clear();
1430
1490
  this.collectionsBySlug.clear();
@@ -1493,6 +1553,10 @@ var CollectionRegistry = class {
1493
1553
  }
1494
1554
  normalizeCollection(collection) {
1495
1555
  const result = { ...collection };
1556
+ if (result.dataSource && !result.driver) {
1557
+ const engine = resolveDataSource(result, this.dataSources).engine;
1558
+ if (engine) result.driver = engine;
1559
+ }
1496
1560
  const extractedRelations = this.extractRelationsFromProperties(result.properties);
1497
1561
  const relResult = result;
1498
1562
  const manualRelations = getDataSourceCapabilities(result.driver).supportsRelations ? relResult.relations ?? [] : [];
@@ -2169,6 +2233,125 @@ function buildRebaseData(driver) {
2169
2233
  } });
2170
2234
  }
2171
2235
  //#endregion
2172
- export { COLLECTION_PATH_SEPARATOR, CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, QueryBuilder, addInitialSlash, and, applyPropertyConditions, buildAdditionalFieldDelegate, buildCollection, buildConditionContext, buildEntityCallbacks, buildEnum, buildEnumValueConfig, buildProperties, buildPropertiesOrBuilder, buildProperty, buildPropertyCallbacks, buildRebaseData, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, checkOperation, cond, createRelationRef, createRelationRefWithData, defaultUsersCollection, enumToObjectEntries, evaluateCondition, findRelation, fullPathToCollectionSegments, getArrayResolvedProperties, getCollectionBySlugWithin, getCollectionPathsCombinations, getColumnName, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEntityImagePreviewPropertyKey, getEnumVarName, getLabelOrConfigFrom, getLastSegment, getLocalChangesBackup, getNavigationEntriesFromPath, getParentReferencesFromPath, getPrimaryKeys, getReferenceFrom, getRelationFrom, getSubcollections, getTableName, getTableVarName, isHidden, isPropertyBuilder, isReadOnly, normalizeToEntityRelation, or, registerConditionOperations, removeInitialAndTrailingSlashes, removeInitialSlash, removeTrailingSlash, resolveArrayProperties, resolveCollectionPathIds, resolveCollectionRelations, resolveDefaultSelectedView, resolveEnumValues, resolveProperties, resolveProperty, resolvePropertyEnum, resolvePropertyRelation, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, sanitizeData, sanitizeRelation, segmentsToStrippedPath, sortProperties, stripCollectionPath, traverseValueProperty, traverseValuesProperties, updateDateAutoValues };
2236
+ //#region src/data/buildRoutedRebaseData.ts
2237
+ /**
2238
+ * Build a {@link RebaseData} that routes each collection to the right
2239
+ * backend based on its resolved data source.
2240
+ *
2241
+ * `.collection(path)` (and dynamic `data.products`-style access) resolves the
2242
+ * collection's data-source key via `resolveKey` and delegates to the matching
2243
+ * entry in `sources`, falling back to `defaultData` when there is no match.
2244
+ * Because routing keys off the *path being accessed*, a reference widget
2245
+ * inside a Firestore form that points at a Postgres collection is still
2246
+ * served by Postgres — routing follows the target, not the ancestor.
2247
+ *
2248
+ * When `sources` is empty this returns `defaultData` untouched, so the
2249
+ * single-driver setup keeps identical behaviour and identity (important for
2250
+ * effect dependencies that key off the data instance).
2251
+ *
2252
+ * @example
2253
+ * const data = buildRoutedRebaseData({
2254
+ * defaultData: client.data,
2255
+ * sources: { analytics: buildRebaseData(firestoreDriver) },
2256
+ * resolveKey: (path) => resolveDataSource(registry.getCollection(path), defs).key
2257
+ * });
2258
+ * await data.products.find(); // → default (server / Postgres)
2259
+ * await data.events.find(); // → Firestore, if `events.dataSource === "analytics"`
2260
+ */
2261
+ function buildRoutedRebaseData({ defaultData, sources, resolveKey }) {
2262
+ if (!sources || Object.keys(sources).length === 0) return defaultData;
2263
+ function resolve(slugOrPath) {
2264
+ const key = resolveKey(slugOrPath);
2265
+ if (key && sources[key]) return sources[key];
2266
+ return defaultData;
2267
+ }
2268
+ function getAccessor(slugOrPath) {
2269
+ return resolve(slugOrPath).collection(slugOrPath);
2270
+ }
2271
+ return new Proxy({ collection: getAccessor }, { get(_target, prop) {
2272
+ if (prop === "collection") return getAccessor;
2273
+ if (typeof prop === "symbol") return void 0;
2274
+ if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return void 0;
2275
+ return getAccessor(toSnakeCase(prop));
2276
+ } });
2277
+ }
2278
+ //#endregion
2279
+ //#region src/table-classification.ts
2280
+ /** Schemas that are always considered Rebase-internal. */
2281
+ var REBASE_INTERNAL_SCHEMAS = ["rebase", "auth"];
2282
+ /** Table-name prefixes that mark a table as Rebase-internal regardless of schema. */
2283
+ var REBASE_INTERNAL_PREFIXES = [
2284
+ "_rebase_",
2285
+ "_auth_",
2286
+ "drizzle_"
2287
+ ];
2288
+ /**
2289
+ * Synchronously classify a table based on naming conventions.
2290
+ *
2291
+ * @param tableName - The unqualified name of the table.
2292
+ * @param schemaName - The schema the table belongs to (e.g. `"public"`, `"rebase"`).
2293
+ * @returns `"rebase-internal"` when the table belongs to a reserved schema or
2294
+ * carries a reserved prefix; `"user"` otherwise.
2295
+ *
2296
+ * @remarks
2297
+ * Junction-table detection requires an async database query and is therefore
2298
+ * **not** handled by this function. Use {@link detectJunctionTables} to obtain
2299
+ * the set of junction tables, then reclassify as needed.
2300
+ */
2301
+ function classifyTable(tableName, schemaName) {
2302
+ if (REBASE_INTERNAL_SCHEMAS.includes(schemaName) || REBASE_INTERNAL_PREFIXES.some((prefix) => tableName.startsWith(prefix))) return "rebase-internal";
2303
+ return "user";
2304
+ }
2305
+ /**
2306
+ * Convenience predicate that checks whether a table is Rebase-internal.
2307
+ *
2308
+ * @param tableName - The unqualified name of the table.
2309
+ * @param schemaName - The schema the table belongs to.
2310
+ * @returns `true` if the table is classified as `"rebase-internal"`.
2311
+ */
2312
+ function isRebaseInternalTable(tableName, schemaName) {
2313
+ return classifyTable(tableName, schemaName) === "rebase-internal";
2314
+ }
2315
+ /** SQL query that detects junction tables in the `public` schema. */
2316
+ var JUNCTION_TABLES_SQL = `
2317
+ SELECT t.table_name
2318
+ FROM information_schema.tables t
2319
+ WHERE t.table_schema = 'public'
2320
+ AND t.table_type = 'BASE TABLE'
2321
+ AND NOT EXISTS (
2322
+ SELECT 1
2323
+ FROM information_schema.columns c
2324
+ WHERE c.table_schema = t.table_schema
2325
+ AND c.table_name = t.table_name
2326
+ AND c.column_name NOT IN (
2327
+ SELECT kcu.column_name
2328
+ FROM information_schema.key_column_usage kcu
2329
+ JOIN information_schema.table_constraints tc
2330
+ ON tc.constraint_name = kcu.constraint_name
2331
+ AND tc.table_schema = kcu.table_schema
2332
+ WHERE tc.constraint_type = 'FOREIGN KEY'
2333
+ AND kcu.table_schema = t.table_schema
2334
+ AND kcu.table_name = t.table_name
2335
+ )
2336
+ )
2337
+ `;
2338
+ /**
2339
+ * Asynchronously detect junction (link) tables in the `public` schema.
2340
+ *
2341
+ * A junction table is defined as a table where **every** column participates in
2342
+ * at least one foreign-key constraint.
2343
+ *
2344
+ * @param executeSql - A callback that executes a raw SQL string and returns the
2345
+ * resulting rows.
2346
+ * @returns A `Set` containing the names of all detected junction tables.
2347
+ */
2348
+ async function detectJunctionTables(executeSql) {
2349
+ const rows = await executeSql(JUNCTION_TABLES_SQL);
2350
+ const junctionTables = /* @__PURE__ */ new Set();
2351
+ for (const row of rows) if (typeof row.table_name === "string") junctionTables.add(row.table_name);
2352
+ return junctionTables;
2353
+ }
2354
+ //#endregion
2355
+ export { COLLECTION_PATH_SEPARATOR, CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, JUNCTION_TABLES_SQL, QueryBuilder, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, addInitialSlash, and, applyPropertyConditions, buildAdditionalFieldDelegate, buildCollection, buildConditionContext, buildEntityCallbacks, buildEnum, buildEnumValueConfig, buildProperties, buildPropertiesOrBuilder, buildProperty, buildPropertyCallbacks, buildRebaseData, buildRoutedRebaseData, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, checkOperation, classifyTable, cond, createDataSourceRegistry, createRelationRef, createRelationRefWithData, defaultUsersCollection, detectJunctionTables, enumToObjectEntries, evaluateCondition, findRelation, fullPathToCollectionSegments, getArrayResolvedProperties, getCollectionBySlugWithin, getCollectionPathsCombinations, getColumnName, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEntityImagePreviewPropertyKey, getEnumVarName, getLabelOrConfigFrom, getLastSegment, getLocalChangesBackup, getNavigationEntriesFromPath, getParentReferencesFromPath, getPrimaryKeys, getReferenceFrom, getRelationFrom, getSubcollections, getTableName, getTableVarName, isHidden, isPropertyBuilder, isReadOnly, isRebaseInternalTable, normalizeToEntityRelation, or, registerConditionOperations, removeInitialAndTrailingSlashes, removeInitialSlash, removeTrailingSlash, resolveArrayProperties, resolveCollectionPathIds, resolveCollectionRelations, resolveDataSource, resolveDefaultSelectedView, resolveEnumValues, resolveProperties, resolveProperty, resolvePropertyEnum, resolvePropertyRelation, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, sanitizeData, sanitizeRelation, segmentsToStrippedPath, sortProperties, stripCollectionPath, traverseValueProperty, traverseValuesProperties, updateDateAutoValues };
2173
2356
 
2174
2357
  //# sourceMappingURL=index.es.js.map