@rebasepro/common 0.11.1-canary.gfd39654 → 0.12.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/data/buildRebaseData.d.ts +8 -4
- package/dist/data/paginate.d.ts +75 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +466 -23
- package/dist/index.es.js.map +1 -1
- package/dist/util/entities.d.ts +1 -1
- package/dist/util/index.d.ts +1 -0
- package/dist/util/pg-column-to-property.d.ts +25 -0
- package/package.json +3 -3
- package/src/data/buildRebaseData.ts +101 -13
- package/src/data/paginate.ts +290 -0
- package/src/index.ts +1 -0
- package/src/util/auth-default-policies.ts +4 -4
- package/src/util/entities.ts +1 -1
- package/src/util/index.ts +1 -0
- package/src/util/junction-policies.ts +1 -1
- package/src/util/pg-column-to-property.ts +335 -0
- package/src/util/relations.ts +3 -3
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ArrayProperty,
|
|
3
|
+
NumberProperty,
|
|
4
|
+
PostgresProperties,
|
|
5
|
+
Property,
|
|
6
|
+
Relation,
|
|
7
|
+
SecurityOperation,
|
|
8
|
+
SecurityRule,
|
|
9
|
+
StringProperty,
|
|
10
|
+
TableColumnInfo,
|
|
11
|
+
TableMetadata
|
|
12
|
+
} from "@rebasepro/types";
|
|
13
|
+
import { prettifyIdentifier } from "@rebasepro/utils";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A collection as introspection can describe it: the table, its columns, the
|
|
17
|
+
* relations its foreign keys imply, and the RLS policies already on it.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately not `Partial<AdminCollection>`, which is what this returned
|
|
20
|
+
* while it lived in `@rebasepro/studio`. `propertiesOrder` is the only admin
|
|
21
|
+
* key it produces, and naming the admin view model for one field would put
|
|
22
|
+
* `@rebasepro/admin-types` on the dependency path of a package the backend
|
|
23
|
+
* loads.
|
|
24
|
+
*/
|
|
25
|
+
export interface IntrospectedCollection {
|
|
26
|
+
name: string;
|
|
27
|
+
slug: string;
|
|
28
|
+
table: string;
|
|
29
|
+
properties: PostgresProperties;
|
|
30
|
+
propertiesOrder: string[];
|
|
31
|
+
relations?: Relation[];
|
|
32
|
+
securityRules?: SecurityRule[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Maps a PostgreSQL column data type to a Rebase property type.
|
|
37
|
+
*/
|
|
38
|
+
function pgTypeToRebaseProperty(column: TableColumnInfo): Property | null {
|
|
39
|
+
const {
|
|
40
|
+
column_name,
|
|
41
|
+
data_type,
|
|
42
|
+
udt_name,
|
|
43
|
+
is_nullable,
|
|
44
|
+
column_default,
|
|
45
|
+
enum_values
|
|
46
|
+
} = column;
|
|
47
|
+
|
|
48
|
+
const required = is_nullable === "NO";
|
|
49
|
+
const prettifiedName = prettifyIdentifier(column_name);
|
|
50
|
+
|
|
51
|
+
// Detect if this column is a primary key (auto-generated id)
|
|
52
|
+
const isAutoId = column_default != null && (
|
|
53
|
+
column_default.includes("nextval") ||
|
|
54
|
+
column_default.includes("gen_random_uuid") ||
|
|
55
|
+
column_default.includes("uuid_generate") ||
|
|
56
|
+
column_default.includes("identity")
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// USER-DEFINED = PostgreSQL enums
|
|
60
|
+
if (data_type === "USER-DEFINED" && enum_values && enum_values.length > 0) {
|
|
61
|
+
return {
|
|
62
|
+
type: "string",
|
|
63
|
+
name: prettifiedName,
|
|
64
|
+
enum: enum_values.map((v: string) => ({ id: v,
|
|
65
|
+
label: prettifyIdentifier(v) })),
|
|
66
|
+
validation: required ? { required: true } : undefined
|
|
67
|
+
} as StringProperty;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const dt = data_type.toLowerCase();
|
|
71
|
+
switch (dt) {
|
|
72
|
+
case "character varying":
|
|
73
|
+
case "varchar":
|
|
74
|
+
case "text":
|
|
75
|
+
case "char":
|
|
76
|
+
case "character":
|
|
77
|
+
case "citext": {
|
|
78
|
+
let colType: "varchar" | "text" | "char" = "varchar";
|
|
79
|
+
if (dt === "text" || dt === "citext") colType = "text";
|
|
80
|
+
if (dt === "char" || dt === "character") colType = "char";
|
|
81
|
+
const prop: StringProperty = {
|
|
82
|
+
type: "string",
|
|
83
|
+
name: prettifiedName,
|
|
84
|
+
columnType: colType,
|
|
85
|
+
validation: required ? { required: true } : undefined
|
|
86
|
+
};
|
|
87
|
+
if (isAutoId) {
|
|
88
|
+
prop.isId = "manual";
|
|
89
|
+
}
|
|
90
|
+
return prop;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
case "uuid": {
|
|
94
|
+
const prop: StringProperty = {
|
|
95
|
+
type: "string",
|
|
96
|
+
name: prettifiedName,
|
|
97
|
+
validation: required ? { required: true } : undefined
|
|
98
|
+
};
|
|
99
|
+
if (isAutoId) {
|
|
100
|
+
prop.isId = "uuid";
|
|
101
|
+
}
|
|
102
|
+
return prop;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
case "integer":
|
|
106
|
+
case "bigint":
|
|
107
|
+
case "smallint": {
|
|
108
|
+
const colType = dt === "bigint" ? "bigint" : "integer";
|
|
109
|
+
const prop: NumberProperty = {
|
|
110
|
+
type: "number",
|
|
111
|
+
name: prettifiedName,
|
|
112
|
+
columnType: colType,
|
|
113
|
+
validation: {
|
|
114
|
+
...(required ? { required: true } : {}),
|
|
115
|
+
integer: true
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
if (isAutoId) {
|
|
119
|
+
prop.isId = "increment";
|
|
120
|
+
}
|
|
121
|
+
return prop;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
case "serial":
|
|
125
|
+
case "bigserial":
|
|
126
|
+
case "smallserial": {
|
|
127
|
+
const colType = dt === "bigserial" ? "bigserial" : "serial";
|
|
128
|
+
return {
|
|
129
|
+
type: "number",
|
|
130
|
+
name: prettifiedName,
|
|
131
|
+
columnType: colType,
|
|
132
|
+
isId: "increment",
|
|
133
|
+
validation: {
|
|
134
|
+
...(required ? { required: true } : {}),
|
|
135
|
+
integer: true
|
|
136
|
+
}
|
|
137
|
+
} as NumberProperty;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
case "numeric":
|
|
141
|
+
case "decimal":
|
|
142
|
+
case "real":
|
|
143
|
+
case "double precision": {
|
|
144
|
+
let colType: "numeric" | "real" | "double precision" = "numeric";
|
|
145
|
+
if (dt === "real") colType = "real";
|
|
146
|
+
if (dt === "double precision") colType = "double precision";
|
|
147
|
+
return {
|
|
148
|
+
type: "number",
|
|
149
|
+
name: prettifiedName,
|
|
150
|
+
columnType: colType,
|
|
151
|
+
validation: required ? { required: true } : undefined
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
case "boolean":
|
|
156
|
+
return {
|
|
157
|
+
type: "boolean",
|
|
158
|
+
name: prettifiedName,
|
|
159
|
+
validation: required ? { required: true } : undefined
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
case "timestamp with time zone":
|
|
163
|
+
case "timestamp without time zone":
|
|
164
|
+
case "timestamp":
|
|
165
|
+
case "timestamptz":
|
|
166
|
+
case "date":
|
|
167
|
+
case "time with time zone":
|
|
168
|
+
case "time without time zone":
|
|
169
|
+
case "time": {
|
|
170
|
+
let colType: "timestamp" | "date" | "time" = "timestamp";
|
|
171
|
+
if (dt.startsWith("date")) colType = "date";
|
|
172
|
+
if (dt.startsWith("time ") || dt === "time") colType = "time";
|
|
173
|
+
return {
|
|
174
|
+
type: "date",
|
|
175
|
+
name: prettifiedName,
|
|
176
|
+
columnType: colType,
|
|
177
|
+
validation: required ? { required: true } : undefined
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
case "jsonb":
|
|
182
|
+
case "json":
|
|
183
|
+
return {
|
|
184
|
+
type: "map",
|
|
185
|
+
name: prettifiedName,
|
|
186
|
+
columnType: dt === "jsonb" ? "jsonb" : "json",
|
|
187
|
+
keyValue: true,
|
|
188
|
+
properties: {}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
case "array":
|
|
192
|
+
case "ARRAY": {
|
|
193
|
+
let innerType = "string";
|
|
194
|
+
let colType: ArrayProperty["columnType"] = undefined;
|
|
195
|
+
if (udt_name === "_text" || udt_name === "_varchar") {
|
|
196
|
+
innerType = "string";
|
|
197
|
+
colType = "text[]";
|
|
198
|
+
} else if (udt_name === "_int4" || udt_name === "_int2" || udt_name === "_int8") {
|
|
199
|
+
innerType = "number";
|
|
200
|
+
colType = "integer[]";
|
|
201
|
+
} else if (udt_name === "_bool") {
|
|
202
|
+
innerType = "boolean";
|
|
203
|
+
colType = "boolean[]";
|
|
204
|
+
} else if (udt_name === "_numeric") {
|
|
205
|
+
innerType = "number";
|
|
206
|
+
colType = "numeric[]";
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
type: "array",
|
|
210
|
+
name: prettifiedName,
|
|
211
|
+
columnType: colType,
|
|
212
|
+
of: { type: innerType }
|
|
213
|
+
} as ArrayProperty;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
default:
|
|
217
|
+
// Fallback: treat unknown types as string
|
|
218
|
+
return {
|
|
219
|
+
type: "string",
|
|
220
|
+
name: prettifiedName,
|
|
221
|
+
validation: required ? { required: true } : undefined
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Builds a collection description from PostgreSQL table metadata.
|
|
228
|
+
* This is used when creating a new collection from an existing database table.
|
|
229
|
+
*/
|
|
230
|
+
export function buildCollectionFromTableMetadata(
|
|
231
|
+
tableName: string,
|
|
232
|
+
metadata: TableMetadata
|
|
233
|
+
): IntrospectedCollection {
|
|
234
|
+
const properties: Record<string, Property> = {};
|
|
235
|
+
const propertiesOrder: string[] = [];
|
|
236
|
+
// Introspection can only ever produce two shapes: a foreign key on this
|
|
237
|
+
// table, or a junction between two. Both are named by their kind.
|
|
238
|
+
const relations: Array<{
|
|
239
|
+
id: string;
|
|
240
|
+
relationName: string;
|
|
241
|
+
target: string;
|
|
242
|
+
kind: "belongsTo" | "manyToMany";
|
|
243
|
+
localKey?: string;
|
|
244
|
+
through?: { table: string; sourceColumn: string; targetColumn: string };
|
|
245
|
+
}> = [];
|
|
246
|
+
const securityRules: SecurityRule[] = [];
|
|
247
|
+
|
|
248
|
+
// Parse columns
|
|
249
|
+
for (const column of metadata.columns) {
|
|
250
|
+
const property = pgTypeToRebaseProperty(column);
|
|
251
|
+
if (property) {
|
|
252
|
+
const propRecord = property as unknown as Record<string, unknown>;
|
|
253
|
+
Object.keys(propRecord).forEach(key => propRecord[key] === undefined && delete propRecord[key]);
|
|
254
|
+
|
|
255
|
+
properties[column.column_name] = property;
|
|
256
|
+
propertiesOrder.push(column.column_name);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Parse Outgoing Foreign Keys -> Many-to-One / One-to-One
|
|
261
|
+
if (metadata.foreignKeys) {
|
|
262
|
+
for (const fk of metadata.foreignKeys) {
|
|
263
|
+
const relName = fk.column_name.endsWith("_id") ? fk.column_name.substring(0, fk.column_name.length - 3) : fk.column_name;
|
|
264
|
+
relations.push({
|
|
265
|
+
id: fk.column_name,
|
|
266
|
+
relationName: relName,
|
|
267
|
+
target: fk.foreign_table_name, // Will be hydrated later
|
|
268
|
+
kind: "belongsTo",
|
|
269
|
+
localKey: fk.column_name
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Parse Incoming Junctions -> Many-to-Many
|
|
275
|
+
if (metadata.junctions) {
|
|
276
|
+
for (const junction of metadata.junctions) {
|
|
277
|
+
const relName = junction.target_table_name; // E.g., 'roles'
|
|
278
|
+
relations.push({
|
|
279
|
+
id: junction.target_table_name + "_relation",
|
|
280
|
+
relationName: relName,
|
|
281
|
+
target: junction.target_table_name, // Will be hydrated later
|
|
282
|
+
kind: "manyToMany",
|
|
283
|
+
through: {
|
|
284
|
+
table: junction.junction_table_name,
|
|
285
|
+
sourceColumn: junction.source_column_name,
|
|
286
|
+
targetColumn: junction.target_column_name
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Parse RLS Policies
|
|
293
|
+
if (metadata.policies) {
|
|
294
|
+
for (const policy of metadata.policies) {
|
|
295
|
+
// Attempt to map typical cmds to operations.
|
|
296
|
+
// Postgres cmd: SELECT, INSERT, UPDATE, DELETE, ALL
|
|
297
|
+
let operations: SecurityOperation[] = [];
|
|
298
|
+
switch (policy.cmd) {
|
|
299
|
+
case "ALL": operations = ["all"]; break;
|
|
300
|
+
case "SELECT": operations = ["select"]; break;
|
|
301
|
+
case "INSERT": operations = ["insert"]; break;
|
|
302
|
+
case "UPDATE": operations = ["update"]; break;
|
|
303
|
+
case "DELETE": operations = ["delete"]; break;
|
|
304
|
+
}
|
|
305
|
+
const qual = policy.qual ?? undefined;
|
|
306
|
+
const withCheck = policy.with_check ?? undefined;
|
|
307
|
+
if (qual) {
|
|
308
|
+
securityRules.push({
|
|
309
|
+
name: policy.policy_name,
|
|
310
|
+
operations,
|
|
311
|
+
roles: policy.roles ?? [],
|
|
312
|
+
using: qual,
|
|
313
|
+
...(withCheck ? { withCheck } : {})
|
|
314
|
+
});
|
|
315
|
+
} else {
|
|
316
|
+
securityRules.push({
|
|
317
|
+
name: policy.policy_name,
|
|
318
|
+
operations,
|
|
319
|
+
roles: policy.roles ?? []
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
name: prettifyIdentifier(tableName),
|
|
327
|
+
slug: tableName,
|
|
328
|
+
table: tableName,
|
|
329
|
+
properties: properties as PostgresProperties,
|
|
330
|
+
propertiesOrder,
|
|
331
|
+
// `target` is still a slug here — the caller hydrates it into a thunk.
|
|
332
|
+
...(relations.length > 0 ? { relations: relations as unknown as Relation[] } : {}),
|
|
333
|
+
...(securityRules.length > 0 ? { securityRules } : {})
|
|
334
|
+
};
|
|
335
|
+
}
|
package/src/util/relations.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CollectionConfig,
|
|
1
|
+
import { CollectionConfig, isRelationalCollectionConfig, Property, ResolvedRelation, RelationProperty } from "@rebasepro/types";
|
|
2
2
|
import { toSnakeCase } from "@rebasepro/utils";
|
|
3
3
|
|
|
4
4
|
import { resolveRelation } from "./resolve-relation";
|
|
@@ -42,7 +42,7 @@ export function resolveCollectionRelations(
|
|
|
42
42
|
const cached = _resolvedRelationsCache.get(collection);
|
|
43
43
|
if (cached) return cached;
|
|
44
44
|
|
|
45
|
-
if (!
|
|
45
|
+
if (!isRelationalCollectionConfig(collection)) return {};
|
|
46
46
|
|
|
47
47
|
const relations: Record<string, ResolvedRelation> = {};
|
|
48
48
|
|
|
@@ -67,7 +67,7 @@ export function resolveCollectionRelations(
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
export function getTableName(collection: CollectionConfig): string {
|
|
70
|
-
if (
|
|
70
|
+
if (isRelationalCollectionConfig(collection)) {
|
|
71
71
|
return collection.table ?? toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
|
|
72
72
|
}
|
|
73
73
|
return toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
|