@rebasepro/common 0.16.0 → 0.16.1-canary.g0d7af95
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 +1 -1
- package/dist/collections/index.d.ts +2 -2
- package/dist/index.d.ts +11 -11
- package/dist/index.es.js +50 -1
- package/dist/index.es.js.map +1 -1
- package/dist/util/index.d.ts +21 -21
- package/dist/util/internal-tables.d.ts +9 -0
- package/dist/util/internal-tables.test.d.ts +1 -0
- package/dist/util/policy/index.d.ts +4 -4
- package/dist/util/relations.d.ts +25 -0
- package/package.json +4 -4
- package/src/data/buildRebaseData.ts +9 -0
- package/src/util/internal-tables.test.ts +176 -0
- package/src/util/internal-tables.ts +11 -0
- package/src/util/relations.ts +48 -1
package/dist/util/index.d.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
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";
|
|
1
|
+
export * from "./collections.js";
|
|
2
|
+
export * from "./common.js";
|
|
3
|
+
export * from "./entities.js";
|
|
4
|
+
export * from "./identity.js";
|
|
5
|
+
export * from "./email.js";
|
|
6
|
+
export * from "./enums.js";
|
|
7
|
+
export * from "./paths.js";
|
|
8
|
+
export * from "./resolutions.js";
|
|
9
|
+
export * from "./policy/index.js";
|
|
10
|
+
export * from "./permissions.js";
|
|
11
|
+
export * from "./builders.js";
|
|
12
|
+
export * from "./storage.js";
|
|
13
|
+
export * from "./callbacks.js";
|
|
14
|
+
export * from "./relations.js";
|
|
15
|
+
export * from "./resolve-relation.js";
|
|
16
|
+
export * from "./auth-default-policies.js";
|
|
17
|
+
export * from "./junction-policies.js";
|
|
18
|
+
export * from "./conditions.js";
|
|
19
|
+
export * from "./pg-column-to-property.js";
|
|
20
|
+
export * from "./string-column-length.js";
|
|
21
|
+
export * from "./internal-tables.js";
|
|
@@ -62,6 +62,15 @@ export declare const REBASE_USER_ROLE = "rebase_user";
|
|
|
62
62
|
*
|
|
63
63
|
* `atlas_schema_revisions` is Atlas's migration ledger, which lands in `rebase`
|
|
64
64
|
* because `db migrate apply` passes `--revisions-schema rebase`.
|
|
65
|
+
*
|
|
66
|
+
* Every entry here must also be revoked by whatever creates it, and vice versa:
|
|
67
|
+
* the creation-time revoke fires once, on the boot that first makes the table,
|
|
68
|
+
* so it cannot help a database provisioned before that revoke existed. This
|
|
69
|
+
* list is what the boot-time sweep in `ensureAppRole` iterates, and the sweep
|
|
70
|
+
* is the only thing that can repair an already-granted table. A table revoked
|
|
71
|
+
* at creation but missing here is therefore permanently stranded on any
|
|
72
|
+
* database that predates its revoke — which is exactly what happened to
|
|
73
|
+
* `jobs`, added here after a scan of a pre-0.14 database found it.
|
|
65
74
|
*/
|
|
66
75
|
export declare const REBASE_INTERNAL_TABLES: readonly string[];
|
|
67
76
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export * from "./securityRuleToConditions";
|
|
2
|
-
export * from "./sqlToPolicy";
|
|
3
|
-
export * from "./policyToPostgres";
|
|
4
|
-
export * from "./evaluatePolicy";
|
|
1
|
+
export * from "./securityRuleToConditions.js";
|
|
2
|
+
export * from "./sqlToPolicy.js";
|
|
3
|
+
export * from "./policyToPostgres.js";
|
|
4
|
+
export * from "./evaluatePolicy.js";
|
package/dist/util/relations.d.ts
CHANGED
|
@@ -45,6 +45,31 @@ export declare function resolveCollectionRelations(collection: CollectionConfig)
|
|
|
45
45
|
*/
|
|
46
46
|
export declare function getRelationTargetPath(property: RelationProperty): string | undefined;
|
|
47
47
|
export declare function getTableName(collection: CollectionConfig): string;
|
|
48
|
+
/**
|
|
49
|
+
* The variable name a generated table is bound to.
|
|
50
|
+
*
|
|
51
|
+
* Camel-cases underscores, and then guarantees the result is a legal
|
|
52
|
+
* identifier. It did only the first, so a table name that is legal in Postgres
|
|
53
|
+
* and not in JavaScript produced a `schema.generated.ts` that does not parse:
|
|
54
|
+
*
|
|
55
|
+
* `2024_archive` → `export const 2024Archive = pgTable(…)`
|
|
56
|
+
* "An identifier or keyword cannot immediately follow
|
|
57
|
+
* a numeric literal"
|
|
58
|
+
* `reporting.events` → `export const reporting.events = pgTable(…)`
|
|
59
|
+
* "',' expected"
|
|
60
|
+
*
|
|
61
|
+
* That file is imported by the server, so the failure is not one broken
|
|
62
|
+
* collection — `rebase build` and `db push` fail at tsc for the whole
|
|
63
|
+
* directory. And it is reachable from a documented flow: `rebase init` against
|
|
64
|
+
* a database holding a table called `2024_archive` writes a collection file
|
|
65
|
+
* that parses and a schema file that does not.
|
|
66
|
+
*
|
|
67
|
+
* **A no-op for every name that already worked**, which is what makes changing
|
|
68
|
+
* a derived name safe here: the only inputs whose output changes are the ones
|
|
69
|
+
* that produced a syntax error, and nothing can be running against those.
|
|
70
|
+
* Separators become camel case rather than disappearing, so `reporting.events`
|
|
71
|
+
* and `reporting_events` do not collide into one variable.
|
|
72
|
+
*/
|
|
48
73
|
export declare function getTableVarName(tableName: string): string;
|
|
49
74
|
export declare function getEnumVarName(tableName: string, propName: string): string;
|
|
50
75
|
export declare function getColumnName(fullColumn: string): string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/common",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.16.
|
|
4
|
+
"version": "0.16.1-canary.g0d7af95",
|
|
5
5
|
"description": "Rebase shared core — collection registry, data driver adapter and fluent query builder. No React dependency.",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"fast-equals": "6.0.2",
|
|
42
42
|
"json-logic-js": "^2.0.5",
|
|
43
|
-
"@rebasepro/
|
|
44
|
-
"@rebasepro/
|
|
43
|
+
"@rebasepro/types": "0.16.1-canary.g0d7af95",
|
|
44
|
+
"@rebasepro/utils": "0.16.1-canary.g0d7af95"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@jest/globals": "^30.4.1",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
},
|
|
99
99
|
"scripts": {
|
|
100
100
|
"watch": "vite build --watch",
|
|
101
|
-
"build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json && node ../../scripts/assert-build-output.mjs",
|
|
101
|
+
"build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json && node ../../tooling/scripts/add-dts-extensions.mjs dist && node ../../tooling/scripts/assert-build-output.mjs",
|
|
102
102
|
"test:lint": "eslint \"src/**\" --quiet",
|
|
103
103
|
"test": "jest --passWithNoTests",
|
|
104
104
|
"clean": "rm -rf dist && find ./src -name '*.js' -type f | xargs rm -f",
|
|
@@ -333,6 +333,15 @@ ids });
|
|
|
333
333
|
orderBy: normalizeOrderBy(params?.orderBy),
|
|
334
334
|
searchString: params?.searchString,
|
|
335
335
|
searchExplain: params?.searchExplain,
|
|
336
|
+
// Forwarded so the SERVER can refuse it. `realtimeService`
|
|
337
|
+
// rejects a subscription carrying `vectorSearch` — a
|
|
338
|
+
// subscription is re-run on every matching write and
|
|
339
|
+
// nothing there computes distances — and the docs promise
|
|
340
|
+
// that refusal. Both producers hand-list their fields and
|
|
341
|
+
// both omitted this one, so the guard could not fire and
|
|
342
|
+
// `.vectorSearch(…).listen()` returned an ordinary
|
|
343
|
+
// `id DESC` listing with no `_distance` and no error.
|
|
344
|
+
vectorSearch: params?.vectorSearch,
|
|
336
345
|
onUpdate: (entities) => {
|
|
337
346
|
onUpdate({
|
|
338
347
|
data: entities.map((row: Record<string, unknown>) => rowToEntity<M>(normalize(row), slug, getPks())),
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import {
|
|
2
|
+
REBASE_INTERNAL_TABLES,
|
|
3
|
+
REBASE_USER_ROLE,
|
|
4
|
+
revokeInternalTableAccess,
|
|
5
|
+
revokeInternalTableSql
|
|
6
|
+
} from "./internal-tables";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The privilege boundary around Rebase's own tables.
|
|
10
|
+
*
|
|
11
|
+
* This module is the only thing keeping `rebase_user` away from refresh-token
|
|
12
|
+
* hashes, TOTP secrets and API keys, and until now nothing tested it. Its
|
|
13
|
+
* failure mode is the quiet kind: `revokeInternalTableAccess` swallows every
|
|
14
|
+
* error into an `onError` callback, so a revoke that fails on all twenty tables
|
|
15
|
+
* produces a boot that looks completely normal.
|
|
16
|
+
*
|
|
17
|
+
* The list membership assertions matter as much as the SQL ones. A table
|
|
18
|
+
* revoked at creation but missing from `REBASE_INTERNAL_TABLES` is stranded on
|
|
19
|
+
* every database provisioned before that revoke shipped — the boot-time sweep
|
|
20
|
+
* in `ensureAppRole` iterates this list and nothing else can repair an
|
|
21
|
+
* already-granted table.
|
|
22
|
+
*/
|
|
23
|
+
describe("internal table access", () => {
|
|
24
|
+
|
|
25
|
+
// ─── The list itself ─────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
describe("REBASE_INTERNAL_TABLES", () => {
|
|
28
|
+
|
|
29
|
+
it("covers every table a creator revokes at creation time", () => {
|
|
30
|
+
// Kept in sync by hand with the `revokeInternalTableSql` call sites.
|
|
31
|
+
// A creation-time revoke fires once, on the boot that first makes
|
|
32
|
+
// the table; only the sweep can fix a database that predates it.
|
|
33
|
+
const revokedAtCreation = [
|
|
34
|
+
"api_keys",
|
|
35
|
+
"branches",
|
|
36
|
+
"channel_cursors",
|
|
37
|
+
"channel_messages",
|
|
38
|
+
"channel_presence",
|
|
39
|
+
"cron_claims",
|
|
40
|
+
"cron_logs",
|
|
41
|
+
"entity_history",
|
|
42
|
+
"idempotency_keys",
|
|
43
|
+
"jobs",
|
|
44
|
+
"rate_limit_hits"
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const missing = revokedAtCreation.filter((table) => !REBASE_INTERNAL_TABLES.includes(table));
|
|
48
|
+
expect(missing).toEqual([]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("holds the auth tables whose rows no end user may ever address", () => {
|
|
52
|
+
for (const table of [
|
|
53
|
+
"refresh_tokens",
|
|
54
|
+
"password_reset_tokens",
|
|
55
|
+
"magic_link_tokens",
|
|
56
|
+
"mfa_factors",
|
|
57
|
+
"mfa_challenges",
|
|
58
|
+
"recovery_codes",
|
|
59
|
+
"user_identities"
|
|
60
|
+
]) {
|
|
61
|
+
expect(REBASE_INTERNAL_TABLES).toContain(table);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("leaves `users` out, because revoking there breaks sign-in", () => {
|
|
66
|
+
// The auth user table is also a collection: RLS enabled, policies
|
|
67
|
+
// applied, and users read their own row through it.
|
|
68
|
+
expect(REBASE_INTERNAL_TABLES).not.toContain("users");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("has no duplicates", () => {
|
|
72
|
+
expect(new Set(REBASE_INTERNAL_TABLES).size).toBe(REBASE_INTERNAL_TABLES.length);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ─── The statement ───────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
describe("revokeInternalTableSql", () => {
|
|
79
|
+
|
|
80
|
+
it("revokes every privilege from the end-user role", () => {
|
|
81
|
+
const sql = revokeInternalTableSql("rebase", "refresh_tokens");
|
|
82
|
+
|
|
83
|
+
expect(sql).toContain(`REVOKE ALL ON "rebase"."refresh_tokens" FROM ${REBASE_USER_ROLE}`);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("guards on the role existing, so an unprivileged install is a no-op", () => {
|
|
87
|
+
// A bare REVOKE naming a missing role is an error, not a no-op, and
|
|
88
|
+
// it would abort the boot it runs in.
|
|
89
|
+
const sql = revokeInternalTableSql("rebase", "api_keys");
|
|
90
|
+
|
|
91
|
+
expect(sql).toContain(`SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}'`);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("tolerates a table that does not exist yet", () => {
|
|
95
|
+
// `cron_logs` never appears in a project with no cron jobs.
|
|
96
|
+
expect(revokeInternalTableSql("rebase", "cron_logs")).toContain("to_regclass");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("is a single statement, for handles that reject multi-statement strings", () => {
|
|
100
|
+
// The semicolons inside the block are PL/pgSQL's, not the protocol's:
|
|
101
|
+
// what matters is that the whole string is one dollar-quoted DO,
|
|
102
|
+
// opened once and closed at the very end.
|
|
103
|
+
const sql = revokeInternalTableSql("rebase", "jobs");
|
|
104
|
+
|
|
105
|
+
expect(sql.startsWith("DO $rebase_revoke$")).toBe(true);
|
|
106
|
+
expect(sql.endsWith("$rebase_revoke$;")).toBe(true);
|
|
107
|
+
expect(sql.split("$rebase_revoke$")).toHaveLength(3);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("refuses to interpolate an unsafe identifier", () => {
|
|
111
|
+
expect(() => revokeInternalTableSql("rebase", 'x"; DROP TABLE users; --')).toThrow(/unsafe table name/);
|
|
112
|
+
expect(() => revokeInternalTableSql('pub"lic', "api_keys")).toThrow(/unsafe schema name/);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ─── The sweep ───────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
describe("revokeInternalTableAccess", () => {
|
|
119
|
+
|
|
120
|
+
it("issues one statement per internal table", async () => {
|
|
121
|
+
const executed: string[] = [];
|
|
122
|
+
|
|
123
|
+
await revokeInternalTableAccess(async (sql) => { executed.push(sql); }, "rebase");
|
|
124
|
+
|
|
125
|
+
expect(executed).toHaveLength(REBASE_INTERNAL_TABLES.length);
|
|
126
|
+
for (const table of REBASE_INTERNAL_TABLES) {
|
|
127
|
+
expect(executed.some((sql) => sql.includes(`"rebase"."${table}"`))).toBe(true);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("reports a failure rather than swallowing it", async () => {
|
|
132
|
+
// The whole risk: an onError that no caller wires up turns twenty
|
|
133
|
+
// failed revokes into a silent, apparently-clean boot.
|
|
134
|
+
const seen: string[] = [];
|
|
135
|
+
|
|
136
|
+
await revokeInternalTableAccess(
|
|
137
|
+
async (sql) => {
|
|
138
|
+
if (sql.includes("refresh_tokens")) throw new Error("permission denied");
|
|
139
|
+
},
|
|
140
|
+
"rebase",
|
|
141
|
+
{ onError: (table) => seen.push(table) }
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
expect(seen).toEqual(["refresh_tokens"]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("keeps going after one table fails", async () => {
|
|
148
|
+
const executed: string[] = [];
|
|
149
|
+
|
|
150
|
+
await revokeInternalTableAccess(
|
|
151
|
+
async (sql) => {
|
|
152
|
+
executed.push(sql);
|
|
153
|
+
if (sql.includes("user_identities")) throw new Error("not the owner");
|
|
154
|
+
},
|
|
155
|
+
"rebase",
|
|
156
|
+
{ onError: () => undefined }
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// A connection that does not own one table must not strand the rest.
|
|
160
|
+
expect(executed).toHaveLength(REBASE_INTERNAL_TABLES.length);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("honours an explicit table list", async () => {
|
|
164
|
+
const executed: string[] = [];
|
|
165
|
+
|
|
166
|
+
await revokeInternalTableAccess(
|
|
167
|
+
async (sql) => { executed.push(sql); },
|
|
168
|
+
"rebase",
|
|
169
|
+
{ tables: ["api_keys"] }
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
expect(executed).toHaveLength(1);
|
|
173
|
+
expect(executed[0]).toContain('"rebase"."api_keys"');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -64,6 +64,15 @@ export const REBASE_USER_ROLE = "rebase_user";
|
|
|
64
64
|
*
|
|
65
65
|
* `atlas_schema_revisions` is Atlas's migration ledger, which lands in `rebase`
|
|
66
66
|
* because `db migrate apply` passes `--revisions-schema rebase`.
|
|
67
|
+
*
|
|
68
|
+
* Every entry here must also be revoked by whatever creates it, and vice versa:
|
|
69
|
+
* the creation-time revoke fires once, on the boot that first makes the table,
|
|
70
|
+
* so it cannot help a database provisioned before that revoke existed. This
|
|
71
|
+
* list is what the boot-time sweep in `ensureAppRole` iterates, and the sweep
|
|
72
|
+
* is the only thing that can repair an already-granted table. A table revoked
|
|
73
|
+
* at creation but missing here is therefore permanently stranded on any
|
|
74
|
+
* database that predates its revoke — which is exactly what happened to
|
|
75
|
+
* `jobs`, added here after a scan of a pre-0.14 database found it.
|
|
67
76
|
*/
|
|
68
77
|
export const REBASE_INTERNAL_TABLES: readonly string[] = [
|
|
69
78
|
// auth
|
|
@@ -80,10 +89,12 @@ export const REBASE_INTERNAL_TABLES: readonly string[] = [
|
|
|
80
89
|
"api_keys",
|
|
81
90
|
"cron_logs",
|
|
82
91
|
"cron_claims",
|
|
92
|
+
"jobs",
|
|
83
93
|
"rate_limit_hits",
|
|
84
94
|
"idempotency_keys",
|
|
85
95
|
"entity_history",
|
|
86
96
|
"branches",
|
|
97
|
+
"metric_samples",
|
|
87
98
|
// realtime channels — authorization for these lives in the channel rules the
|
|
88
99
|
// server evaluates before it reads or writes, never in a row policy
|
|
89
100
|
"channel_messages",
|
package/src/util/relations.ts
CHANGED
|
@@ -104,8 +104,55 @@ export function getTableName(collection: CollectionConfig): string {
|
|
|
104
104
|
return toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* A JavaScript identifier: what a generated `export const <name> =` needs.
|
|
109
|
+
*
|
|
110
|
+
* Deliberately the same shape the two schema generators already define
|
|
111
|
+
* privately — this is the third place that needed it, and the first two guard
|
|
112
|
+
* property keys and member accesses while nothing guarded the variable name
|
|
113
|
+
* itself.
|
|
114
|
+
*/
|
|
115
|
+
const JS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The variable name a generated table is bound to.
|
|
119
|
+
*
|
|
120
|
+
* Camel-cases underscores, and then guarantees the result is a legal
|
|
121
|
+
* identifier. It did only the first, so a table name that is legal in Postgres
|
|
122
|
+
* and not in JavaScript produced a `schema.generated.ts` that does not parse:
|
|
123
|
+
*
|
|
124
|
+
* `2024_archive` → `export const 2024Archive = pgTable(…)`
|
|
125
|
+
* "An identifier or keyword cannot immediately follow
|
|
126
|
+
* a numeric literal"
|
|
127
|
+
* `reporting.events` → `export const reporting.events = pgTable(…)`
|
|
128
|
+
* "',' expected"
|
|
129
|
+
*
|
|
130
|
+
* That file is imported by the server, so the failure is not one broken
|
|
131
|
+
* collection — `rebase build` and `db push` fail at tsc for the whole
|
|
132
|
+
* directory. And it is reachable from a documented flow: `rebase init` against
|
|
133
|
+
* a database holding a table called `2024_archive` writes a collection file
|
|
134
|
+
* that parses and a schema file that does not.
|
|
135
|
+
*
|
|
136
|
+
* **A no-op for every name that already worked**, which is what makes changing
|
|
137
|
+
* a derived name safe here: the only inputs whose output changes are the ones
|
|
138
|
+
* that produced a syntax error, and nothing can be running against those.
|
|
139
|
+
* Separators become camel case rather than disappearing, so `reporting.events`
|
|
140
|
+
* and `reporting_events` do not collide into one variable.
|
|
141
|
+
*/
|
|
107
142
|
export function getTableVarName(tableName: string): string {
|
|
108
|
-
|
|
143
|
+
const camel = tableName.replace(/_([a-z])/g, (_, char: string) => char.toUpperCase());
|
|
144
|
+
if (JS_IDENTIFIER.test(camel)) return camel;
|
|
145
|
+
|
|
146
|
+
const sanitised = camel
|
|
147
|
+
// Any other separator gets the same treatment `_` did, so two tables
|
|
148
|
+
// differing only by separator keep differing.
|
|
149
|
+
.replace(/[^A-Za-z0-9_$]+([A-Za-z0-9])?/g, (_, char?: string) =>
|
|
150
|
+
(char ? char.toUpperCase() : ""))
|
|
151
|
+
// A leading digit is legal in Postgres and not in JavaScript. Prefixed
|
|
152
|
+
// rather than stripped, so `2024_archive` and `archive` stay distinct.
|
|
153
|
+
.replace(/^([0-9])/, "t$1");
|
|
154
|
+
|
|
155
|
+
return JS_IDENTIFIER.test(sanitised) ? sanitised : `t${sanitised}`;
|
|
109
156
|
}
|
|
110
157
|
|
|
111
158
|
export function getEnumVarName(tableName: string, propName: string): string {
|