@promakeai/orm 1.0.6 → 1.3.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/README.md +155 -185
- package/dist/adapters/RestAdapter.d.ts +94 -0
- package/dist/index.d.ts +12 -13
- package/dist/index.js +374 -192
- package/dist/schema/index.d.ts +1 -3
- package/dist/schema/schemaHelpers.d.ts +5 -1
- package/dist/schema/validator.d.ts +2 -0
- package/dist/types.d.ts +16 -15
- package/package.json +1 -1
- package/src/adapters/RestAdapter.ts +483 -0
- package/src/index.ts +23 -26
- package/src/schema/index.ts +1 -4
- package/src/schema/schemaHelpers.ts +8 -1
- package/src/schema/validator.ts +33 -1
- package/src/types.ts +21 -17
- package/src/utils/jsonConverter.ts +124 -117
- package/src/utils/translationQuery.ts +62 -62
- package/src/schema/defineSchema.ts +0 -164
- package/src/schema/fieldBuilder.ts +0 -293
package/dist/types.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface FieldReference {
|
|
|
25
25
|
onUpdate?: "CASCADE" | "SET_NULL" | "RESTRICT" | "NO_ACTION";
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
|
-
* Complete field definition
|
|
28
|
+
* Complete field definition (normalized from JSON schema)
|
|
29
29
|
*/
|
|
30
30
|
export interface FieldDefinition {
|
|
31
31
|
type: FieldType;
|
|
@@ -47,12 +47,27 @@ export interface FieldDefinition {
|
|
|
47
47
|
enum?: string[];
|
|
48
48
|
match?: string;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Permission roles for table access control
|
|
52
|
+
*/
|
|
53
|
+
export type PermissionRole = "anon" | "user" | "admin";
|
|
54
|
+
/**
|
|
55
|
+
* Permission actions
|
|
56
|
+
*/
|
|
57
|
+
export type PermissionAction = "create" | "read" | "update" | "delete";
|
|
58
|
+
/**
|
|
59
|
+
* Table-level permissions mapping roles to allowed actions.
|
|
60
|
+
* If not defined on a table, no restrictions apply (backward compatible).
|
|
61
|
+
* Enforced by the backend, not by the ORM.
|
|
62
|
+
*/
|
|
63
|
+
export type TablePermissions = Partial<Record<PermissionRole, PermissionAction[]>>;
|
|
50
64
|
/**
|
|
51
65
|
* Table definition with all fields
|
|
52
66
|
*/
|
|
53
67
|
export interface TableDefinition {
|
|
54
68
|
name: string;
|
|
55
69
|
fields: Record<string, FieldDefinition>;
|
|
70
|
+
permissions?: TablePermissions;
|
|
56
71
|
}
|
|
57
72
|
/**
|
|
58
73
|
* Language configuration
|
|
@@ -69,20 +84,6 @@ export interface SchemaDefinition {
|
|
|
69
84
|
languages: LanguageConfig;
|
|
70
85
|
tables: Record<string, TableDefinition>;
|
|
71
86
|
}
|
|
72
|
-
/**
|
|
73
|
-
* Interface for FieldBuilder-like objects (for type checking)
|
|
74
|
-
*/
|
|
75
|
-
export interface FieldBuilderLike {
|
|
76
|
-
build(): FieldDefinition;
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Raw schema input before processing (from user DSL)
|
|
80
|
-
*/
|
|
81
|
-
export interface SchemaInput {
|
|
82
|
-
name?: string;
|
|
83
|
-
languages: string[] | LanguageConfig;
|
|
84
|
-
tables: Record<string, Record<string, FieldBuilderLike>>;
|
|
85
|
-
}
|
|
86
87
|
/**
|
|
87
88
|
* JSON-native type syntax for schema.json
|
|
88
89
|
*
|
package/package.json
CHANGED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST API Adapter
|
|
3
|
+
*
|
|
4
|
+
* Communicates with a backend REST API implementing the /database endpoints.
|
|
5
|
+
* Works in both Node.js and browser environments (uses fetch).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { IDataAdapter } from "./IDataAdapter";
|
|
9
|
+
import type { QueryOptions, PaginatedResult, SchemaDefinition } from "../types";
|
|
10
|
+
import { toTranslationTableName, toTranslationFKName } from "../schema";
|
|
11
|
+
|
|
12
|
+
type FetchFn = (input: string, init?: any) => Promise<any>;
|
|
13
|
+
|
|
14
|
+
export interface RestAdapterConfig {
|
|
15
|
+
/** Base URL for API (e.g., "https://backend.promake.ai/api") */
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
/** Database endpoint prefix (default: "/database") */
|
|
18
|
+
databasePrefix?: string;
|
|
19
|
+
/** Static authorization token for Bearer auth */
|
|
20
|
+
token?: string;
|
|
21
|
+
/** Dynamic token getter - called on every request. Takes priority over static token. */
|
|
22
|
+
getToken?: () => string | null;
|
|
23
|
+
/** Custom headers to include in all requests */
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
/** Schema definition for translation support */
|
|
26
|
+
schema?: SchemaDefinition;
|
|
27
|
+
/** Default language for translations */
|
|
28
|
+
defaultLang?: string;
|
|
29
|
+
/** Custom fetch function (for testing/SSR) */
|
|
30
|
+
fetch?: FetchFn;
|
|
31
|
+
/** Request timeout in ms (default: 30000) */
|
|
32
|
+
timeout?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class RestAdapter implements IDataAdapter {
|
|
36
|
+
private baseUrl: string;
|
|
37
|
+
private databasePrefix: string;
|
|
38
|
+
private token?: string;
|
|
39
|
+
private getTokenFn?: () => string | null;
|
|
40
|
+
private customHeaders: Record<string, string>;
|
|
41
|
+
private fetchFn: FetchFn;
|
|
42
|
+
private timeout: number;
|
|
43
|
+
|
|
44
|
+
schema?: SchemaDefinition;
|
|
45
|
+
defaultLang?: string;
|
|
46
|
+
|
|
47
|
+
constructor(config: RestAdapterConfig) {
|
|
48
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
49
|
+
this.databasePrefix = config.databasePrefix ?? "/database";
|
|
50
|
+
this.token = config.token;
|
|
51
|
+
this.getTokenFn = config.getToken;
|
|
52
|
+
this.customHeaders = config.headers ?? {};
|
|
53
|
+
const gf = globalThis as any;
|
|
54
|
+
this.fetchFn = config.fetch ?? gf.fetch.bind(gf);
|
|
55
|
+
this.timeout = config.timeout ?? 30000;
|
|
56
|
+
this.schema = config.schema;
|
|
57
|
+
this.defaultLang = config.defaultLang;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ==================== Private Helpers ====================
|
|
61
|
+
|
|
62
|
+
private buildUrl(path: string): string {
|
|
63
|
+
return `${this.baseUrl}${this.databasePrefix}${path}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private getHeaders(): Record<string, string> {
|
|
67
|
+
const headers: Record<string, string> = {
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
Accept: "application/json",
|
|
70
|
+
...this.customHeaders,
|
|
71
|
+
};
|
|
72
|
+
const token = this.getTokenFn ? this.getTokenFn() : this.token;
|
|
73
|
+
if (token) {
|
|
74
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
75
|
+
}
|
|
76
|
+
return headers;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private async request<T>(url: string, init?: Record<string, any>): Promise<T> {
|
|
80
|
+
const controller = new AbortController();
|
|
81
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const response: any = await this.fetchFn(url, {
|
|
85
|
+
...init,
|
|
86
|
+
headers: { ...this.getHeaders(), ...(init?.headers ?? {}) },
|
|
87
|
+
signal: controller.signal,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
const body = await response.json().catch(() => null);
|
|
92
|
+
const message =
|
|
93
|
+
body?.message || body?.error || `HTTP ${response.status} ${response.statusText}`;
|
|
94
|
+
throw new Error(message);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return await response.json();
|
|
98
|
+
} finally {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private buildQueryParams(options?: QueryOptions): URLSearchParams {
|
|
104
|
+
const params = new URLSearchParams();
|
|
105
|
+
if (!options) return params;
|
|
106
|
+
|
|
107
|
+
if (options.where && Object.keys(options.where).length > 0) {
|
|
108
|
+
params.set("where", JSON.stringify(options.where));
|
|
109
|
+
}
|
|
110
|
+
if (options.orderBy?.length) {
|
|
111
|
+
params.set(
|
|
112
|
+
"order",
|
|
113
|
+
options.orderBy.map((o) => `${o.field}:${o.direction}`).join(",")
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
if (options.limit != null) {
|
|
117
|
+
params.set("limit", String(options.limit));
|
|
118
|
+
}
|
|
119
|
+
if (options.offset != null) {
|
|
120
|
+
params.set("offset", String(options.offset));
|
|
121
|
+
}
|
|
122
|
+
if (options.lang) {
|
|
123
|
+
params.set("lang", options.lang);
|
|
124
|
+
}
|
|
125
|
+
if (options.fallbackLang) {
|
|
126
|
+
params.set("fallback_lang", options.fallbackLang);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return params;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private urlWithParams(base: string, params: URLSearchParams): string {
|
|
133
|
+
const qs = params.toString();
|
|
134
|
+
return qs ? `${base}?${qs}` : base;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ==================== Lifecycle ====================
|
|
138
|
+
|
|
139
|
+
setSchema(schema: SchemaDefinition): void {
|
|
140
|
+
this.schema = schema;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async connect(): Promise<void> {
|
|
144
|
+
if (!this.schema) return;
|
|
145
|
+
|
|
146
|
+
// Convert internal SchemaDefinition to the API's expected format
|
|
147
|
+
const apiSchema: Record<string, { columns: Record<string, unknown>[] }> = {};
|
|
148
|
+
|
|
149
|
+
for (const [tableName, tableDef] of Object.entries(this.schema.tables)) {
|
|
150
|
+
const columns: Record<string, unknown>[] = [];
|
|
151
|
+
for (const [fieldName, fieldDef] of Object.entries(tableDef.fields)) {
|
|
152
|
+
const col: Record<string, unknown> = {
|
|
153
|
+
name: fieldName,
|
|
154
|
+
type: this.mapFieldType(fieldDef.type),
|
|
155
|
+
};
|
|
156
|
+
if (fieldDef.primary) col.primary_key = true;
|
|
157
|
+
if (!fieldDef.nullable) col.not_null = true;
|
|
158
|
+
if (fieldDef.unique) col.unique = true;
|
|
159
|
+
if (fieldDef.default !== undefined) col.default_value = String(fieldDef.default);
|
|
160
|
+
if (fieldDef.translatable) col.translatable = true;
|
|
161
|
+
columns.push(col);
|
|
162
|
+
}
|
|
163
|
+
const tableEntry: Record<string, unknown> = { columns };
|
|
164
|
+
if (tableDef.permissions) {
|
|
165
|
+
tableEntry.permissions = tableDef.permissions;
|
|
166
|
+
}
|
|
167
|
+
apiSchema[tableName] = tableEntry as { columns: Record<string, unknown>[] };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
await this.request(this.buildUrl("/generate"), {
|
|
171
|
+
method: "POST",
|
|
172
|
+
body: JSON.stringify({ schema: { tables: apiSchema } }),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private mapFieldType(type: string): string {
|
|
177
|
+
switch (type) {
|
|
178
|
+
case "id":
|
|
179
|
+
case "int":
|
|
180
|
+
return "INTEGER";
|
|
181
|
+
case "string":
|
|
182
|
+
case "text":
|
|
183
|
+
case "timestamp":
|
|
184
|
+
return "TEXT";
|
|
185
|
+
case "decimal":
|
|
186
|
+
return "REAL";
|
|
187
|
+
case "bool":
|
|
188
|
+
return "INTEGER";
|
|
189
|
+
case "json":
|
|
190
|
+
case "object":
|
|
191
|
+
case "object[]":
|
|
192
|
+
case "string[]":
|
|
193
|
+
case "number[]":
|
|
194
|
+
case "boolean[]":
|
|
195
|
+
return "TEXT";
|
|
196
|
+
default:
|
|
197
|
+
return "TEXT";
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
close(): void {
|
|
202
|
+
// No-op for REST
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ==================== Query Methods ====================
|
|
206
|
+
|
|
207
|
+
async list<T = unknown>(table: string, options?: QueryOptions): Promise<T[]> {
|
|
208
|
+
const params = this.buildQueryParams(options);
|
|
209
|
+
const url = this.urlWithParams(this.buildUrl(`/${table}/list`), params);
|
|
210
|
+
const res = await this.request<{ data: T[] }>(url);
|
|
211
|
+
return res.data;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async get<T = unknown>(
|
|
215
|
+
table: string,
|
|
216
|
+
id: string | number,
|
|
217
|
+
options?: QueryOptions
|
|
218
|
+
): Promise<T | null> {
|
|
219
|
+
const params = new URLSearchParams();
|
|
220
|
+
if (options?.lang) params.set("lang", options.lang);
|
|
221
|
+
if (options?.fallbackLang) params.set("fallback_lang", options.fallbackLang);
|
|
222
|
+
const url = this.urlWithParams(this.buildUrl(`/${table}/${id}`), params);
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const res = await this.request<{ data: T }>(url);
|
|
226
|
+
return res.data ?? null;
|
|
227
|
+
} catch (err) {
|
|
228
|
+
if (err instanceof Error && err.message.includes("404")) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
throw err;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async findOne<T = unknown>(
|
|
236
|
+
table: string,
|
|
237
|
+
options?: QueryOptions
|
|
238
|
+
): Promise<T | null> {
|
|
239
|
+
const results = await this.list<T>(table, { ...options, limit: 1 });
|
|
240
|
+
return results[0] ?? null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async count(table: string, options?: QueryOptions): Promise<number> {
|
|
244
|
+
const params = new URLSearchParams();
|
|
245
|
+
if (options?.where && Object.keys(options.where).length > 0) {
|
|
246
|
+
params.set("where", JSON.stringify(options.where));
|
|
247
|
+
}
|
|
248
|
+
const url = this.urlWithParams(this.buildUrl(`/${table}/count`), params);
|
|
249
|
+
const res = await this.request<{ count: number }>(url);
|
|
250
|
+
return res.count;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async paginate<T = unknown>(
|
|
254
|
+
table: string,
|
|
255
|
+
page: number,
|
|
256
|
+
limit: number,
|
|
257
|
+
options?: QueryOptions
|
|
258
|
+
): Promise<PaginatedResult<T>> {
|
|
259
|
+
const offset = (page - 1) * limit;
|
|
260
|
+
const [total, data] = await Promise.all([
|
|
261
|
+
this.count(table, options),
|
|
262
|
+
this.list<T>(table, { ...options, limit, offset }),
|
|
263
|
+
]);
|
|
264
|
+
const totalPages = Math.ceil(total / limit);
|
|
265
|
+
return {
|
|
266
|
+
data,
|
|
267
|
+
page,
|
|
268
|
+
limit,
|
|
269
|
+
total,
|
|
270
|
+
totalPages,
|
|
271
|
+
hasMore: page < totalPages,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ==================== Write Methods ====================
|
|
276
|
+
|
|
277
|
+
async create<T = unknown>(
|
|
278
|
+
table: string,
|
|
279
|
+
data: Record<string, unknown>
|
|
280
|
+
): Promise<T> {
|
|
281
|
+
const url = this.buildUrl(`/${table}/create`);
|
|
282
|
+
const res = await this.request<{ data: T; id: unknown }>(url, {
|
|
283
|
+
method: "POST",
|
|
284
|
+
body: JSON.stringify({ data }),
|
|
285
|
+
});
|
|
286
|
+
// Backend returns id at top level, merge it into data
|
|
287
|
+
const result = (res.data ?? {}) as Record<string, unknown>;
|
|
288
|
+
if (res.id !== undefined && !("id" in result)) {
|
|
289
|
+
result.id = res.id;
|
|
290
|
+
}
|
|
291
|
+
return result as T;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async update<T = unknown>(
|
|
295
|
+
table: string,
|
|
296
|
+
id: string | number,
|
|
297
|
+
data: Record<string, unknown>,
|
|
298
|
+
options?: { translations?: Record<string, Record<string, unknown>> }
|
|
299
|
+
): Promise<T> {
|
|
300
|
+
const url = this.buildUrl(`/${table}/${id}`);
|
|
301
|
+
const body: Record<string, unknown> = { data };
|
|
302
|
+
if (options?.translations) {
|
|
303
|
+
body.translations = options.translations;
|
|
304
|
+
}
|
|
305
|
+
const res = await this.request<{ data: T }>(url, {
|
|
306
|
+
method: "PUT",
|
|
307
|
+
body: JSON.stringify(body),
|
|
308
|
+
});
|
|
309
|
+
return res.data;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async delete(table: string, id: string | number): Promise<boolean> {
|
|
313
|
+
const url = this.buildUrl(`/${table}/${id}`);
|
|
314
|
+
try {
|
|
315
|
+
const res = await this.request<{ deleted: boolean }>(url, {
|
|
316
|
+
method: "DELETE",
|
|
317
|
+
});
|
|
318
|
+
return res.deleted ?? true;
|
|
319
|
+
} catch (err) {
|
|
320
|
+
if (err instanceof Error && err.message.includes("404")) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
throw err;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ==================== Batch Methods ====================
|
|
328
|
+
|
|
329
|
+
async createMany<T = unknown>(
|
|
330
|
+
table: string,
|
|
331
|
+
records: Record<string, unknown>[],
|
|
332
|
+
options?: { ignore?: boolean; noAtomic?: boolean }
|
|
333
|
+
): Promise<{ created: number; ids: (number | bigint)[] }> {
|
|
334
|
+
const url = this.buildUrl(`/${table}/create-batch`);
|
|
335
|
+
const res = await this.request<{ created: number; ids: (number | bigint)[] }>(url, {
|
|
336
|
+
method: "POST",
|
|
337
|
+
body: JSON.stringify({
|
|
338
|
+
records,
|
|
339
|
+
ignore: options?.ignore ?? false,
|
|
340
|
+
no_atomic: options?.noAtomic ?? false,
|
|
341
|
+
}),
|
|
342
|
+
});
|
|
343
|
+
return { created: res.created, ids: res.ids };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async updateMany(
|
|
347
|
+
table: string,
|
|
348
|
+
updates: { id: number | string; data: Record<string, unknown> }[],
|
|
349
|
+
options?: { noAtomic?: boolean }
|
|
350
|
+
): Promise<{ updated: number }> {
|
|
351
|
+
const url = this.buildUrl(`/${table}/update-batch`);
|
|
352
|
+
const res = await this.request<{ updated: number }>(url, {
|
|
353
|
+
method: "POST",
|
|
354
|
+
body: JSON.stringify({
|
|
355
|
+
updates,
|
|
356
|
+
no_atomic: options?.noAtomic ?? false,
|
|
357
|
+
}),
|
|
358
|
+
});
|
|
359
|
+
return { updated: res.updated };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async deleteMany(
|
|
363
|
+
table: string,
|
|
364
|
+
ids: (number | string)[],
|
|
365
|
+
options?: { noAtomic?: boolean }
|
|
366
|
+
): Promise<{ deleted: number }> {
|
|
367
|
+
const url = this.buildUrl(`/${table}/delete-batch`);
|
|
368
|
+
const res = await this.request<{ deleted: number }>(url, {
|
|
369
|
+
method: "POST",
|
|
370
|
+
body: JSON.stringify({
|
|
371
|
+
ids,
|
|
372
|
+
no_atomic: options?.noAtomic ?? false,
|
|
373
|
+
}),
|
|
374
|
+
});
|
|
375
|
+
return { deleted: res.deleted };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ==================== Translation Methods ====================
|
|
379
|
+
|
|
380
|
+
async createWithTranslations<T = unknown>(
|
|
381
|
+
table: string,
|
|
382
|
+
data: Record<string, unknown>,
|
|
383
|
+
translations?: Record<string, Record<string, unknown>>
|
|
384
|
+
): Promise<T> {
|
|
385
|
+
const url = this.buildUrl(`/${table}/create`);
|
|
386
|
+
const res = await this.request<{ data: T; id: unknown }>(url, {
|
|
387
|
+
method: "POST",
|
|
388
|
+
body: JSON.stringify({ data, translations }),
|
|
389
|
+
});
|
|
390
|
+
// Backend returns id at top level, merge it into data
|
|
391
|
+
const result = (res.data ?? {}) as Record<string, unknown>;
|
|
392
|
+
if (res.id !== undefined && !("id" in result)) {
|
|
393
|
+
result.id = res.id;
|
|
394
|
+
}
|
|
395
|
+
return result as T;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async upsertTranslation(
|
|
399
|
+
table: string,
|
|
400
|
+
id: string | number,
|
|
401
|
+
lang: string,
|
|
402
|
+
data: Record<string, unknown>
|
|
403
|
+
): Promise<void> {
|
|
404
|
+
const url = this.buildUrl(`/${table}/${id}`);
|
|
405
|
+
await this.request(url, {
|
|
406
|
+
method: "PUT",
|
|
407
|
+
body: JSON.stringify({ data, lang }),
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async getTranslations<T = unknown>(
|
|
412
|
+
table: string,
|
|
413
|
+
id: string | number
|
|
414
|
+
): Promise<T[]> {
|
|
415
|
+
const translationTable = toTranslationTableName(table);
|
|
416
|
+
const fkName = toTranslationFKName(table);
|
|
417
|
+
const params = new URLSearchParams();
|
|
418
|
+
params.set("where", JSON.stringify({ [fkName]: id }));
|
|
419
|
+
const url = this.urlWithParams(
|
|
420
|
+
this.buildUrl(`/${translationTable}/list`),
|
|
421
|
+
params
|
|
422
|
+
);
|
|
423
|
+
const res = await this.request<{ data: T[] }>(url);
|
|
424
|
+
return res.data;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ==================== Raw Query Methods ====================
|
|
428
|
+
|
|
429
|
+
async raw<T = unknown>(query: string, params?: unknown[]): Promise<T[]> {
|
|
430
|
+
const url = this.buildUrl("/exec");
|
|
431
|
+
const res = await this.request<{ data: T[] }>(url, {
|
|
432
|
+
method: "POST",
|
|
433
|
+
body: JSON.stringify({ sql: query, params: params ?? [] }),
|
|
434
|
+
});
|
|
435
|
+
return res.data;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async execute(
|
|
439
|
+
query: string,
|
|
440
|
+
params?: unknown[]
|
|
441
|
+
): Promise<{ changes: number; lastInsertRowid: number | bigint }> {
|
|
442
|
+
const url = this.buildUrl("/exec");
|
|
443
|
+
const res = await this.request<{
|
|
444
|
+
changes: number;
|
|
445
|
+
last_insert_rowid: number;
|
|
446
|
+
}>(url, {
|
|
447
|
+
method: "POST",
|
|
448
|
+
body: JSON.stringify({ sql: query, params: params ?? [] }),
|
|
449
|
+
});
|
|
450
|
+
return {
|
|
451
|
+
changes: res.changes,
|
|
452
|
+
lastInsertRowid: res.last_insert_rowid ?? 0,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ==================== Transaction Methods ====================
|
|
457
|
+
|
|
458
|
+
async beginTransaction(): Promise<void> {
|
|
459
|
+
// No-op: REST API handles atomicity per-request
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async commit(): Promise<void> {
|
|
463
|
+
// No-op
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async rollback(): Promise<void> {
|
|
467
|
+
// No-op
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ==================== Schema Methods ====================
|
|
471
|
+
|
|
472
|
+
async getTables(): Promise<string[]> {
|
|
473
|
+
const url = this.buildUrl("/tables");
|
|
474
|
+
const res = await this.request<{ tables: string[] }>(url);
|
|
475
|
+
return res.tables;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async getTableSchema(table: string): Promise<unknown[]> {
|
|
479
|
+
const url = this.buildUrl(`/${table}/schema`);
|
|
480
|
+
const res = await this.request<{ columns: unknown[] }>(url);
|
|
481
|
+
return res.columns;
|
|
482
|
+
}
|
|
483
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -5,21 +5,21 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @example
|
|
7
7
|
* ```typescript
|
|
8
|
-
* import { ORM,
|
|
8
|
+
* import { ORM, parseJSONSchema } from '@promakeai/orm';
|
|
9
9
|
*
|
|
10
|
-
* //
|
|
11
|
-
* const schema =
|
|
10
|
+
* // Parse JSON schema
|
|
11
|
+
* const schema = parseJSONSchema({
|
|
12
12
|
* languages: ['en', 'tr'],
|
|
13
13
|
* tables: {
|
|
14
14
|
* products: {
|
|
15
|
-
* id:
|
|
16
|
-
* name:
|
|
17
|
-
* price:
|
|
18
|
-
* categoryId:
|
|
15
|
+
* id: { type: 'id' },
|
|
16
|
+
* name: { type: 'string', translatable: true, required: true },
|
|
17
|
+
* price: { type: 'decimal', required: true },
|
|
18
|
+
* categoryId: { type: 'int', ref: 'categories' },
|
|
19
19
|
* },
|
|
20
20
|
* categories: {
|
|
21
|
-
* id:
|
|
22
|
-
* name:
|
|
21
|
+
* id: { type: 'id' },
|
|
22
|
+
* name: { type: 'string', translatable: true, required: true },
|
|
23
23
|
* },
|
|
24
24
|
* },
|
|
25
25
|
* });
|
|
@@ -39,14 +39,8 @@ export { ORM } from "./ORM";
|
|
|
39
39
|
// Adapter Interface
|
|
40
40
|
export type { IDataAdapter } from "./adapters/IDataAdapter";
|
|
41
41
|
|
|
42
|
-
//
|
|
43
|
-
export {
|
|
44
|
-
defineSchema,
|
|
45
|
-
f,
|
|
46
|
-
mergeSchemas,
|
|
47
|
-
createSchemaUnsafe,
|
|
48
|
-
} from "./schema";
|
|
49
|
-
export type { FieldBuilder } from "./schema";
|
|
42
|
+
// REST Adapter
|
|
43
|
+
export { RestAdapter, type RestAdapterConfig } from "./adapters/RestAdapter";
|
|
50
44
|
|
|
51
45
|
// Schema Validation
|
|
52
46
|
export {
|
|
@@ -86,6 +80,7 @@ export {
|
|
|
86
80
|
getRequiredFields,
|
|
87
81
|
getRefTarget,
|
|
88
82
|
getRefTargetFull,
|
|
83
|
+
getTablePermissions,
|
|
89
84
|
} from "./schema";
|
|
90
85
|
|
|
91
86
|
// Query Builder
|
|
@@ -113,12 +108,12 @@ export {
|
|
|
113
108
|
} from "./utils/populateResolver";
|
|
114
109
|
export type { PopulateAdapter } from "./utils/populateResolver";
|
|
115
110
|
|
|
116
|
-
// JSON Schema Converter
|
|
117
|
-
export {
|
|
118
|
-
parseJSONSchema,
|
|
119
|
-
parseJSONSchemaWithWarnings,
|
|
120
|
-
} from "./utils/jsonConverter";
|
|
121
|
-
export type { ParseJSONSchemaResult } from "./utils/jsonConverter";
|
|
111
|
+
// JSON Schema Converter
|
|
112
|
+
export {
|
|
113
|
+
parseJSONSchema,
|
|
114
|
+
parseJSONSchemaWithWarnings,
|
|
115
|
+
} from "./utils/jsonConverter";
|
|
116
|
+
export type { ParseJSONSchemaResult } from "./utils/jsonConverter";
|
|
122
117
|
|
|
123
118
|
// Type Helpers
|
|
124
119
|
export { isJsonType } from "./types";
|
|
@@ -132,13 +127,15 @@ export type {
|
|
|
132
127
|
FieldType,
|
|
133
128
|
FieldDefinition,
|
|
134
129
|
FieldReference,
|
|
135
|
-
FieldBuilderLike,
|
|
136
|
-
|
|
137
130
|
// Schema types
|
|
138
131
|
TableDefinition,
|
|
139
132
|
LanguageConfig,
|
|
140
133
|
SchemaDefinition,
|
|
141
|
-
|
|
134
|
+
|
|
135
|
+
// Permission types
|
|
136
|
+
PermissionRole,
|
|
137
|
+
PermissionAction,
|
|
138
|
+
TablePermissions,
|
|
142
139
|
|
|
143
140
|
// JSON Schema types (AI-friendly)
|
|
144
141
|
JSONFieldType,
|
package/src/schema/index.ts
CHANGED
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
* Schema Module Exports
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
// Main API
|
|
6
|
-
export { defineSchema, f, mergeSchemas, createSchemaUnsafe } from "./defineSchema";
|
|
7
|
-
export type { FieldBuilder } from "./fieldBuilder";
|
|
8
|
-
|
|
9
5
|
// Validation
|
|
10
6
|
export {
|
|
11
7
|
validateSchema,
|
|
@@ -44,4 +40,5 @@ export {
|
|
|
44
40
|
getRequiredFields,
|
|
45
41
|
getRefTarget,
|
|
46
42
|
getRefTargetFull,
|
|
43
|
+
getTablePermissions,
|
|
47
44
|
} from "./schemaHelpers";
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Utilities for working with schema definitions.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { TableDefinition, FieldDefinition, FieldReference } from "../types";
|
|
7
|
+
import type { TableDefinition, FieldDefinition, FieldReference, TablePermissions } from "../types";
|
|
8
8
|
import { isJsonType } from "../types";
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -122,3 +122,10 @@ export function getRefTargetFull(
|
|
|
122
122
|
}
|
|
123
123
|
return { table: field.ref.table, field: field.ref.field || "id" };
|
|
124
124
|
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get table permissions (undefined if no restrictions)
|
|
128
|
+
*/
|
|
129
|
+
export function getTablePermissions(table: TableDefinition): TablePermissions | undefined {
|
|
130
|
+
return table.permissions;
|
|
131
|
+
}
|