@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.
- package/dist/collections/CollectionRegistry.d.ts +14 -1
- package/dist/data/buildRoutedRebaseData.d.ts +53 -0
- package/dist/data/resolveDataSource.d.ts +43 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.es.js +186 -3
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +193 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/table-classification.d.ts +47 -0
- package/package.json +13 -13
- package/src/collections/CollectionRegistry.ts +34 -1
- package/src/data/buildRoutedRebaseData.ts +97 -0
- package/src/data/resolveDataSource.ts +79 -0
- package/src/index.ts +3 -1
- package/src/table-classification.ts +109 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table Classification
|
|
3
|
+
*
|
|
4
|
+
* Shared constants and pure functions for classifying database tables.
|
|
5
|
+
* Used by both the server-side PostgresBackendDriver and the Studio RLS editor.
|
|
6
|
+
*/
|
|
7
|
+
/** Possible categories a database table can belong to. */
|
|
8
|
+
export type TableCategory = "rebase-internal" | "junction" | "user";
|
|
9
|
+
/** Schemas that are always considered Rebase-internal. */
|
|
10
|
+
export declare const REBASE_INTERNAL_SCHEMAS: readonly string[];
|
|
11
|
+
/** Table-name prefixes that mark a table as Rebase-internal regardless of schema. */
|
|
12
|
+
export declare const REBASE_INTERNAL_PREFIXES: readonly string[];
|
|
13
|
+
/**
|
|
14
|
+
* Synchronously classify a table based on naming conventions.
|
|
15
|
+
*
|
|
16
|
+
* @param tableName - The unqualified name of the table.
|
|
17
|
+
* @param schemaName - The schema the table belongs to (e.g. `"public"`, `"rebase"`).
|
|
18
|
+
* @returns `"rebase-internal"` when the table belongs to a reserved schema or
|
|
19
|
+
* carries a reserved prefix; `"user"` otherwise.
|
|
20
|
+
*
|
|
21
|
+
* @remarks
|
|
22
|
+
* Junction-table detection requires an async database query and is therefore
|
|
23
|
+
* **not** handled by this function. Use {@link detectJunctionTables} to obtain
|
|
24
|
+
* the set of junction tables, then reclassify as needed.
|
|
25
|
+
*/
|
|
26
|
+
export declare function classifyTable(tableName: string, schemaName: string): TableCategory;
|
|
27
|
+
/**
|
|
28
|
+
* Convenience predicate that checks whether a table is Rebase-internal.
|
|
29
|
+
*
|
|
30
|
+
* @param tableName - The unqualified name of the table.
|
|
31
|
+
* @param schemaName - The schema the table belongs to.
|
|
32
|
+
* @returns `true` if the table is classified as `"rebase-internal"`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isRebaseInternalTable(tableName: string, schemaName: string): boolean;
|
|
35
|
+
/** SQL query that detects junction tables in the `public` schema. */
|
|
36
|
+
export declare const JUNCTION_TABLES_SQL = "\n SELECT t.table_name\n FROM information_schema.tables t\n WHERE t.table_schema = 'public'\n AND t.table_type = 'BASE TABLE'\n AND NOT EXISTS (\n SELECT 1\n FROM information_schema.columns c\n WHERE c.table_schema = t.table_schema\n AND c.table_name = t.table_name\n AND c.column_name NOT IN (\n SELECT kcu.column_name\n FROM information_schema.key_column_usage kcu\n JOIN information_schema.table_constraints tc\n ON tc.constraint_name = kcu.constraint_name\n AND tc.table_schema = kcu.table_schema\n WHERE tc.constraint_type = 'FOREIGN KEY'\n AND kcu.table_schema = t.table_schema\n AND kcu.table_name = t.table_name\n )\n )\n";
|
|
37
|
+
/**
|
|
38
|
+
* Asynchronously detect junction (link) tables in the `public` schema.
|
|
39
|
+
*
|
|
40
|
+
* A junction table is defined as a table where **every** column participates in
|
|
41
|
+
* at least one foreign-key constraint.
|
|
42
|
+
*
|
|
43
|
+
* @param executeSql - A callback that executes a raw SQL string and returns the
|
|
44
|
+
* resulting rows.
|
|
45
|
+
* @returns A `Set` containing the names of all detected junction tables.
|
|
46
|
+
*/
|
|
47
|
+
export declare function detectJunctionTables(executeSql: (sql: string) => Promise<Record<string, unknown>[]>): Promise<Set<string>>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/common",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.7.0",
|
|
5
5
|
"description": "Awesome Firebase/Firestore-based headless open-source CMS",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -29,6 +29,14 @@
|
|
|
29
29
|
"headless cms",
|
|
30
30
|
"content manager"
|
|
31
31
|
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"watch": "vite build --watch",
|
|
34
|
+
"build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json",
|
|
35
|
+
"test:lint": "eslint \"src/**\" --quiet",
|
|
36
|
+
"test": "jest --passWithNoTests",
|
|
37
|
+
"clean": "rm -rf dist && find ./src -name '*.js' -type f | xargs rm -f",
|
|
38
|
+
"generateIcons": "ts-node --esm src/icons/generateIcons.ts"
|
|
39
|
+
},
|
|
32
40
|
"exports": {
|
|
33
41
|
".": {
|
|
34
42
|
"types": "./dist/index.d.ts",
|
|
@@ -39,10 +47,10 @@
|
|
|
39
47
|
"./package.json": "./package.json"
|
|
40
48
|
},
|
|
41
49
|
"dependencies": {
|
|
50
|
+
"@rebasepro/types": "workspace:*",
|
|
51
|
+
"@rebasepro/utils": "workspace:*",
|
|
42
52
|
"fast-equals": "6.0.0",
|
|
43
|
-
"json-logic-js": "^2.0.5"
|
|
44
|
-
"@rebasepro/types": "0.6.1",
|
|
45
|
-
"@rebasepro/utils": "0.6.1"
|
|
53
|
+
"json-logic-js": "^2.0.5"
|
|
46
54
|
},
|
|
47
55
|
"devDependencies": {
|
|
48
56
|
"@jest/globals": "^30.4.1",
|
|
@@ -97,13 +105,5 @@
|
|
|
97
105
|
"^@rebasepro/types$": "<rootDir>/../types/src/index.ts",
|
|
98
106
|
"^@rebasepro/utils$": "<rootDir>/../utils/src/index.ts"
|
|
99
107
|
}
|
|
100
|
-
},
|
|
101
|
-
"scripts": {
|
|
102
|
-
"watch": "vite build --watch",
|
|
103
|
-
"build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json",
|
|
104
|
-
"test:lint": "eslint \"src/**\" --quiet",
|
|
105
|
-
"test": "jest --passWithNoTests",
|
|
106
|
-
"clean": "rm -rf dist && find ./src -name '*.js' -type f | xargs rm -f",
|
|
107
|
-
"generateIcons": "ts-node --esm src/icons/generateIcons.ts"
|
|
108
108
|
}
|
|
109
|
-
}
|
|
109
|
+
}
|
|
@@ -15,9 +15,17 @@ import { deepEqual } from "fast-equals";
|
|
|
15
15
|
|
|
16
16
|
import { enumToObjectEntries, getSubcollections, getTableName, resolveCollectionRelations, findRelation, sanitizeRelation } from "../util";
|
|
17
17
|
import { removeFunctions, mergeDeep, deepClone } from "@rebasepro/utils";
|
|
18
|
+
import { resolveDataSource, DataSourceRegistry } from "../data/resolveDataSource";
|
|
18
19
|
|
|
19
20
|
export class CollectionRegistry {
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Declared data sources, used during normalization to resolve each
|
|
24
|
+
* collection's engine (so `dataSource`-only collections get the right
|
|
25
|
+
* capabilities). Empty by default → behaviour keys off `driver` as before.
|
|
26
|
+
*/
|
|
27
|
+
private dataSources: DataSourceRegistry = {};
|
|
28
|
+
|
|
21
29
|
// Normalized runtime layer (used by Data Grid / UI)
|
|
22
30
|
private collectionsByTableName = new Map<string, EntityCollection>();
|
|
23
31
|
private collectionsBySlug = new Map<string, EntityCollection>();
|
|
@@ -34,12 +42,24 @@ export class CollectionRegistry {
|
|
|
34
42
|
// to avoid the issue where normalization creates new objects that always fail equality.
|
|
35
43
|
private lastRawInputSnapshot: ReturnType<typeof removeFunctions>[] | null = null;
|
|
36
44
|
|
|
37
|
-
constructor(collections?: EntityCollection[]) {
|
|
45
|
+
constructor(collections?: EntityCollection[], dataSources?: DataSourceRegistry) {
|
|
46
|
+
if (dataSources) this.dataSources = dataSources;
|
|
38
47
|
if (collections) {
|
|
39
48
|
this.registerMultiple(collections);
|
|
40
49
|
}
|
|
41
50
|
}
|
|
42
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Provide the declared data sources used to resolve each collection's
|
|
54
|
+
* engine during normalization. Set this before registering collections.
|
|
55
|
+
* Returns true if the registry changed (callers may re-register).
|
|
56
|
+
*/
|
|
57
|
+
setDataSources(dataSources: DataSourceRegistry): boolean {
|
|
58
|
+
if (deepEqual(this.dataSources, dataSources)) return false;
|
|
59
|
+
this.dataSources = dataSources ?? {};
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
43
63
|
reset() {
|
|
44
64
|
this.collectionsByTableName.clear();
|
|
45
65
|
this.collectionsBySlug.clear();
|
|
@@ -163,6 +183,19 @@ export class CollectionRegistry {
|
|
|
163
183
|
// and for preventing mutation of module-level collection singletons.
|
|
164
184
|
const result = { ...collection } as EntityCollection;
|
|
165
185
|
|
|
186
|
+
// 0. For `dataSource`-only collections (no explicit `driver`), resolve
|
|
187
|
+
// the engine from the data-source registry and stamp it as `driver`
|
|
188
|
+
// on the normalized copy, so downstream capability lookups (which
|
|
189
|
+
// read `driver`) are correct. Surgical on purpose: collections that
|
|
190
|
+
// already set `driver`, and plain default collections (no driver,
|
|
191
|
+
// no dataSource), are left untouched. Only the normalized layer is
|
|
192
|
+
// affected — the raw layer used by the collection editor keeps the
|
|
193
|
+
// author's original fields.
|
|
194
|
+
if (result.dataSource && !result.driver) {
|
|
195
|
+
const engine = resolveDataSource(result, this.dataSources).engine;
|
|
196
|
+
if (engine) (result as { driver?: string }).driver = engine;
|
|
197
|
+
}
|
|
198
|
+
|
|
166
199
|
// 1. Extract relations from properties that have inline config (target set)
|
|
167
200
|
const extractedRelations = this.extractRelationsFromProperties(result.properties);
|
|
168
201
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { RebaseData, CollectionAccessor } from "@rebasepro/types";
|
|
2
|
+
import { toSnakeCase } from "@rebasepro/utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parameters for {@link buildRoutedRebaseData}.
|
|
6
|
+
*/
|
|
7
|
+
export interface RoutedRebaseDataParams {
|
|
8
|
+
/**
|
|
9
|
+
* The default data source. Handles every collection that does not
|
|
10
|
+
* resolve to an entry in `sources` (i.e. server-transport collections,
|
|
11
|
+
* which ride the Rebase client).
|
|
12
|
+
*/
|
|
13
|
+
defaultData: RebaseData;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Per-data-source {@link RebaseData} instances for direct and custom
|
|
17
|
+
* transports, keyed by data-source key (e.g. `"analytics"`). Server-
|
|
18
|
+
* mediated sources are not listed here — they fall through to
|
|
19
|
+
* `defaultData`.
|
|
20
|
+
*/
|
|
21
|
+
sources: Record<string, RebaseData>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the data-source key for a given collection slug or path.
|
|
25
|
+
* Typically backed by the collection registry + `resolveDataSource`
|
|
26
|
+
* (`resolveDataSource(registry.getCollection(path), defs).key`).
|
|
27
|
+
*
|
|
28
|
+
* Return `undefined` (or a key absent from `sources`) to route to the
|
|
29
|
+
* default data source.
|
|
30
|
+
*/
|
|
31
|
+
resolveKey: (slugOrPath: string) => string | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build a {@link RebaseData} that routes each collection to the right
|
|
36
|
+
* backend based on its resolved data source.
|
|
37
|
+
*
|
|
38
|
+
* `.collection(path)` (and dynamic `data.products`-style access) resolves the
|
|
39
|
+
* collection's data-source key via `resolveKey` and delegates to the matching
|
|
40
|
+
* entry in `sources`, falling back to `defaultData` when there is no match.
|
|
41
|
+
* Because routing keys off the *path being accessed*, a reference widget
|
|
42
|
+
* inside a Firestore form that points at a Postgres collection is still
|
|
43
|
+
* served by Postgres — routing follows the target, not the ancestor.
|
|
44
|
+
*
|
|
45
|
+
* When `sources` is empty this returns `defaultData` untouched, so the
|
|
46
|
+
* single-driver setup keeps identical behaviour and identity (important for
|
|
47
|
+
* effect dependencies that key off the data instance).
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* const data = buildRoutedRebaseData({
|
|
51
|
+
* defaultData: client.data,
|
|
52
|
+
* sources: { analytics: buildRebaseData(firestoreDriver) },
|
|
53
|
+
* resolveKey: (path) => resolveDataSource(registry.getCollection(path), defs).key
|
|
54
|
+
* });
|
|
55
|
+
* await data.products.find(); // → default (server / Postgres)
|
|
56
|
+
* await data.events.find(); // → Firestore, if `events.dataSource === "analytics"`
|
|
57
|
+
*/
|
|
58
|
+
export function buildRoutedRebaseData({
|
|
59
|
+
defaultData,
|
|
60
|
+
sources,
|
|
61
|
+
resolveKey
|
|
62
|
+
}: RoutedRebaseDataParams): RebaseData {
|
|
63
|
+
|
|
64
|
+
// Fast path: nothing to route → return the default untouched (preserves
|
|
65
|
+
// referential identity for effect dependencies).
|
|
66
|
+
if (!sources || Object.keys(sources).length === 0) {
|
|
67
|
+
return defaultData;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resolve(slugOrPath: string): RebaseData {
|
|
71
|
+
const key = resolveKey(slugOrPath);
|
|
72
|
+
if (key && sources[key]) return sources[key];
|
|
73
|
+
return defaultData;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getAccessor(slugOrPath: string): CollectionAccessor {
|
|
77
|
+
return resolve(slugOrPath).collection(slugOrPath);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const target = {
|
|
81
|
+
collection: getAccessor
|
|
82
|
+
} as RebaseData;
|
|
83
|
+
|
|
84
|
+
return new Proxy(target, {
|
|
85
|
+
get(_target, prop: string | symbol) {
|
|
86
|
+
if (prop === "collection") return getAccessor;
|
|
87
|
+
// Ignore Symbol properties (e.g. Symbol.toPrimitive, Symbol.iterator)
|
|
88
|
+
if (typeof prop === "symbol") return undefined;
|
|
89
|
+
// Ignore internal JS properties
|
|
90
|
+
if (prop === "then" || prop === "toJSON" || prop === "$$typeof") return undefined;
|
|
91
|
+
|
|
92
|
+
// Convert camelCase property names to snake_case slugs, mirroring
|
|
93
|
+
// buildRebaseData so dynamic access routes consistently.
|
|
94
|
+
return getAccessor(toSnakeCase(prop));
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DataSourceDefinition,
|
|
3
|
+
ResolvedDataSource,
|
|
4
|
+
DEFAULT_DATA_SOURCE_KEY,
|
|
5
|
+
getDataSourceCapabilities
|
|
6
|
+
} from "@rebasepro/types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The subset of a collection needed to resolve its data source. Accepting a
|
|
10
|
+
* structural type (rather than the full `EntityCollection`) keeps this usable
|
|
11
|
+
* from anywhere — frontend router, backend registry, editor — without coupling
|
|
12
|
+
* to the collection union.
|
|
13
|
+
*/
|
|
14
|
+
export interface DataSourceResolvable {
|
|
15
|
+
/** Preferred routing key. */
|
|
16
|
+
dataSource?: string;
|
|
17
|
+
/** Legacy engine hint / fallback routing key. */
|
|
18
|
+
driver?: string;
|
|
19
|
+
/** Within-engine instance. */
|
|
20
|
+
databaseId?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A lookup of data-source definitions by key. */
|
|
24
|
+
export type DataSourceRegistry = Record<string, DataSourceDefinition>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build a keyed registry from a list of {@link DataSourceDefinition}s.
|
|
28
|
+
* Later entries win on key collision.
|
|
29
|
+
*/
|
|
30
|
+
export function createDataSourceRegistry(definitions?: DataSourceDefinition[]): DataSourceRegistry {
|
|
31
|
+
const registry: DataSourceRegistry = {};
|
|
32
|
+
for (const def of definitions ?? []) {
|
|
33
|
+
registry[def.key] = def;
|
|
34
|
+
}
|
|
35
|
+
return registry;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the effective data source for a collection — the single source of
|
|
40
|
+
* truth shared by the frontend router, the backend driver registry, and the
|
|
41
|
+
* editor's capability lookups.
|
|
42
|
+
*
|
|
43
|
+
* Resolution order:
|
|
44
|
+
* 1. The routing **key** is `collection.dataSource`, else the legacy
|
|
45
|
+
* `collection.driver`, else {@link DEFAULT_DATA_SOURCE_KEY}.
|
|
46
|
+
* 2. If a definition is registered for that key, it provides `engine`,
|
|
47
|
+
* `transport`, and `databaseId`.
|
|
48
|
+
* 3. Otherwise values are synthesized for backward compatibility: `engine`
|
|
49
|
+
* from the legacy `driver` (or the key, or `"postgres"`), `transport`
|
|
50
|
+
* defaults to `"server"`, and `databaseId` from the collection.
|
|
51
|
+
*
|
|
52
|
+
* `capabilities` are always derived from the resolved `engine`, so two
|
|
53
|
+
* data sources sharing an engine share capabilities.
|
|
54
|
+
*
|
|
55
|
+
* @param collection the collection (or any object carrying the routing fields)
|
|
56
|
+
* @param registry optional registry of declared data sources
|
|
57
|
+
*/
|
|
58
|
+
export function resolveDataSource(
|
|
59
|
+
collection: DataSourceResolvable | undefined,
|
|
60
|
+
registry?: DataSourceRegistry
|
|
61
|
+
): ResolvedDataSource {
|
|
62
|
+
const key = collection?.dataSource ?? collection?.driver ?? DEFAULT_DATA_SOURCE_KEY;
|
|
63
|
+
const def = registry?.[key];
|
|
64
|
+
|
|
65
|
+
const engine = def?.engine
|
|
66
|
+
?? collection?.driver
|
|
67
|
+
?? (key !== DEFAULT_DATA_SOURCE_KEY ? key : "postgres");
|
|
68
|
+
|
|
69
|
+
const transport = def?.transport ?? "server";
|
|
70
|
+
const databaseId = collection?.databaseId ?? def?.databaseId;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
key,
|
|
74
|
+
engine,
|
|
75
|
+
transport,
|
|
76
|
+
databaseId,
|
|
77
|
+
capabilities: getDataSourceCapabilities(engine)
|
|
78
|
+
};
|
|
79
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +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";
|
|
5
|
-
|
|
7
|
+
export * from "./table-classification";
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table Classification
|
|
3
|
+
*
|
|
4
|
+
* Shared constants and pure functions for classifying database tables.
|
|
5
|
+
* Used by both the server-side PostgresBackendDriver and the Studio RLS editor.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Possible categories a database table can belong to. */
|
|
9
|
+
export type TableCategory = "rebase-internal" | "junction" | "user";
|
|
10
|
+
|
|
11
|
+
/** Schemas that are always considered Rebase-internal. */
|
|
12
|
+
export const REBASE_INTERNAL_SCHEMAS: readonly string[] = ["rebase", "auth"];
|
|
13
|
+
|
|
14
|
+
/** Table-name prefixes that mark a table as Rebase-internal regardless of schema. */
|
|
15
|
+
export const REBASE_INTERNAL_PREFIXES: readonly string[] = [
|
|
16
|
+
"_rebase_",
|
|
17
|
+
"_auth_",
|
|
18
|
+
"drizzle_",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Synchronously classify a table based on naming conventions.
|
|
23
|
+
*
|
|
24
|
+
* @param tableName - The unqualified name of the table.
|
|
25
|
+
* @param schemaName - The schema the table belongs to (e.g. `"public"`, `"rebase"`).
|
|
26
|
+
* @returns `"rebase-internal"` when the table belongs to a reserved schema or
|
|
27
|
+
* carries a reserved prefix; `"user"` otherwise.
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* Junction-table detection requires an async database query and is therefore
|
|
31
|
+
* **not** handled by this function. Use {@link detectJunctionTables} to obtain
|
|
32
|
+
* the set of junction tables, then reclassify as needed.
|
|
33
|
+
*/
|
|
34
|
+
export function classifyTable(
|
|
35
|
+
tableName: string,
|
|
36
|
+
schemaName: string,
|
|
37
|
+
): TableCategory {
|
|
38
|
+
if (
|
|
39
|
+
REBASE_INTERNAL_SCHEMAS.includes(schemaName) ||
|
|
40
|
+
REBASE_INTERNAL_PREFIXES.some((prefix) => tableName.startsWith(prefix))
|
|
41
|
+
) {
|
|
42
|
+
return "rebase-internal";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return "user";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Convenience predicate that checks whether a table is Rebase-internal.
|
|
50
|
+
*
|
|
51
|
+
* @param tableName - The unqualified name of the table.
|
|
52
|
+
* @param schemaName - The schema the table belongs to.
|
|
53
|
+
* @returns `true` if the table is classified as `"rebase-internal"`.
|
|
54
|
+
*/
|
|
55
|
+
export function isRebaseInternalTable(
|
|
56
|
+
tableName: string,
|
|
57
|
+
schemaName: string,
|
|
58
|
+
): boolean {
|
|
59
|
+
return classifyTable(tableName, schemaName) === "rebase-internal";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** SQL query that detects junction tables in the `public` schema. */
|
|
63
|
+
export const JUNCTION_TABLES_SQL = `
|
|
64
|
+
SELECT t.table_name
|
|
65
|
+
FROM information_schema.tables t
|
|
66
|
+
WHERE t.table_schema = 'public'
|
|
67
|
+
AND t.table_type = 'BASE TABLE'
|
|
68
|
+
AND NOT EXISTS (
|
|
69
|
+
SELECT 1
|
|
70
|
+
FROM information_schema.columns c
|
|
71
|
+
WHERE c.table_schema = t.table_schema
|
|
72
|
+
AND c.table_name = t.table_name
|
|
73
|
+
AND c.column_name NOT IN (
|
|
74
|
+
SELECT kcu.column_name
|
|
75
|
+
FROM information_schema.key_column_usage kcu
|
|
76
|
+
JOIN information_schema.table_constraints tc
|
|
77
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
78
|
+
AND tc.table_schema = kcu.table_schema
|
|
79
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
80
|
+
AND kcu.table_schema = t.table_schema
|
|
81
|
+
AND kcu.table_name = t.table_name
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Asynchronously detect junction (link) tables in the `public` schema.
|
|
88
|
+
*
|
|
89
|
+
* A junction table is defined as a table where **every** column participates in
|
|
90
|
+
* at least one foreign-key constraint.
|
|
91
|
+
*
|
|
92
|
+
* @param executeSql - A callback that executes a raw SQL string and returns the
|
|
93
|
+
* resulting rows.
|
|
94
|
+
* @returns A `Set` containing the names of all detected junction tables.
|
|
95
|
+
*/
|
|
96
|
+
export async function detectJunctionTables(
|
|
97
|
+
executeSql: (sql: string) => Promise<Record<string, unknown>[]>,
|
|
98
|
+
): Promise<Set<string>> {
|
|
99
|
+
const rows = await executeSql(JUNCTION_TABLES_SQL);
|
|
100
|
+
const junctionTables = new Set<string>();
|
|
101
|
+
|
|
102
|
+
for (const row of rows) {
|
|
103
|
+
if (typeof row.table_name === "string") {
|
|
104
|
+
junctionTables.add(row.table_name);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return junctionTables;
|
|
109
|
+
}
|