@promakeai/orm 1.0.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/index.js ADDED
@@ -0,0 +1,1054 @@
1
+ // src/utils/populateResolver.ts
2
+ function getRefInfo(fieldName, field) {
3
+ if (!field.ref)
4
+ return null;
5
+ const ref = field.ref;
6
+ const isString = typeof ref === "string";
7
+ return {
8
+ table: isString ? ref : ref.table,
9
+ field: isString ? "id" : ref.field || "id",
10
+ localField: fieldName,
11
+ targetField: isString ? "id" : ref.field || "id",
12
+ isArray: Array.isArray(field.default) || fieldName.endsWith("Ids")
13
+ };
14
+ }
15
+ function toPopulatedName(fieldName) {
16
+ if (fieldName.endsWith("Ids")) {
17
+ return fieldName.slice(0, -3) + "s";
18
+ }
19
+ if (fieldName.endsWith("Id")) {
20
+ return fieldName.slice(0, -2);
21
+ }
22
+ if (fieldName.endsWith("_id")) {
23
+ return fieldName.slice(0, -3);
24
+ }
25
+ return fieldName;
26
+ }
27
+ function extractUniqueValues(records, fieldName) {
28
+ const values = new Set;
29
+ for (const record of records) {
30
+ const value = record[fieldName];
31
+ if (value == null)
32
+ continue;
33
+ if (Array.isArray(value)) {
34
+ for (const v of value) {
35
+ if (v != null)
36
+ values.add(v);
37
+ }
38
+ } else {
39
+ values.add(value);
40
+ }
41
+ }
42
+ return Array.from(values);
43
+ }
44
+ function createLookupMap(records, keyField) {
45
+ const map = new Map;
46
+ for (const record of records) {
47
+ const key = record[keyField];
48
+ if (key != null) {
49
+ map.set(key, record);
50
+ }
51
+ }
52
+ return map;
53
+ }
54
+ function normalizePopulate(populate) {
55
+ if (!populate)
56
+ return [];
57
+ if (typeof populate === "string") {
58
+ return populate.split(/\s+/).map((fieldName) => ({ fieldName }));
59
+ }
60
+ if (Array.isArray(populate)) {
61
+ return populate.map((fieldName) => ({ fieldName }));
62
+ }
63
+ const entries = [];
64
+ for (const [fieldName, value] of Object.entries(populate)) {
65
+ if (value === true) {
66
+ entries.push({ fieldName });
67
+ } else if (value && typeof value === "object") {
68
+ entries.push({ fieldName, options: value });
69
+ }
70
+ }
71
+ return entries;
72
+ }
73
+ async function resolvePopulate(records, table, populate, schema, adapter) {
74
+ if (!records.length)
75
+ return records;
76
+ const populateEntries = normalizePopulate(populate);
77
+ if (!populateEntries.length)
78
+ return records;
79
+ const tableDef = schema.tables[table];
80
+ if (!tableDef)
81
+ return records;
82
+ for (const entry of populateEntries) {
83
+ const { fieldName, options } = entry;
84
+ const field = tableDef.fields[fieldName];
85
+ if (!field)
86
+ continue;
87
+ const refInfo = getRefInfo(fieldName, field);
88
+ if (!refInfo)
89
+ continue;
90
+ const fkValues = extractUniqueValues(records, fieldName);
91
+ if (!fkValues.length)
92
+ continue;
93
+ const refRecords = await adapter.findMany(refInfo.table, {
94
+ where: { [refInfo.targetField]: { $in: fkValues } }
95
+ });
96
+ let populatedRefRecords = refRecords;
97
+ if (options?.populate) {
98
+ populatedRefRecords = await resolvePopulate(refRecords, refInfo.table, options.populate, schema, adapter);
99
+ }
100
+ const lookupMap = createLookupMap(populatedRefRecords, refInfo.targetField);
101
+ const populatedName = toPopulatedName(fieldName);
102
+ for (const record of records) {
103
+ const fkValue = record[fieldName];
104
+ if (fkValue == null) {
105
+ record[populatedName] = null;
106
+ continue;
107
+ }
108
+ if (refInfo.isArray && Array.isArray(fkValue)) {
109
+ record[populatedName] = fkValue.map((v) => lookupMap.get(v)).filter(Boolean);
110
+ } else {
111
+ record[populatedName] = lookupMap.get(fkValue) || null;
112
+ }
113
+ }
114
+ }
115
+ return records;
116
+ }
117
+ function getPopulatableFields(tableDef) {
118
+ return Object.entries(tableDef.fields).filter(([_, field]) => field.ref != null).map(([name]) => name);
119
+ }
120
+ function validatePopulate(populate, table, schema) {
121
+ const errors = [];
122
+ const populateEntries = normalizePopulate(populate);
123
+ const tableDef = schema.tables[table];
124
+ if (!tableDef) {
125
+ errors.push(`Table '${table}' not found in schema`);
126
+ return { valid: false, errors };
127
+ }
128
+ for (const entry of populateEntries) {
129
+ const { fieldName, options } = entry;
130
+ const field = tableDef.fields[fieldName];
131
+ if (!field) {
132
+ errors.push(`Field '${fieldName}' not found in table '${table}'`);
133
+ continue;
134
+ }
135
+ if (!field.ref) {
136
+ errors.push(`Field '${fieldName}' in table '${table}' is not a reference field`);
137
+ continue;
138
+ }
139
+ if (options?.populate) {
140
+ const refInfo = getRefInfo(fieldName, field);
141
+ if (refInfo) {
142
+ const nestedValidation = validatePopulate(options.populate, refInfo.table, schema);
143
+ errors.push(...nestedValidation.errors);
144
+ }
145
+ }
146
+ }
147
+ return { valid: errors.length === 0, errors };
148
+ }
149
+
150
+ // src/ORM.ts
151
+ class ORM {
152
+ adapter;
153
+ schema;
154
+ defaultLang;
155
+ fallbackLang;
156
+ constructor(adapter, config) {
157
+ this.adapter = adapter;
158
+ this.schema = config?.schema;
159
+ this.defaultLang = config?.defaultLang || "en";
160
+ this.fallbackLang = config?.fallbackLang;
161
+ }
162
+ async list(table, options) {
163
+ const opts = this.normalizeOptions(options);
164
+ const { populate, ...queryOpts } = opts;
165
+ let records = await this.adapter.list(table, queryOpts);
166
+ if (populate && this.schema) {
167
+ records = await this.resolvePopulateForRecords(records, table, populate);
168
+ }
169
+ return records;
170
+ }
171
+ async get(table, id, options) {
172
+ const opts = this.normalizeOptions(options);
173
+ const { populate, ...queryOpts } = opts;
174
+ const record = await this.adapter.get(table, id, queryOpts);
175
+ if (record && populate && this.schema) {
176
+ const [populated] = await this.resolvePopulateForRecords([record], table, populate);
177
+ return populated;
178
+ }
179
+ return record;
180
+ }
181
+ async count(table, options) {
182
+ const opts = this.normalizeOptions(options);
183
+ return this.adapter.count(table, opts);
184
+ }
185
+ async paginate(table, page, limit, options) {
186
+ const opts = this.normalizeOptions(options);
187
+ return this.adapter.paginate(table, page, limit, opts);
188
+ }
189
+ async create(table, data) {
190
+ if (this.schema) {
191
+ this.validateData(table, data);
192
+ }
193
+ return this.adapter.create(table, data);
194
+ }
195
+ async update(table, id, data) {
196
+ if (this.schema) {
197
+ this.validateData(table, data, true);
198
+ }
199
+ return this.adapter.update(table, id, data);
200
+ }
201
+ async delete(table, id) {
202
+ return this.adapter.delete(table, id);
203
+ }
204
+ async createMany(table, records, options) {
205
+ return this.adapter.createMany(table, records, options);
206
+ }
207
+ async updateMany(table, updates) {
208
+ return this.adapter.updateMany(table, updates);
209
+ }
210
+ async deleteMany(table, ids) {
211
+ return this.adapter.deleteMany(table, ids);
212
+ }
213
+ async raw(query, params) {
214
+ return this.adapter.raw(query, params);
215
+ }
216
+ async execute(query, params) {
217
+ return this.adapter.execute(query, params);
218
+ }
219
+ async transaction(callback) {
220
+ await this.adapter.beginTransaction();
221
+ try {
222
+ const result = await callback(this);
223
+ await this.adapter.commit();
224
+ return result;
225
+ } catch (error) {
226
+ await this.adapter.rollback();
227
+ throw error;
228
+ }
229
+ }
230
+ async getTables() {
231
+ if (this.adapter.getTables) {
232
+ return this.adapter.getTables();
233
+ }
234
+ throw new Error("Adapter does not support getTables()");
235
+ }
236
+ async getTableSchema(table) {
237
+ if (this.adapter.getTableSchema) {
238
+ return this.adapter.getTableSchema(table);
239
+ }
240
+ throw new Error("Adapter does not support getTableSchema()");
241
+ }
242
+ async close() {
243
+ return this.adapter.close();
244
+ }
245
+ normalizeOptions(options) {
246
+ return {
247
+ ...options,
248
+ lang: options?.lang || this.defaultLang,
249
+ fallbackLang: options?.fallbackLang || this.fallbackLang
250
+ };
251
+ }
252
+ validateData(table, data, isUpdate = false) {
253
+ if (!this.schema)
254
+ return;
255
+ const tableDef = this.schema.tables[table];
256
+ if (!tableDef) {
257
+ throw new Error(`Table "${table}" not found in schema`);
258
+ }
259
+ }
260
+ async resolvePopulateForRecords(records, table, populate) {
261
+ if (!this.schema)
262
+ return records;
263
+ const validation = validatePopulate(populate, table, this.schema);
264
+ if (!validation.valid) {
265
+ console.warn("Populate validation warnings:", validation.errors);
266
+ }
267
+ const adapterWrapper = {
268
+ findMany: (t, opts) => {
269
+ return this.adapter.list(t, opts);
270
+ }
271
+ };
272
+ return resolvePopulate(records, table, populate, this.schema, adapterWrapper);
273
+ }
274
+ getAdapter() {
275
+ return this.adapter;
276
+ }
277
+ getSchema() {
278
+ return this.schema;
279
+ }
280
+ setSchema(schema) {
281
+ this.schema = schema;
282
+ }
283
+ }
284
+ // src/schema/schemaHelpers.ts
285
+ function getTranslatableFields(table) {
286
+ return Object.entries(table.fields).filter(([_, field]) => field.translatable).map(([name]) => name);
287
+ }
288
+ function getNonTranslatableFields(table) {
289
+ return Object.entries(table.fields).filter(([_, field]) => !field.translatable && !field.primary).map(([name]) => name);
290
+ }
291
+ function getInsertableFields(table) {
292
+ return Object.entries(table.fields).filter(([_, field]) => !field.primary).map(([name]) => name);
293
+ }
294
+ function hasTranslatableFields(table) {
295
+ return getTranslatableFields(table).length > 0;
296
+ }
297
+ function getPrimaryKeyField(table) {
298
+ const entry = Object.entries(table.fields).find(([_, field]) => field.primary);
299
+ return entry ? entry[0] : null;
300
+ }
301
+ function getReferenceFields(table) {
302
+ return Object.entries(table.fields).filter(([_, field]) => field.ref).map(([name, field]) => {
303
+ const ref = field.ref;
304
+ if (typeof ref === "string") {
305
+ return [name, { table: ref, field: "id" }];
306
+ }
307
+ return [name, { ...ref, field: ref.field || "id" }];
308
+ });
309
+ }
310
+ function getMainTableFields(table) {
311
+ return Object.entries(table.fields).filter(([_, field]) => !field.translatable);
312
+ }
313
+ function getTranslationTableFields(table) {
314
+ return Object.entries(table.fields).filter(([_, field]) => field.translatable);
315
+ }
316
+ function isRequiredField(field) {
317
+ return !field.nullable && field.default === undefined && !field.primary;
318
+ }
319
+ function getRequiredFields(table) {
320
+ return Object.entries(table.fields).filter(([_, field]) => isRequiredField(field)).map(([name]) => name);
321
+ }
322
+ function getRefTarget(field) {
323
+ if (!field.ref)
324
+ return null;
325
+ return typeof field.ref === "string" ? field.ref : field.ref.table;
326
+ }
327
+ function getRefTargetFull(field) {
328
+ if (!field.ref)
329
+ return null;
330
+ if (typeof field.ref === "string") {
331
+ return { table: field.ref, field: "id" };
332
+ }
333
+ return { table: field.ref.table, field: field.ref.field || "id" };
334
+ }
335
+
336
+ // src/schema/validator.ts
337
+ var ValidationErrorCode = {
338
+ MISSING_PRIMARY_KEY: "MISSING_PRIMARY_KEY",
339
+ INVALID_REFERENCE: "INVALID_REFERENCE",
340
+ INVALID_TRANSLATABLE_TYPE: "INVALID_TRANSLATABLE_TYPE",
341
+ NO_LANGUAGES: "NO_LANGUAGES",
342
+ DUPLICATE_TABLE: "DUPLICATE_TABLE",
343
+ RESERVED_FIELD_NAME: "RESERVED_FIELD_NAME",
344
+ SELF_REFERENCE_ON_REQUIRED: "SELF_REFERENCE_ON_REQUIRED"
345
+ };
346
+ var RESERVED_FIELDS = ["language_code"];
347
+ function validateSchema(schema) {
348
+ const errors = [];
349
+ const tableNames = Object.keys(schema.tables);
350
+ if (!schema.languages.supported || schema.languages.supported.length === 0) {
351
+ errors.push({
352
+ table: "_schema",
353
+ message: "At least one language must be defined",
354
+ code: ValidationErrorCode.NO_LANGUAGES
355
+ });
356
+ }
357
+ for (const [tableName, table] of Object.entries(schema.tables)) {
358
+ const hasPrimaryKey = Object.values(table.fields).some((field) => field.primary);
359
+ if (!hasPrimaryKey) {
360
+ errors.push({
361
+ table: tableName,
362
+ message: "Table must have a primary key (use f.id())",
363
+ code: ValidationErrorCode.MISSING_PRIMARY_KEY
364
+ });
365
+ }
366
+ for (const [fieldName, field] of Object.entries(table.fields)) {
367
+ if (RESERVED_FIELDS.includes(fieldName)) {
368
+ errors.push({
369
+ table: tableName,
370
+ field: fieldName,
371
+ message: `'${fieldName}' is a reserved field name`,
372
+ code: ValidationErrorCode.RESERVED_FIELD_NAME
373
+ });
374
+ }
375
+ const refTarget = getRefTarget(field);
376
+ if (refTarget) {
377
+ if (!tableNames.includes(refTarget)) {
378
+ errors.push({
379
+ table: tableName,
380
+ field: fieldName,
381
+ message: `Foreign key references non-existent table: ${refTarget}`,
382
+ code: ValidationErrorCode.INVALID_REFERENCE
383
+ });
384
+ }
385
+ if (refTarget === tableName && !field.nullable) {
386
+ errors.push({
387
+ table: tableName,
388
+ field: fieldName,
389
+ message: `Self-referencing foreign key must be nullable`,
390
+ code: ValidationErrorCode.SELF_REFERENCE_ON_REQUIRED
391
+ });
392
+ }
393
+ }
394
+ if (field.translatable && !["string", "text"].includes(field.type)) {
395
+ errors.push({
396
+ table: tableName,
397
+ field: fieldName,
398
+ message: `Only string/text fields can be translatable, got: ${field.type}`,
399
+ code: ValidationErrorCode.INVALID_TRANSLATABLE_TYPE
400
+ });
401
+ }
402
+ }
403
+ }
404
+ return errors;
405
+ }
406
+ function assertValidSchema(schema) {
407
+ const errors = validateSchema(schema);
408
+ if (errors.length > 0) {
409
+ const messages = errors.map((e) => e.field ? `[${e.table}.${e.field}] ${e.message}` : `[${e.table}] ${e.message}`);
410
+ throw new Error(`Schema validation failed:
411
+ ${messages.join(`
412
+ `)}`);
413
+ }
414
+ }
415
+ function isValidSchema(schema) {
416
+ return validateSchema(schema).length === 0;
417
+ }
418
+ function validateTable(tableName, table, allTableNames) {
419
+ const errors = [];
420
+ const hasPrimaryKey = Object.values(table.fields).some((field) => field.primary);
421
+ if (!hasPrimaryKey) {
422
+ errors.push({
423
+ table: tableName,
424
+ message: "Table must have a primary key",
425
+ code: ValidationErrorCode.MISSING_PRIMARY_KEY
426
+ });
427
+ }
428
+ for (const [fieldName, field] of Object.entries(table.fields)) {
429
+ const refTarget = getRefTarget(field);
430
+ if (refTarget && !allTableNames.includes(refTarget)) {
431
+ errors.push({
432
+ table: tableName,
433
+ field: fieldName,
434
+ message: `Foreign key references non-existent table: ${refTarget}`,
435
+ code: ValidationErrorCode.INVALID_REFERENCE
436
+ });
437
+ }
438
+ if (field.translatable && !["string", "text"].includes(field.type)) {
439
+ errors.push({
440
+ table: tableName,
441
+ field: fieldName,
442
+ message: `Only string/text fields can be translatable`,
443
+ code: ValidationErrorCode.INVALID_TRANSLATABLE_TYPE
444
+ });
445
+ }
446
+ }
447
+ return errors;
448
+ }
449
+
450
+ // src/schema/fieldBuilder.ts
451
+ class FieldBuilder {
452
+ definition;
453
+ constructor(type) {
454
+ this.definition = {
455
+ type,
456
+ nullable: true,
457
+ unique: false,
458
+ primary: false,
459
+ translatable: false
460
+ };
461
+ }
462
+ required() {
463
+ this.definition.nullable = false;
464
+ return this;
465
+ }
466
+ nullable() {
467
+ this.definition.nullable = true;
468
+ return this;
469
+ }
470
+ unique() {
471
+ this.definition.unique = true;
472
+ return this;
473
+ }
474
+ default(value) {
475
+ this.definition.default = value;
476
+ return this;
477
+ }
478
+ translatable() {
479
+ this.definition.translatable = true;
480
+ return this;
481
+ }
482
+ ref(tableOrOptions) {
483
+ this.definition.ref = tableOrOptions;
484
+ return this;
485
+ }
486
+ trim() {
487
+ this.definition.trim = true;
488
+ return this;
489
+ }
490
+ lowercase() {
491
+ this.definition.lowercase = true;
492
+ return this;
493
+ }
494
+ uppercase() {
495
+ this.definition.uppercase = true;
496
+ return this;
497
+ }
498
+ minLength(value) {
499
+ this.definition.minLength = value;
500
+ return this;
501
+ }
502
+ maxLength(value) {
503
+ this.definition.maxLength = value;
504
+ return this;
505
+ }
506
+ min(value) {
507
+ this.definition.min = value;
508
+ return this;
509
+ }
510
+ max(value) {
511
+ this.definition.max = value;
512
+ return this;
513
+ }
514
+ enum(values) {
515
+ this.definition.enum = values;
516
+ return this;
517
+ }
518
+ match(pattern) {
519
+ this.definition.match = pattern;
520
+ return this;
521
+ }
522
+ build() {
523
+ return { ...this.definition };
524
+ }
525
+ }
526
+ var f = {
527
+ id: () => {
528
+ const builder = new FieldBuilder("id");
529
+ builder["definition"].primary = true;
530
+ builder["definition"].nullable = false;
531
+ return builder;
532
+ },
533
+ string: () => new FieldBuilder("string"),
534
+ text: () => new FieldBuilder("text"),
535
+ int: () => new FieldBuilder("int"),
536
+ decimal: () => new FieldBuilder("decimal"),
537
+ bool: () => new FieldBuilder("bool"),
538
+ timestamp: () => new FieldBuilder("timestamp"),
539
+ json: () => new FieldBuilder("json")
540
+ };
541
+
542
+ // src/schema/defineSchema.ts
543
+ function defineSchema(input) {
544
+ const languages = normalizeLanguages(input.languages);
545
+ const tables = {};
546
+ for (const [tableName, fields] of Object.entries(input.tables)) {
547
+ tables[tableName] = {
548
+ name: tableName,
549
+ fields: Object.fromEntries(Object.entries(fields).map(([fieldName, builder]) => [
550
+ fieldName,
551
+ builder.build()
552
+ ]))
553
+ };
554
+ }
555
+ const schema = {
556
+ name: input.name,
557
+ languages,
558
+ tables
559
+ };
560
+ assertValidSchema(schema);
561
+ return schema;
562
+ }
563
+ function normalizeLanguages(input) {
564
+ if (Array.isArray(input)) {
565
+ if (input.length === 0) {
566
+ throw new Error("At least one language must be defined");
567
+ }
568
+ return {
569
+ default: input[0],
570
+ supported: input
571
+ };
572
+ }
573
+ return input;
574
+ }
575
+ function createSchemaUnsafe(input) {
576
+ const languages = normalizeLanguages(input.languages);
577
+ const tables = {};
578
+ for (const [tableName, fields] of Object.entries(input.tables)) {
579
+ tables[tableName] = {
580
+ name: tableName,
581
+ fields: Object.fromEntries(Object.entries(fields).map(([fieldName, builder]) => [
582
+ fieldName,
583
+ builder.build()
584
+ ]))
585
+ };
586
+ }
587
+ return {
588
+ name: input.name,
589
+ languages,
590
+ tables
591
+ };
592
+ }
593
+ function mergeSchemas(schemas) {
594
+ if (schemas.length === 0) {
595
+ throw new Error("At least one schema is required");
596
+ }
597
+ const defaultLang = schemas[0].languages.default;
598
+ const allLangs = new Set;
599
+ for (const schema of schemas) {
600
+ for (const lang of schema.languages.supported) {
601
+ allLangs.add(lang);
602
+ }
603
+ }
604
+ const tables = {};
605
+ for (const schema of schemas) {
606
+ for (const [tableName, table] of Object.entries(schema.tables)) {
607
+ if (tables[tableName]) {
608
+ throw new Error(`Duplicate table name across schemas: ${tableName}`);
609
+ }
610
+ tables[tableName] = table;
611
+ }
612
+ }
613
+ return {
614
+ languages: {
615
+ default: defaultLang,
616
+ supported: Array.from(allLangs)
617
+ },
618
+ tables
619
+ };
620
+ }
621
+ // src/schema/helpers.ts
622
+ function singularize(word) {
623
+ const irregulars = {
624
+ people: "person",
625
+ children: "child",
626
+ men: "man",
627
+ women: "woman",
628
+ teeth: "tooth",
629
+ feet: "foot",
630
+ mice: "mouse",
631
+ geese: "goose"
632
+ };
633
+ if (irregulars[word]) {
634
+ return irregulars[word];
635
+ }
636
+ if (word.endsWith("ies")) {
637
+ return word.slice(0, -3) + "y";
638
+ }
639
+ if (word.endsWith("ses") || word.endsWith("xes") || word.endsWith("zes") || word.endsWith("ches") || word.endsWith("shes")) {
640
+ return word.slice(0, -2);
641
+ }
642
+ if (word.endsWith("oes")) {
643
+ return word.slice(0, -2);
644
+ }
645
+ if (word.endsWith("s") && !word.endsWith("ss")) {
646
+ return word.slice(0, -1);
647
+ }
648
+ return word;
649
+ }
650
+ function toPascalCase(str) {
651
+ return str.split(/[_\-\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
652
+ }
653
+ function toCamelCase(str) {
654
+ const pascal = toPascalCase(str);
655
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
656
+ }
657
+ function toSnakeCase(str) {
658
+ return str.replace(/([A-Z])/g, "_$1").replace(/[-\s]+/g, "_").toLowerCase().replace(/^_/, "");
659
+ }
660
+ function toInterfaceName(tableName) {
661
+ return toPascalCase(singularize(tableName));
662
+ }
663
+ function toDbInterfaceName(tableName) {
664
+ return "Db" + toInterfaceName(tableName);
665
+ }
666
+ function pluralize(word) {
667
+ const irregulars = {
668
+ person: "people",
669
+ child: "children",
670
+ man: "men",
671
+ woman: "women",
672
+ tooth: "teeth",
673
+ foot: "feet",
674
+ mouse: "mice",
675
+ goose: "geese"
676
+ };
677
+ if (irregulars[word]) {
678
+ return irregulars[word];
679
+ }
680
+ if (word.endsWith("y") && !/[aeiou]y$/.test(word)) {
681
+ return word.slice(0, -1) + "ies";
682
+ }
683
+ if (word.endsWith("s") || word.endsWith("x") || word.endsWith("z") || word.endsWith("ch") || word.endsWith("sh")) {
684
+ return word + "es";
685
+ }
686
+ if (word.endsWith("o") && /[^aeiou]o$/.test(word)) {
687
+ return word + "es";
688
+ }
689
+ return word + "s";
690
+ }
691
+ function toPascalCasePlural(tableName) {
692
+ return toPascalCase(tableName);
693
+ }
694
+ function toTranslationTableName(tableName) {
695
+ return `${tableName}_translations`;
696
+ }
697
+ function toTranslationFKName(tableName) {
698
+ return `${singularize(tableName)}_id`;
699
+ }
700
+ // src/utils/whereBuilder.ts
701
+ var COMPARISON_OPERATORS = {
702
+ $eq: "=",
703
+ $ne: "!=",
704
+ $gt: ">",
705
+ $gte: ">=",
706
+ $lt: "<",
707
+ $lte: "<="
708
+ };
709
+ var LOGICAL_OPERATORS = new Set(["$and", "$or", "$nor"]);
710
+ function processFieldOperators(field, operators) {
711
+ const parts = [];
712
+ const params = [];
713
+ for (const [op, value] of Object.entries(operators)) {
714
+ if (op in COMPARISON_OPERATORS) {
715
+ parts.push(`${field} ${COMPARISON_OPERATORS[op]} ?`);
716
+ params.push(value);
717
+ } else if (op === "$in" || op === "$nin") {
718
+ if (!Array.isArray(value)) {
719
+ throw new Error(`${op} operator requires an array value`);
720
+ }
721
+ if (value.length === 0) {
722
+ parts.push(op === "$in" ? "0 = 1" : "1 = 1");
723
+ } else {
724
+ const placeholders = value.map(() => "?").join(", ");
725
+ const keyword = op === "$in" ? "IN" : "NOT IN";
726
+ parts.push(`${field} ${keyword} (${placeholders})`);
727
+ params.push(...value);
728
+ }
729
+ } else if (op === "$like") {
730
+ parts.push(`${field} LIKE ?`);
731
+ params.push(value);
732
+ } else if (op === "$notLike") {
733
+ parts.push(`${field} NOT LIKE ?`);
734
+ params.push(value);
735
+ } else if (op === "$between") {
736
+ if (!Array.isArray(value) || value.length !== 2) {
737
+ throw new Error("$between operator requires an array of exactly 2 values");
738
+ }
739
+ parts.push(`${field} BETWEEN ? AND ?`);
740
+ params.push(value[0], value[1]);
741
+ } else if (op === "$isNull") {
742
+ parts.push(value ? `${field} IS NULL` : `${field} IS NOT NULL`);
743
+ } else if (op === "$not") {
744
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
745
+ throw new Error("$not operator requires an object with operators");
746
+ }
747
+ const inner = processFieldOperators(field, value);
748
+ parts.push(`NOT (${inner.sql})`);
749
+ params.push(...inner.params);
750
+ } else {
751
+ throw new Error(`Unknown operator: ${op}`);
752
+ }
753
+ }
754
+ const sql = parts.length === 1 ? parts[0] : `(${parts.join(" AND ")})`;
755
+ return { sql, params };
756
+ }
757
+ function processFieldCondition(field, value) {
758
+ if (value === null) {
759
+ return { sql: `${field} IS NULL`, params: [] };
760
+ }
761
+ if (typeof value !== "object" || Array.isArray(value)) {
762
+ return { sql: `${field} = ?`, params: [value] };
763
+ }
764
+ return processFieldOperators(field, value);
765
+ }
766
+ function processCondition(where) {
767
+ const parts = [];
768
+ const params = [];
769
+ for (const [key, value] of Object.entries(where)) {
770
+ if (LOGICAL_OPERATORS.has(key)) {
771
+ if (!Array.isArray(value)) {
772
+ throw new Error(`${key} operator requires an array of conditions`);
773
+ }
774
+ const subResults = value.map((sub) => processCondition(sub));
775
+ const joiner = key === "$and" ? " AND " : " OR ";
776
+ const combined = subResults.map((r) => r.sql).join(joiner);
777
+ if (combined) {
778
+ const wrappedSql = key === "$nor" ? `NOT (${combined})` : `(${combined})`;
779
+ parts.push(wrappedSql);
780
+ subResults.forEach((r) => params.push(...r.params));
781
+ }
782
+ } else {
783
+ const result = processFieldCondition(key, value);
784
+ parts.push(result.sql);
785
+ params.push(...result.params);
786
+ }
787
+ }
788
+ return { sql: parts.join(" AND "), params };
789
+ }
790
+ function buildWhereClause(where) {
791
+ if (!where || Object.keys(where).length === 0) {
792
+ return { sql: "", params: [] };
793
+ }
794
+ return processCondition(where);
795
+ }
796
+ // src/utils/translationQuery.ts
797
+ function buildAliasedWhereClause(where, alias) {
798
+ const result = buildWhereClause(where);
799
+ if (!alias || !result.sql) {
800
+ return result;
801
+ }
802
+ let sql = result.sql;
803
+ const fields = Object.keys(where).filter((k) => !k.startsWith("$"));
804
+ for (const field of fields) {
805
+ const regex = new RegExp(`\\b${field}\\b(?!\\.)`, "g");
806
+ sql = sql.replace(regex, `${alias}.${field}`);
807
+ }
808
+ return { sql, params: result.params };
809
+ }
810
+ function buildTranslationQuery(options) {
811
+ const {
812
+ table,
813
+ schema,
814
+ lang,
815
+ fallbackLang = schema.languages.default,
816
+ where,
817
+ orderBy,
818
+ limit,
819
+ offset
820
+ } = options;
821
+ const tableSchema = schema.tables[table];
822
+ if (!tableSchema) {
823
+ throw new Error(`Table not found in schema: ${table}`);
824
+ }
825
+ const translatableFields = getTranslatableFields(tableSchema);
826
+ if (translatableFields.length === 0) {
827
+ return buildSimpleQuery(table, { where, orderBy, limit, offset });
828
+ }
829
+ const transTable = toTranslationTableName(table);
830
+ const fkName = toTranslationFKName(table);
831
+ const mainAlias = "m";
832
+ const transAlias = "t";
833
+ const fallbackAlias = "fb";
834
+ const selectFields = [];
835
+ for (const [fieldName, field] of Object.entries(tableSchema.fields)) {
836
+ if (field.translatable) {
837
+ selectFields.push(`COALESCE(${transAlias}.${fieldName}, ${fallbackAlias}.${fieldName}) as ${fieldName}`);
838
+ } else {
839
+ selectFields.push(`${mainAlias}.${fieldName}`);
840
+ }
841
+ }
842
+ let sql = `SELECT ${selectFields.join(", ")}
843
+ FROM ${table} ${mainAlias}
844
+ LEFT JOIN ${transTable} ${transAlias}
845
+ ON ${mainAlias}.id = ${transAlias}.${fkName}
846
+ AND ${transAlias}.language_code = ?
847
+ LEFT JOIN ${transTable} ${fallbackAlias}
848
+ ON ${mainAlias}.id = ${fallbackAlias}.${fkName}
849
+ AND ${fallbackAlias}.language_code = ?`;
850
+ const params = [lang, fallbackLang];
851
+ if (where && Object.keys(where).length > 0) {
852
+ const whereResult = buildAliasedWhereClause(where, mainAlias);
853
+ sql += `
854
+ WHERE ${whereResult.sql}`;
855
+ params.push(...whereResult.params);
856
+ }
857
+ if (orderBy && orderBy.length > 0) {
858
+ const orderClauses = orderBy.map((o) => {
859
+ if (translatableFields.includes(o.field)) {
860
+ return `COALESCE(${transAlias}.${o.field}, ${fallbackAlias}.${o.field}) ${o.direction}`;
861
+ }
862
+ return `${mainAlias}.${o.field} ${o.direction}`;
863
+ });
864
+ sql += `
865
+ ORDER BY ${orderClauses.join(", ")}`;
866
+ }
867
+ if (limit !== undefined) {
868
+ sql += `
869
+ LIMIT ?`;
870
+ params.push(limit);
871
+ }
872
+ if (offset !== undefined) {
873
+ sql += `
874
+ OFFSET ?`;
875
+ params.push(offset);
876
+ }
877
+ return { sql, params };
878
+ }
879
+ function buildTranslationQueryById(table, schema, id, lang, fallbackLang) {
880
+ return buildTranslationQuery({
881
+ table,
882
+ schema,
883
+ lang,
884
+ fallbackLang,
885
+ where: { id },
886
+ limit: 1
887
+ });
888
+ }
889
+ function buildSimpleQuery(table, options) {
890
+ let sql = `SELECT * FROM ${table}`;
891
+ const params = [];
892
+ if (options.where && Object.keys(options.where).length > 0) {
893
+ const whereResult = buildWhereClause(options.where);
894
+ sql += ` WHERE ${whereResult.sql}`;
895
+ params.push(...whereResult.params);
896
+ }
897
+ if (options.orderBy && options.orderBy.length > 0) {
898
+ const orderClauses = options.orderBy.map((o) => `${o.field} ${o.direction}`);
899
+ sql += ` ORDER BY ${orderClauses.join(", ")}`;
900
+ }
901
+ if (options.limit !== undefined) {
902
+ sql += ` LIMIT ?`;
903
+ params.push(options.limit);
904
+ }
905
+ if (options.offset !== undefined) {
906
+ sql += ` OFFSET ?`;
907
+ params.push(options.offset);
908
+ }
909
+ return { sql, params };
910
+ }
911
+ function buildTranslationInsert(table, schema, recordId, lang, data) {
912
+ const tableSchema = schema.tables[table];
913
+ if (!tableSchema) {
914
+ throw new Error(`Table not found in schema: ${table}`);
915
+ }
916
+ const translatableFields = getTranslatableFields(tableSchema);
917
+ const transTable = toTranslationTableName(table);
918
+ const fkName = toTranslationFKName(table);
919
+ const fields = [fkName, "language_code"];
920
+ const values = [recordId, lang];
921
+ for (const field of translatableFields) {
922
+ if (data[field] !== undefined) {
923
+ fields.push(field);
924
+ values.push(data[field]);
925
+ }
926
+ }
927
+ const placeholders = fields.map(() => "?").join(", ");
928
+ const sql = `INSERT INTO ${transTable} (${fields.join(", ")}) VALUES (${placeholders})`;
929
+ return { sql, params: values };
930
+ }
931
+ function buildTranslationUpsert(table, schema, recordId, lang, data) {
932
+ const tableSchema = schema.tables[table];
933
+ if (!tableSchema) {
934
+ throw new Error(`Table not found in schema: ${table}`);
935
+ }
936
+ const translatableFields = getTranslatableFields(tableSchema);
937
+ const transTable = toTranslationTableName(table);
938
+ const fkName = toTranslationFKName(table);
939
+ const fields = [fkName, "language_code"];
940
+ const values = [recordId, lang];
941
+ for (const field of translatableFields) {
942
+ if (data[field] !== undefined) {
943
+ fields.push(field);
944
+ values.push(data[field]);
945
+ }
946
+ }
947
+ const placeholders = fields.map(() => "?").join(", ");
948
+ const sql = `INSERT OR REPLACE INTO ${transTable} (${fields.join(", ")}) VALUES (${placeholders})`;
949
+ return { sql, params: values };
950
+ }
951
+ function extractTranslatableData(data, tableSchema) {
952
+ const translatableFields = getTranslatableFields(tableSchema);
953
+ const mainData = {};
954
+ const translatableData = {};
955
+ for (const [key, value] of Object.entries(data)) {
956
+ if (translatableFields.includes(key)) {
957
+ translatableData[key] = value;
958
+ } else if (key !== "translations") {
959
+ mainData[key] = value;
960
+ }
961
+ }
962
+ return { mainData, translatableData };
963
+ }
964
+ // src/utils/jsonConverter.ts
965
+ function parseJSONSchema(json) {
966
+ const languages = {
967
+ default: json.defaultLanguage || json.languages[0] || "en",
968
+ supported: json.languages || ["en"]
969
+ };
970
+ const tables = {};
971
+ for (const [tableName, jsonTable] of Object.entries(json.tables)) {
972
+ const fields = {};
973
+ for (const [fieldName, jsonField] of Object.entries(jsonTable)) {
974
+ fields[fieldName] = convertJSONField(jsonField);
975
+ }
976
+ tables[tableName] = {
977
+ name: tableName,
978
+ fields
979
+ };
980
+ }
981
+ return {
982
+ name: json.name,
983
+ languages,
984
+ tables
985
+ };
986
+ }
987
+ function convertJSONField(jsonField) {
988
+ let ref;
989
+ if (jsonField.ref) {
990
+ if (typeof jsonField.ref === "string") {
991
+ ref = jsonField.ref;
992
+ } else {
993
+ ref = {
994
+ table: jsonField.ref.table,
995
+ field: jsonField.ref.field,
996
+ onDelete: jsonField.ref.onDelete,
997
+ onUpdate: jsonField.ref.onUpdate
998
+ };
999
+ }
1000
+ }
1001
+ return {
1002
+ type: jsonField.type,
1003
+ nullable: jsonField.nullable ?? !jsonField.required,
1004
+ unique: jsonField.unique ?? false,
1005
+ primary: jsonField.primary ?? false,
1006
+ default: jsonField.default,
1007
+ translatable: jsonField.translatable ?? false,
1008
+ ref
1009
+ };
1010
+ }
1011
+ export {
1012
+ validateTable,
1013
+ validateSchema,
1014
+ validatePopulate,
1015
+ toTranslationTableName,
1016
+ toTranslationFKName,
1017
+ toSnakeCase,
1018
+ toPascalCasePlural,
1019
+ toPascalCase,
1020
+ toInterfaceName,
1021
+ toDbInterfaceName,
1022
+ toCamelCase,
1023
+ singularize,
1024
+ resolvePopulate,
1025
+ pluralize,
1026
+ parseJSONSchema,
1027
+ mergeSchemas,
1028
+ isValidSchema,
1029
+ isRequiredField,
1030
+ hasTranslatableFields,
1031
+ getTranslationTableFields,
1032
+ getTranslatableFields,
1033
+ getRequiredFields,
1034
+ getReferenceFields,
1035
+ getRefTargetFull,
1036
+ getRefTarget,
1037
+ getPrimaryKeyField,
1038
+ getPopulatableFields,
1039
+ getNonTranslatableFields,
1040
+ getMainTableFields,
1041
+ getInsertableFields,
1042
+ f,
1043
+ extractTranslatableData,
1044
+ defineSchema,
1045
+ createSchemaUnsafe,
1046
+ buildWhereClause,
1047
+ buildTranslationUpsert,
1048
+ buildTranslationQueryById,
1049
+ buildTranslationQuery,
1050
+ buildTranslationInsert,
1051
+ assertValidSchema,
1052
+ ValidationErrorCode,
1053
+ ORM
1054
+ };