@kmlckj/licos-platform-sdk 0.6.3 → 0.6.4
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/README.md +57 -47
- package/package.json +2 -2
- package/src/database.js +688 -255
- package/src/index.d.ts +251 -208
package/src/database.js
CHANGED
|
@@ -1,255 +1,688 @@
|
|
|
1
|
-
import { ApiError } from './shared.js';
|
|
2
|
-
import { authHeaders, runtimeConfig } from './runtime.js';
|
|
3
|
-
|
|
4
|
-
function databaseConfig() {
|
|
5
|
-
return runtimeConfig();
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function databaseUrl(config, route) {
|
|
9
|
-
return `${config.baseUrl}/api/v1/studio/runtime/database/projects/${encodeURIComponent(config.projectId)}${route}`;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function
|
|
13
|
-
return
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
this.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
this.
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
this.
|
|
204
|
-
return this;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
this.
|
|
209
|
-
this
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
1
|
+
import { ApiError } from './shared.js';
|
|
2
|
+
import { authHeaders, runtimeConfig } from './runtime.js';
|
|
3
|
+
|
|
4
|
+
function databaseConfig() {
|
|
5
|
+
return runtimeConfig();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function databaseUrl(config, route) {
|
|
9
|
+
return `${config.baseUrl}/api/v1/studio/runtime/database/projects/${encodeURIComponent(config.projectId)}${route}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function controlDatabaseUrl(config, route) {
|
|
13
|
+
return `${config.baseUrl}/api/v1/studio/control/database/projects/${encodeURIComponent(config.projectId)}${route}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function compactObject(value) {
|
|
17
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== null));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeSelect(select) {
|
|
21
|
+
if (select === undefined || select === null) return undefined;
|
|
22
|
+
if (typeof select === 'string') {
|
|
23
|
+
return select.split(',').map((item) => item.trim()).filter(Boolean);
|
|
24
|
+
}
|
|
25
|
+
return Array.from(select).map((item) => String(item));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function decodeResponse(response) {
|
|
29
|
+
const text = await response.text();
|
|
30
|
+
const payload = text ? JSON.parse(text) : null;
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
throw new ApiError(text || `platform database API returned ${response.status}`, {
|
|
33
|
+
status: response.status,
|
|
34
|
+
details: payload,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (!payload || typeof payload !== 'object') return payload;
|
|
38
|
+
if ((payload.code !== undefined && payload.code !== 0) || payload.success === false) {
|
|
39
|
+
throw new ApiError(payload.message || 'platform database API failed', {
|
|
40
|
+
status: response.status,
|
|
41
|
+
code: typeof payload.code === 'number' ? payload.code : undefined,
|
|
42
|
+
details: payload,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return payload.data;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function requestJson(route, body) {
|
|
49
|
+
const config = databaseConfig();
|
|
50
|
+
const payload = { environment: config.environment, ...compactObject(body) };
|
|
51
|
+
const response = await fetch(databaseUrl(config, route), {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: authHeaders(config),
|
|
54
|
+
body: JSON.stringify(payload),
|
|
55
|
+
});
|
|
56
|
+
return decodeResponse(response);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function requestControlJson(route, query = {}) {
|
|
60
|
+
const config = databaseConfig();
|
|
61
|
+
const url = new URL(controlDatabaseUrl(config, route));
|
|
62
|
+
for (const [key, value] of Object.entries({ environment: config.environment, ...query })) {
|
|
63
|
+
if (value !== undefined && value !== null) url.searchParams.set(key, String(value));
|
|
64
|
+
}
|
|
65
|
+
const response = await fetch(url, {
|
|
66
|
+
method: 'GET',
|
|
67
|
+
headers: authHeaders(config),
|
|
68
|
+
});
|
|
69
|
+
return decodeResponse(response);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function query(table, options = {}) {
|
|
73
|
+
return requestJson('/query', {
|
|
74
|
+
table,
|
|
75
|
+
select: normalizeSelect(options.select),
|
|
76
|
+
filters: options.filters?.length ? options.filters : undefined,
|
|
77
|
+
orderBy: options.orderBy?.length ? options.orderBy : undefined,
|
|
78
|
+
range: options.range,
|
|
79
|
+
limit: options.limit,
|
|
80
|
+
offset: options.offset,
|
|
81
|
+
count: options.count,
|
|
82
|
+
head: options.head,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function single(table, options = {}) {
|
|
87
|
+
return requestJson('/single', {
|
|
88
|
+
table,
|
|
89
|
+
select: normalizeSelect(options.select),
|
|
90
|
+
filters: options.filters?.length ? options.filters : undefined,
|
|
91
|
+
orderBy: options.orderBy?.length ? options.orderBy : undefined,
|
|
92
|
+
mode: options.maybe ? 'maybe_single' : 'single',
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function maybeSingle(table, options = {}) {
|
|
97
|
+
return single(table, { ...options, maybe: true });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function aggregate(table, aggregateName, options = {}) {
|
|
101
|
+
return requestJson('/aggregate', {
|
|
102
|
+
table,
|
|
103
|
+
filters: options.filters?.length ? options.filters : undefined,
|
|
104
|
+
aggregate: aggregateName,
|
|
105
|
+
field: options.field,
|
|
106
|
+
groupBy: options.groupBy?.length ? options.groupBy : undefined,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function insert(table, options = {}) {
|
|
111
|
+
return requestJson('/insert', {
|
|
112
|
+
table,
|
|
113
|
+
row: options.row,
|
|
114
|
+
rows: options.rows,
|
|
115
|
+
returning: options.returning ?? true,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function updateRows(table, options = {}) {
|
|
120
|
+
return requestJson('/update', {
|
|
121
|
+
table,
|
|
122
|
+
values: options.values,
|
|
123
|
+
filters: options.filters?.length ? options.filters : undefined,
|
|
124
|
+
updates: options.updates?.length ? options.updates : undefined,
|
|
125
|
+
returning: options.returning ?? true,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function deleteRows(table, options = {}) {
|
|
130
|
+
return requestJson('/delete', {
|
|
131
|
+
table,
|
|
132
|
+
filters: options.filters?.length ? options.filters : undefined,
|
|
133
|
+
deletes: options.deletes?.length ? options.deletes : undefined,
|
|
134
|
+
returning: options.returning ?? true,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function upsert(table, options = {}) {
|
|
139
|
+
return requestJson('/upsert', {
|
|
140
|
+
table,
|
|
141
|
+
row: options.row,
|
|
142
|
+
rows: options.rows,
|
|
143
|
+
conflictKeys: options.conflictKeys?.length ? options.conflictKeys : undefined,
|
|
144
|
+
returning: options.returning ?? true,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function transaction(operations) {
|
|
149
|
+
const config = databaseConfig();
|
|
150
|
+
const normalized = operations.map((operation) => ({
|
|
151
|
+
type: operation.type,
|
|
152
|
+
request: {
|
|
153
|
+
...operation.request,
|
|
154
|
+
environment: config.environment,
|
|
155
|
+
},
|
|
156
|
+
}));
|
|
157
|
+
return requestJson('/transaction', { operations: normalized });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function rpc(functionName, args = {}) {
|
|
161
|
+
return requestJson(`/rpc/${encodeURIComponent(functionName)}`, { args });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function getSchema() {
|
|
165
|
+
return requestControlJson('/schema');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function defaultOrm(language) {
|
|
169
|
+
return normalizeLanguage(language) === 'typescript' ? 'drizzle' : 'sqlalchemy';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function generateOrm(schemaPayload, options = {}) {
|
|
173
|
+
const language = normalizeLanguage(options.language);
|
|
174
|
+
const orm = normalizeOrm(language, options.orm);
|
|
175
|
+
if (language === 'typescript' && orm === 'drizzle') return generateDrizzle(schemaPayload);
|
|
176
|
+
if (language === 'python' && orm === 'sqlalchemy') return generateSqlAlchemy(schemaPayload);
|
|
177
|
+
throw new Error(`unsupported ORM export target: ${options.language}/${options.orm ?? 'default'}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function exportOrm(options = {}) {
|
|
181
|
+
const schemaPayload = await getSchema();
|
|
182
|
+
return generateOrm(schemaPayload, options);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function table(name) {
|
|
186
|
+
return new TableQuery(name);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
class TableQuery {
|
|
190
|
+
constructor(name) {
|
|
191
|
+
this.name = name;
|
|
192
|
+
this._select = undefined;
|
|
193
|
+
this._filters = [];
|
|
194
|
+
this._orderBy = [];
|
|
195
|
+
this._range = undefined;
|
|
196
|
+
this._limit = undefined;
|
|
197
|
+
this._offset = undefined;
|
|
198
|
+
this._count = undefined;
|
|
199
|
+
this._head = undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
select(...fields) {
|
|
203
|
+
this._select = fields.length === 1 && fields[0].includes(',') ? normalizeSelect(fields[0]) : fields;
|
|
204
|
+
return this;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
filter(field, op, value) {
|
|
208
|
+
this._filters.push({ field, op, value });
|
|
209
|
+
return this;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
eq(field, value) { return this.filter(field, 'eq', value); }
|
|
213
|
+
neq(field, value) { return this.filter(field, 'neq', value); }
|
|
214
|
+
gt(field, value) { return this.filter(field, 'gt', value); }
|
|
215
|
+
gte(field, value) { return this.filter(field, 'gte', value); }
|
|
216
|
+
lt(field, value) { return this.filter(field, 'lt', value); }
|
|
217
|
+
lte(field, value) { return this.filter(field, 'lte', value); }
|
|
218
|
+
in(field, value) { return this.filter(field, 'in', value); }
|
|
219
|
+
notIn(field, value) { return this.filter(field, 'not_in', value); }
|
|
220
|
+
isNull(field) { return this.filter(field, 'is_null'); }
|
|
221
|
+
isNotNull(field) { return this.filter(field, 'is_not_null'); }
|
|
222
|
+
like(field, value) { return this.filter(field, 'like', value); }
|
|
223
|
+
ilike(field, value) { return this.filter(field, 'ilike', value); }
|
|
224
|
+
|
|
225
|
+
order(field, options = {}) {
|
|
226
|
+
this._orderBy.push({ field, direction: options.desc ? 'desc' : 'asc' });
|
|
227
|
+
return this;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
range(from, to) {
|
|
231
|
+
this._range = { from, to };
|
|
232
|
+
return this;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
limit(value) {
|
|
236
|
+
this._limit = value;
|
|
237
|
+
return this;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
offset(value) {
|
|
241
|
+
this._offset = value;
|
|
242
|
+
return this;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
withCount(options = {}) {
|
|
246
|
+
this._count = true;
|
|
247
|
+
this._head = Boolean(options.head);
|
|
248
|
+
return this;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
execute() {
|
|
252
|
+
return query(this.name, {
|
|
253
|
+
select: this._select,
|
|
254
|
+
filters: this._filters,
|
|
255
|
+
orderBy: this._orderBy,
|
|
256
|
+
range: this._range,
|
|
257
|
+
limit: this._limit,
|
|
258
|
+
offset: this._offset,
|
|
259
|
+
count: this._count,
|
|
260
|
+
head: this._head,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
single() {
|
|
265
|
+
return single(this.name, {
|
|
266
|
+
select: this._select,
|
|
267
|
+
filters: this._filters,
|
|
268
|
+
orderBy: this._orderBy,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
maybeSingle() {
|
|
273
|
+
return maybeSingle(this.name, {
|
|
274
|
+
select: this._select,
|
|
275
|
+
filters: this._filters,
|
|
276
|
+
orderBy: this._orderBy,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function normalizeLanguage(value) {
|
|
282
|
+
const aliases = {
|
|
283
|
+
ts: 'typescript',
|
|
284
|
+
node: 'typescript',
|
|
285
|
+
nodejs: 'typescript',
|
|
286
|
+
js: 'typescript',
|
|
287
|
+
javascript: 'typescript',
|
|
288
|
+
py: 'python',
|
|
289
|
+
};
|
|
290
|
+
const normalized = aliases[String(value || '').trim().toLowerCase()] || String(value || '').trim().toLowerCase();
|
|
291
|
+
if (!['typescript', 'python'].includes(normalized)) {
|
|
292
|
+
throw new Error('language must be typescript or python');
|
|
293
|
+
}
|
|
294
|
+
return normalized;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function normalizeOrm(language, value) {
|
|
298
|
+
const normalized = String(value || defaultOrm(language)).trim().toLowerCase();
|
|
299
|
+
if (language === 'typescript' && normalized !== 'drizzle') {
|
|
300
|
+
throw new Error('typescript ORM export only supports drizzle');
|
|
301
|
+
}
|
|
302
|
+
if (language === 'python' && normalized !== 'sqlalchemy') {
|
|
303
|
+
throw new Error('python ORM export only supports sqlalchemy');
|
|
304
|
+
}
|
|
305
|
+
return normalized;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function tables(schemaPayload) {
|
|
309
|
+
return Array.isArray(schemaPayload?.tables) ? schemaPayload.tables : [];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function columns(tableValue) {
|
|
313
|
+
return Array.isArray(tableValue?.columns) ? tableValue.columns : [];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function words(value) {
|
|
317
|
+
const matches = String(value || 'value').match(/[A-Za-z0-9]+/g);
|
|
318
|
+
return matches?.length ? matches : ['value'];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function lowerCamel(value) {
|
|
322
|
+
const parts = words(value);
|
|
323
|
+
let ident = parts[0].toLowerCase() + parts.slice(1).map((word) => word[0].toUpperCase() + word.slice(1).toLowerCase()).join('');
|
|
324
|
+
if (/^[0-9]/.test(ident)) ident = `_${ident}`;
|
|
325
|
+
if (TS_RESERVED.has(ident)) ident = `${ident}_`;
|
|
326
|
+
return ident;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function pascal(value) {
|
|
330
|
+
let ident = words(value).map((word) => word[0].toUpperCase() + word.slice(1).toLowerCase()).join('');
|
|
331
|
+
if (/^[0-9]/.test(ident)) ident = `_${ident}`;
|
|
332
|
+
return ident;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function snake(value) {
|
|
336
|
+
let ident = words(value).map((word) => word.toLowerCase()).join('_');
|
|
337
|
+
if (/^[0-9]/.test(ident)) ident = `_${ident}`;
|
|
338
|
+
if (PY_RESERVED.has(ident)) ident = `${ident}_`;
|
|
339
|
+
return ident;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function unique(base, used, fallbackPrefix) {
|
|
343
|
+
let candidate = base;
|
|
344
|
+
if (used.has(candidate) && fallbackPrefix) {
|
|
345
|
+
candidate = `${fallbackPrefix}${base[0].toUpperCase()}${base.slice(1)}`;
|
|
346
|
+
}
|
|
347
|
+
const original = candidate;
|
|
348
|
+
let index = 2;
|
|
349
|
+
while (used.has(candidate)) {
|
|
350
|
+
candidate = `${original}${index}`;
|
|
351
|
+
index += 1;
|
|
352
|
+
}
|
|
353
|
+
used.add(candidate);
|
|
354
|
+
return candidate;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function quote(value) {
|
|
358
|
+
return JSON.stringify(String(value));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function typeName(column) {
|
|
362
|
+
return String(column?.type || '').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function hasDefault(column) {
|
|
366
|
+
return column?.defaultValue !== undefined && column.defaultValue !== null && String(column.defaultValue).trim() !== '';
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function generateDrizzle(schemaPayload) {
|
|
370
|
+
const schemaTables = tables(schemaPayload);
|
|
371
|
+
if (!schemaTables.length) {
|
|
372
|
+
return '// Generated by licos-platform database export-orm.\n// No tables found in schema.\n';
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const pgImports = new Set();
|
|
376
|
+
let needsSql = false;
|
|
377
|
+
const usedTableNames = new Set();
|
|
378
|
+
const usedTypeNames = new Set();
|
|
379
|
+
const schemaNames = [...new Set(schemaTables.map((item) => String(item.schema || 'public')).filter((name) => name !== 'public'))].sort();
|
|
380
|
+
const schemaIdentifiers = Object.fromEntries(schemaNames.map((name) => [name, lowerCamel(`${name}_schema`)]));
|
|
381
|
+
if (schemaNames.length) pgImports.add('pgSchema');
|
|
382
|
+
|
|
383
|
+
const tableBlocks = [];
|
|
384
|
+
const typeBlocks = [];
|
|
385
|
+
for (const tableValue of schemaTables) {
|
|
386
|
+
const schemaName = String(tableValue.schema || 'public');
|
|
387
|
+
const tableName = String(tableValue.name || 'table');
|
|
388
|
+
const variable = unique(lowerCamel(tableName), usedTableNames, schemaName !== 'public' ? lowerCamel(schemaName) : undefined);
|
|
389
|
+
const typeAlias = unique(pascal(tableName), usedTypeNames, schemaName !== 'public' ? pascal(schemaName) : undefined);
|
|
390
|
+
const factory = schemaName === 'public' ? 'pgTable' : `${schemaIdentifiers[schemaName]}.table`;
|
|
391
|
+
if (schemaName === 'public') pgImports.add('pgTable');
|
|
392
|
+
|
|
393
|
+
const usedProps = new Set();
|
|
394
|
+
const columnLines = columns(tableValue).map((column) => {
|
|
395
|
+
const prop = unique(lowerCamel(column.name || 'column'), usedProps);
|
|
396
|
+
const { expression, imports, usesSql } = drizzleColumn(column);
|
|
397
|
+
imports.forEach((item) => pgImports.add(item));
|
|
398
|
+
needsSql = needsSql || usesSql;
|
|
399
|
+
return ` ${prop}: ${expression},`;
|
|
400
|
+
});
|
|
401
|
+
if (!columnLines.length) columnLines.push(' // No columns found.');
|
|
402
|
+
tableBlocks.push(`export const ${variable} = ${factory}(${quote(tableName)}, {\n${columnLines.join('\n')}\n});`);
|
|
403
|
+
typeBlocks.push(`export type ${typeAlias} = typeof ${variable}.$inferSelect;`);
|
|
404
|
+
typeBlocks.push(`export type New${typeAlias} = typeof ${variable}.$inferInsert;`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const lines = ['// Generated by licos-platform database export-orm.', ''];
|
|
408
|
+
if (needsSql) lines.push("import { sql } from 'drizzle-orm';");
|
|
409
|
+
lines.push(`import { ${[...pgImports].sort().join(', ')} } from 'drizzle-orm/pg-core';`);
|
|
410
|
+
lines.push('');
|
|
411
|
+
for (const schemaName of schemaNames) {
|
|
412
|
+
lines.push(`const ${schemaIdentifiers[schemaName]} = pgSchema(${quote(schemaName)});`);
|
|
413
|
+
}
|
|
414
|
+
if (schemaNames.length) lines.push('');
|
|
415
|
+
lines.push(...tableBlocks, '', ...typeBlocks);
|
|
416
|
+
return `${lines.join('\n').trimEnd()}\n`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function drizzleColumn(column) {
|
|
420
|
+
const dataType = typeName(column);
|
|
421
|
+
const imports = new Set();
|
|
422
|
+
let builder = 'text';
|
|
423
|
+
const args = [quote(column.name || 'column')];
|
|
424
|
+
if (dataType === 'uuid') builder = 'uuid';
|
|
425
|
+
else if (['integer', 'int', 'int4', 'serial'].includes(dataType)) builder = 'integer';
|
|
426
|
+
else if (['bigint', 'int8', 'bigserial'].includes(dataType)) {
|
|
427
|
+
builder = 'bigint';
|
|
428
|
+
args.push("{ mode: 'number' }");
|
|
429
|
+
} else if (['smallint', 'int2', 'smallserial'].includes(dataType)) builder = 'smallint';
|
|
430
|
+
else if (['boolean', 'bool'].includes(dataType)) builder = 'boolean';
|
|
431
|
+
else if (['character varying', 'varchar', 'character', 'char'].includes(dataType)) builder = 'varchar';
|
|
432
|
+
else if (['timestamp with time zone', 'timestamptz'].includes(dataType)) {
|
|
433
|
+
builder = 'timestamp';
|
|
434
|
+
args.push('{ withTimezone: true }');
|
|
435
|
+
} else if (['timestamp without time zone', 'timestamp'].includes(dataType)) builder = 'timestamp';
|
|
436
|
+
else if (dataType === 'date') builder = 'date';
|
|
437
|
+
else if (dataType.startsWith('time')) builder = 'time';
|
|
438
|
+
else if (['numeric', 'decimal'].includes(dataType)) builder = 'numeric';
|
|
439
|
+
else if (['double precision', 'float8'].includes(dataType)) builder = 'doublePrecision';
|
|
440
|
+
else if (['real', 'float4'].includes(dataType)) builder = 'real';
|
|
441
|
+
else if (dataType === 'json') builder = 'json';
|
|
442
|
+
else if (dataType === 'jsonb') builder = 'jsonb';
|
|
443
|
+
|
|
444
|
+
imports.add(builder);
|
|
445
|
+
let expression = `${builder}(${args.join(', ')})`;
|
|
446
|
+
if (column.primaryKey) expression += '.primaryKey()';
|
|
447
|
+
else if (column.nullable === false) expression += '.notNull()';
|
|
448
|
+
let usesSql = false;
|
|
449
|
+
if (hasDefault(column)) {
|
|
450
|
+
const defaultSql = String(column.defaultValue).replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
|
451
|
+
expression += `.default(sql\`${defaultSql}\`)`;
|
|
452
|
+
usesSql = true;
|
|
453
|
+
}
|
|
454
|
+
return { expression, imports, usesSql };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function generateSqlAlchemy(schemaPayload) {
|
|
458
|
+
const schemaTables = tables(schemaPayload);
|
|
459
|
+
if (!schemaTables.length) {
|
|
460
|
+
return '# Generated by licos-platform database export-orm.\n# No tables found in schema.\n';
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const sqlalchemyImports = new Set();
|
|
464
|
+
const typingImports = new Set();
|
|
465
|
+
const datetimeImports = new Set();
|
|
466
|
+
let needsDecimal = false;
|
|
467
|
+
let needsAny = false;
|
|
468
|
+
const usedClassNames = new Set();
|
|
469
|
+
const classBlocks = [];
|
|
470
|
+
|
|
471
|
+
for (const tableValue of schemaTables) {
|
|
472
|
+
const schemaName = String(tableValue.schema || 'public');
|
|
473
|
+
const tableName = String(tableValue.name || 'table');
|
|
474
|
+
const className = unique(pascal(tableName), usedClassNames, schemaName !== 'public' ? pascal(schemaName) : undefined);
|
|
475
|
+
const usedAttrs = new Set();
|
|
476
|
+
const body = [` __tablename__ = ${quote(tableName)}`];
|
|
477
|
+
if (schemaName !== 'public') body.push(` __table_args__ = {"schema": ${quote(schemaName)}}`);
|
|
478
|
+
for (const column of columns(tableValue)) {
|
|
479
|
+
const attr = unique(snake(column.name || 'column'), usedAttrs);
|
|
480
|
+
const result = sqlalchemyColumn(column, attr);
|
|
481
|
+
result.imports.forEach((item) => sqlalchemyImports.add(item));
|
|
482
|
+
result.typingImports.forEach((item) => typingImports.add(item));
|
|
483
|
+
result.datetimeImports.forEach((item) => datetimeImports.add(item));
|
|
484
|
+
needsDecimal = needsDecimal || result.needsDecimal;
|
|
485
|
+
needsAny = needsAny || result.needsAny;
|
|
486
|
+
body.push(result.line);
|
|
487
|
+
}
|
|
488
|
+
if (body.length === (schemaName === 'public' ? 1 : 2)) body.push(' pass');
|
|
489
|
+
classBlocks.push(`class ${className}(Base):\n${body.join('\n')}`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const lines = ['# Generated by licos-platform database export-orm.', 'from __future__ import annotations', ''];
|
|
493
|
+
if (datetimeImports.size) lines.push(`from datetime import ${[...datetimeImports].sort().join(', ')}`);
|
|
494
|
+
if (needsDecimal) lines.push('from decimal import Decimal');
|
|
495
|
+
if (typingImports.size || needsAny) {
|
|
496
|
+
lines.push(`from typing import ${[...[...typingImports], ...(needsAny ? ['Any'] : [])].sort().join(', ')}`);
|
|
497
|
+
}
|
|
498
|
+
if (datetimeImports.size || needsDecimal || typingImports.size || needsAny) lines.push('');
|
|
499
|
+
lines.push(`from sqlalchemy import ${[...sqlalchemyImports].sort().join(', ')}`);
|
|
500
|
+
lines.push('from sqlalchemy.orm import Mapped, mapped_column');
|
|
501
|
+
lines.push('');
|
|
502
|
+
lines.push('from .base import Base');
|
|
503
|
+
lines.push('');
|
|
504
|
+
lines.push(...classBlocks);
|
|
505
|
+
return `${lines.join('\n\n').trimEnd()}\n`;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function sqlalchemyColumn(column, attr) {
|
|
509
|
+
const dataType = typeName(column);
|
|
510
|
+
const imports = new Set();
|
|
511
|
+
const typingImports = new Set();
|
|
512
|
+
const datetimeImports = new Set();
|
|
513
|
+
let needsDecimal = false;
|
|
514
|
+
let needsAny = false;
|
|
515
|
+
let sqlType = 'Text';
|
|
516
|
+
let pyType = 'str';
|
|
517
|
+
|
|
518
|
+
if (dataType === 'uuid') {
|
|
519
|
+
sqlType = 'String(36)';
|
|
520
|
+
imports.add('String');
|
|
521
|
+
} else if (['integer', 'int', 'int4', 'serial', 'smallint', 'int2', 'smallserial'].includes(dataType)) {
|
|
522
|
+
sqlType = 'Integer';
|
|
523
|
+
pyType = 'int';
|
|
524
|
+
imports.add('Integer');
|
|
525
|
+
} else if (['bigint', 'int8', 'bigserial'].includes(dataType)) {
|
|
526
|
+
sqlType = 'BigInteger';
|
|
527
|
+
pyType = 'int';
|
|
528
|
+
imports.add('BigInteger');
|
|
529
|
+
} else if (['boolean', 'bool'].includes(dataType)) {
|
|
530
|
+
sqlType = 'Boolean';
|
|
531
|
+
pyType = 'bool';
|
|
532
|
+
imports.add('Boolean');
|
|
533
|
+
} else if (['character varying', 'varchar', 'character', 'char'].includes(dataType)) {
|
|
534
|
+
sqlType = 'String';
|
|
535
|
+
imports.add('String');
|
|
536
|
+
} else if (['timestamp with time zone', 'timestamptz'].includes(dataType)) {
|
|
537
|
+
sqlType = 'DateTime(timezone=True)';
|
|
538
|
+
pyType = 'datetime';
|
|
539
|
+
imports.add('DateTime');
|
|
540
|
+
datetimeImports.add('datetime');
|
|
541
|
+
} else if (['timestamp without time zone', 'timestamp'].includes(dataType)) {
|
|
542
|
+
sqlType = 'DateTime';
|
|
543
|
+
pyType = 'datetime';
|
|
544
|
+
imports.add('DateTime');
|
|
545
|
+
datetimeImports.add('datetime');
|
|
546
|
+
} else if (dataType === 'date') {
|
|
547
|
+
sqlType = 'Date';
|
|
548
|
+
pyType = 'date';
|
|
549
|
+
imports.add('Date');
|
|
550
|
+
datetimeImports.add('date');
|
|
551
|
+
} else if (dataType.startsWith('time')) {
|
|
552
|
+
sqlType = 'Time';
|
|
553
|
+
pyType = 'time';
|
|
554
|
+
imports.add('Time');
|
|
555
|
+
datetimeImports.add('time');
|
|
556
|
+
} else if (['numeric', 'decimal'].includes(dataType)) {
|
|
557
|
+
sqlType = 'Numeric';
|
|
558
|
+
pyType = 'Decimal';
|
|
559
|
+
imports.add('Numeric');
|
|
560
|
+
needsDecimal = true;
|
|
561
|
+
} else if (['double precision', 'float8', 'real', 'float4'].includes(dataType)) {
|
|
562
|
+
sqlType = 'Float';
|
|
563
|
+
pyType = 'float';
|
|
564
|
+
imports.add('Float');
|
|
565
|
+
} else if (['json', 'jsonb'].includes(dataType)) {
|
|
566
|
+
sqlType = 'JSON';
|
|
567
|
+
pyType = 'dict[str, Any]';
|
|
568
|
+
imports.add('JSON');
|
|
569
|
+
needsAny = true;
|
|
570
|
+
} else {
|
|
571
|
+
imports.add('Text');
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (column.nullable === true && !column.primaryKey) {
|
|
575
|
+
pyType = `Optional[${pyType}]`;
|
|
576
|
+
typingImports.add('Optional');
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const args = [];
|
|
580
|
+
if (attr !== column.name) args.push(quote(column.name || attr));
|
|
581
|
+
args.push(sqlType);
|
|
582
|
+
if (column.primaryKey) args.push('primary_key=True');
|
|
583
|
+
if (column.nullable !== undefined && column.nullable !== null) args.push(`nullable=${Boolean(column.nullable) ? 'True' : 'False'}`);
|
|
584
|
+
if (hasDefault(column)) {
|
|
585
|
+
imports.add('text');
|
|
586
|
+
args.push(`server_default=text(${quote(column.defaultValue)})`);
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
line: ` ${attr}: Mapped[${pyType}] = mapped_column(${args.join(', ')})`,
|
|
590
|
+
imports,
|
|
591
|
+
typingImports,
|
|
592
|
+
datetimeImports,
|
|
593
|
+
needsDecimal,
|
|
594
|
+
needsAny,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const TS_RESERVED = new Set([
|
|
599
|
+
'break',
|
|
600
|
+
'case',
|
|
601
|
+
'catch',
|
|
602
|
+
'class',
|
|
603
|
+
'const',
|
|
604
|
+
'continue',
|
|
605
|
+
'debugger',
|
|
606
|
+
'default',
|
|
607
|
+
'delete',
|
|
608
|
+
'do',
|
|
609
|
+
'else',
|
|
610
|
+
'export',
|
|
611
|
+
'extends',
|
|
612
|
+
'finally',
|
|
613
|
+
'for',
|
|
614
|
+
'function',
|
|
615
|
+
'if',
|
|
616
|
+
'import',
|
|
617
|
+
'in',
|
|
618
|
+
'instanceof',
|
|
619
|
+
'new',
|
|
620
|
+
'return',
|
|
621
|
+
'super',
|
|
622
|
+
'switch',
|
|
623
|
+
'this',
|
|
624
|
+
'throw',
|
|
625
|
+
'try',
|
|
626
|
+
'typeof',
|
|
627
|
+
'var',
|
|
628
|
+
'void',
|
|
629
|
+
'while',
|
|
630
|
+
'with',
|
|
631
|
+
'yield',
|
|
632
|
+
]);
|
|
633
|
+
|
|
634
|
+
const PY_RESERVED = new Set([
|
|
635
|
+
'False',
|
|
636
|
+
'None',
|
|
637
|
+
'True',
|
|
638
|
+
'and',
|
|
639
|
+
'as',
|
|
640
|
+
'assert',
|
|
641
|
+
'async',
|
|
642
|
+
'await',
|
|
643
|
+
'break',
|
|
644
|
+
'class',
|
|
645
|
+
'continue',
|
|
646
|
+
'def',
|
|
647
|
+
'del',
|
|
648
|
+
'elif',
|
|
649
|
+
'else',
|
|
650
|
+
'except',
|
|
651
|
+
'finally',
|
|
652
|
+
'for',
|
|
653
|
+
'from',
|
|
654
|
+
'global',
|
|
655
|
+
'if',
|
|
656
|
+
'import',
|
|
657
|
+
'in',
|
|
658
|
+
'is',
|
|
659
|
+
'lambda',
|
|
660
|
+
'nonlocal',
|
|
661
|
+
'not',
|
|
662
|
+
'or',
|
|
663
|
+
'pass',
|
|
664
|
+
'raise',
|
|
665
|
+
'return',
|
|
666
|
+
'try',
|
|
667
|
+
'while',
|
|
668
|
+
'with',
|
|
669
|
+
'yield',
|
|
670
|
+
]);
|
|
671
|
+
|
|
672
|
+
export const database = {
|
|
673
|
+
query,
|
|
674
|
+
single,
|
|
675
|
+
maybeSingle,
|
|
676
|
+
aggregate,
|
|
677
|
+
insert,
|
|
678
|
+
updateRows,
|
|
679
|
+
deleteRows,
|
|
680
|
+
upsert,
|
|
681
|
+
transaction,
|
|
682
|
+
rpc,
|
|
683
|
+
getSchema,
|
|
684
|
+
defaultOrm,
|
|
685
|
+
generateOrm,
|
|
686
|
+
exportOrm,
|
|
687
|
+
table,
|
|
688
|
+
};
|