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