@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.
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- import { CollectionConfig, getDataSourceCapabilities, Property, ResolvedRelation, RelationProperty } from "@rebasepro/types";
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 (!getDataSourceCapabilities(collection.engine).supportsRelations) return {};
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 (getDataSourceCapabilities(collection.engine).supportsRelations) {
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);