@promakeai/orm 1.0.1 → 1.0.3
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.d.ts +2 -1
- package/dist/index.js +68 -10
- package/dist/schema/fieldBuilder.d.ts +37 -1
- package/dist/types.d.ts +23 -2
- package/dist/utils/whereBuilder.d.ts +1 -0
- package/package.json +1 -1
- package/src/ORM.ts +14 -5
- package/src/index.ts +4 -0
- package/src/schema/fieldBuilder.ts +50 -1
- package/src/schema/schemaHelpers.ts +2 -1
- package/src/types.ts +39 -2
- package/src/utils/deserializer.ts +3 -2
- package/src/utils/jsonConverter.ts +40 -1
- package/src/utils/populateResolver.ts +2 -1
- package/src/utils/whereBuilder.ts +17 -0
package/dist/index.d.ts
CHANGED
|
@@ -47,5 +47,6 @@ export type { TranslationQueryOptions, TranslationQueryResult, } from "./utils/t
|
|
|
47
47
|
export { resolvePopulate, getPopulatableFields, validatePopulate, } from "./utils/populateResolver";
|
|
48
48
|
export type { PopulateAdapter } from "./utils/populateResolver";
|
|
49
49
|
export { parseJSONSchema } from "./utils/jsonConverter";
|
|
50
|
+
export { isJsonType } from "./types";
|
|
50
51
|
export { deserializeRow, serializeRow } from "./utils/deserializer";
|
|
51
|
-
export type { FieldType, FieldDefinition, FieldReference, FieldBuilderLike, TableDefinition, LanguageConfig, SchemaDefinition, SchemaInput, JSONFieldDefinition, JSONTableDefinition, JSONSchemaDefinition, WhereCondition, OrderByOption, PopulateOption, PopulateNested, QueryOptions, PaginatedResult, ORMConfig, } from "./types";
|
|
52
|
+
export type { FieldType, FieldDefinition, FieldReference, FieldBuilderLike, TableDefinition, LanguageConfig, SchemaDefinition, SchemaInput, JSONFieldType, JSONFieldDefinition, JSONTableDefinition, JSONSchemaDefinition, WhereCondition, OrderByOption, PopulateOption, PopulateNested, QueryOptions, PaginatedResult, ORMConfig, } from "./types";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
function isJsonType(type) {
|
|
3
|
+
return type === "json" || type === "string[]" || type === "number[]" || type === "boolean[]" || type === "object" || type === "object[]";
|
|
4
|
+
}
|
|
5
|
+
|
|
1
6
|
// src/utils/populateResolver.ts
|
|
2
7
|
function getRefInfo(fieldName, field) {
|
|
3
8
|
if (!field.ref)
|
|
@@ -9,7 +14,7 @@ function getRefInfo(fieldName, field) {
|
|
|
9
14
|
field: isString ? "id" : ref.field || "id",
|
|
10
15
|
localField: fieldName,
|
|
11
16
|
targetField: isString ? "id" : ref.field || "id",
|
|
12
|
-
isArray: Array.isArray(field.default) || fieldName.endsWith("Ids")
|
|
17
|
+
isArray: isJsonType(field.type) || Array.isArray(field.default) || fieldName.endsWith("Ids")
|
|
13
18
|
};
|
|
14
19
|
}
|
|
15
20
|
function toPopulatedName(fieldName) {
|
|
@@ -164,7 +169,10 @@ class ORM {
|
|
|
164
169
|
const { populate, ...queryOpts } = opts;
|
|
165
170
|
let records = await this.adapter.list(table, queryOpts);
|
|
166
171
|
if (populate && this.schema) {
|
|
167
|
-
records = await this.resolvePopulateForRecords(records, table, populate
|
|
172
|
+
records = await this.resolvePopulateForRecords(records, table, populate, {
|
|
173
|
+
lang: opts.lang,
|
|
174
|
+
fallbackLang: opts.fallbackLang
|
|
175
|
+
});
|
|
168
176
|
}
|
|
169
177
|
return records;
|
|
170
178
|
}
|
|
@@ -173,7 +181,7 @@ class ORM {
|
|
|
173
181
|
const { populate, ...queryOpts } = opts;
|
|
174
182
|
const record = await this.adapter.get(table, id, queryOpts);
|
|
175
183
|
if (record && populate && this.schema) {
|
|
176
|
-
const [populated] = await this.resolvePopulateForRecords([record], table, populate);
|
|
184
|
+
const [populated] = await this.resolvePopulateForRecords([record], table, populate, { lang: opts.lang, fallbackLang: opts.fallbackLang });
|
|
177
185
|
return populated;
|
|
178
186
|
}
|
|
179
187
|
return record;
|
|
@@ -257,7 +265,7 @@ class ORM {
|
|
|
257
265
|
throw new Error(`Table "${table}" not found in schema`);
|
|
258
266
|
}
|
|
259
267
|
}
|
|
260
|
-
async resolvePopulateForRecords(records, table, populate) {
|
|
268
|
+
async resolvePopulateForRecords(records, table, populate, queryOptions) {
|
|
261
269
|
if (!this.schema)
|
|
262
270
|
return records;
|
|
263
271
|
const validation = validatePopulate(populate, table, this.schema);
|
|
@@ -266,7 +274,11 @@ class ORM {
|
|
|
266
274
|
}
|
|
267
275
|
const adapterWrapper = {
|
|
268
276
|
findMany: (t, opts) => {
|
|
269
|
-
return this.adapter.list(t,
|
|
277
|
+
return this.adapter.list(t, {
|
|
278
|
+
...opts,
|
|
279
|
+
lang: queryOptions?.lang,
|
|
280
|
+
fallbackLang: queryOptions?.fallbackLang
|
|
281
|
+
});
|
|
270
282
|
}
|
|
271
283
|
};
|
|
272
284
|
return resolvePopulate(records, table, populate, this.schema, adapterWrapper);
|
|
@@ -299,7 +311,7 @@ function getPrimaryKeyField(table) {
|
|
|
299
311
|
return entry ? entry[0] : null;
|
|
300
312
|
}
|
|
301
313
|
function getReferenceFields(table) {
|
|
302
|
-
return Object.entries(table.fields).filter(([_, field]) => field.ref).map(([name, field]) => {
|
|
314
|
+
return Object.entries(table.fields).filter(([_, field]) => field.ref && !isJsonType(field.type)).map(([name, field]) => {
|
|
303
315
|
const ref = field.ref;
|
|
304
316
|
if (typeof ref === "string") {
|
|
305
317
|
return [name, { table: ref, field: "id" }];
|
|
@@ -536,7 +548,20 @@ var f = {
|
|
|
536
548
|
decimal: () => new FieldBuilder("decimal"),
|
|
537
549
|
bool: () => new FieldBuilder("bool"),
|
|
538
550
|
timestamp: () => new FieldBuilder("timestamp"),
|
|
539
|
-
json: () => new FieldBuilder("json")
|
|
551
|
+
json: () => new FieldBuilder("json"),
|
|
552
|
+
stringArray: () => new FieldBuilder("string[]"),
|
|
553
|
+
numberArray: () => new FieldBuilder("number[]"),
|
|
554
|
+
boolArray: () => new FieldBuilder("boolean[]"),
|
|
555
|
+
object: (properties) => {
|
|
556
|
+
const builder = new FieldBuilder("object");
|
|
557
|
+
builder["definition"].properties = properties;
|
|
558
|
+
return builder;
|
|
559
|
+
},
|
|
560
|
+
objectArray: (properties) => {
|
|
561
|
+
const builder = new FieldBuilder("object[]");
|
|
562
|
+
builder["definition"].properties = properties;
|
|
563
|
+
return builder;
|
|
564
|
+
}
|
|
540
565
|
};
|
|
541
566
|
|
|
542
567
|
// src/schema/defineSchema.ts
|
|
@@ -740,6 +765,20 @@ function processFieldOperators(field, operators) {
|
|
|
740
765
|
params.push(value[0], value[1]);
|
|
741
766
|
} else if (op === "$isNull") {
|
|
742
767
|
parts.push(value ? `${field} IS NULL` : `${field} IS NOT NULL`);
|
|
768
|
+
} else if (op === "$contains") {
|
|
769
|
+
parts.push(`EXISTS (SELECT 1 FROM json_each(${field}) WHERE value = ?)`);
|
|
770
|
+
params.push(value);
|
|
771
|
+
} else if (op === "$containsAny") {
|
|
772
|
+
if (!Array.isArray(value)) {
|
|
773
|
+
throw new Error("$containsAny operator requires an array value");
|
|
774
|
+
}
|
|
775
|
+
if (value.length === 0) {
|
|
776
|
+
parts.push("0 = 1");
|
|
777
|
+
} else {
|
|
778
|
+
const placeholders = value.map(() => "?").join(", ");
|
|
779
|
+
parts.push(`EXISTS (SELECT 1 FROM json_each(${field}) WHERE value IN (${placeholders}))`);
|
|
780
|
+
params.push(...value);
|
|
781
|
+
}
|
|
743
782
|
} else if (op === "$not") {
|
|
744
783
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
745
784
|
throw new Error("$not operator requires an object with operators");
|
|
@@ -984,6 +1023,22 @@ function parseJSONSchema(json) {
|
|
|
984
1023
|
tables
|
|
985
1024
|
};
|
|
986
1025
|
}
|
|
1026
|
+
function normalizeJsonType(type) {
|
|
1027
|
+
if (Array.isArray(type)) {
|
|
1028
|
+
const first = type[0];
|
|
1029
|
+
if (typeof first === "string") {
|
|
1030
|
+
const arrayType = `${first}[]`;
|
|
1031
|
+
return { type: arrayType };
|
|
1032
|
+
}
|
|
1033
|
+
if (typeof first === "object" && first !== null) {
|
|
1034
|
+
return { type: "object[]", properties: first };
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
if (typeof type === "object" && type !== null && !Array.isArray(type)) {
|
|
1038
|
+
return { type: "object", properties: type };
|
|
1039
|
+
}
|
|
1040
|
+
return { type };
|
|
1041
|
+
}
|
|
987
1042
|
function convertJSONField(jsonField) {
|
|
988
1043
|
let ref;
|
|
989
1044
|
if (jsonField.ref) {
|
|
@@ -998,13 +1053,15 @@ function convertJSONField(jsonField) {
|
|
|
998
1053
|
};
|
|
999
1054
|
}
|
|
1000
1055
|
}
|
|
1056
|
+
const { type, properties } = normalizeJsonType(jsonField.type);
|
|
1001
1057
|
return {
|
|
1002
|
-
type
|
|
1058
|
+
type,
|
|
1003
1059
|
nullable: jsonField.nullable ?? !jsonField.required,
|
|
1004
1060
|
unique: jsonField.unique ?? false,
|
|
1005
1061
|
primary: jsonField.primary ?? false,
|
|
1006
1062
|
default: jsonField.default,
|
|
1007
1063
|
translatable: jsonField.translatable ?? false,
|
|
1064
|
+
properties,
|
|
1008
1065
|
ref
|
|
1009
1066
|
};
|
|
1010
1067
|
}
|
|
@@ -1015,7 +1072,7 @@ function deserializeValue(value, fieldType) {
|
|
|
1015
1072
|
if (fieldType === "bool") {
|
|
1016
1073
|
return value === 1 || value === true;
|
|
1017
1074
|
}
|
|
1018
|
-
if (fieldType
|
|
1075
|
+
if (isJsonType(fieldType) && typeof value === "string") {
|
|
1019
1076
|
try {
|
|
1020
1077
|
return JSON.parse(value);
|
|
1021
1078
|
} catch {
|
|
@@ -1039,7 +1096,7 @@ function serializeValue(value, fieldType) {
|
|
|
1039
1096
|
if (fieldType === "bool") {
|
|
1040
1097
|
return value ? 1 : 0;
|
|
1041
1098
|
}
|
|
1042
|
-
if (fieldType
|
|
1099
|
+
if (isJsonType(fieldType) && typeof value !== "string") {
|
|
1043
1100
|
return JSON.stringify(value);
|
|
1044
1101
|
}
|
|
1045
1102
|
return value;
|
|
@@ -1073,6 +1130,7 @@ export {
|
|
|
1073
1130
|
mergeSchemas,
|
|
1074
1131
|
isValidSchema,
|
|
1075
1132
|
isRequiredField,
|
|
1133
|
+
isJsonType,
|
|
1076
1134
|
hasTranslatableFields,
|
|
1077
1135
|
getTranslationTableFields,
|
|
1078
1136
|
getTranslatableFields,
|
|
@@ -146,7 +146,43 @@ export declare const f: {
|
|
|
146
146
|
*/
|
|
147
147
|
timestamp: () => FieldBuilder;
|
|
148
148
|
/**
|
|
149
|
-
* JSON field (TEXT with JSON serialization)
|
|
149
|
+
* JSON field (TEXT with JSON serialization) — untyped, generates Record<string, unknown>
|
|
150
150
|
*/
|
|
151
151
|
json: () => FieldBuilder;
|
|
152
|
+
/**
|
|
153
|
+
* String array field — stored as JSON TEXT, generates string[]
|
|
154
|
+
*/
|
|
155
|
+
stringArray: () => FieldBuilder;
|
|
156
|
+
/**
|
|
157
|
+
* Number array field — stored as JSON TEXT, generates number[]
|
|
158
|
+
*/
|
|
159
|
+
numberArray: () => FieldBuilder;
|
|
160
|
+
/**
|
|
161
|
+
* Boolean array field — stored as JSON TEXT, generates boolean[]
|
|
162
|
+
*/
|
|
163
|
+
boolArray: () => FieldBuilder;
|
|
164
|
+
/**
|
|
165
|
+
* Typed object field — stored as JSON TEXT, generates typed interface
|
|
166
|
+
* @param properties - Property definitions. Keys ending with "?" are optional.
|
|
167
|
+
* Values: "string" | "number" | "boolean"
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* ```typescript
|
|
171
|
+
* f.object({ key: "string", value: "number" })
|
|
172
|
+
* // Generates: { key: string; value: number }
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
object: (properties: Record<string, string>) => FieldBuilder;
|
|
176
|
+
/**
|
|
177
|
+
* Typed object array field — stored as JSON TEXT, generates Array<{...}>
|
|
178
|
+
* @param properties - Property definitions. Keys ending with "?" are optional.
|
|
179
|
+
* Values: "string" | "number" | "boolean"
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* ```typescript
|
|
183
|
+
* f.objectArray({ id: "number", slug: "string", "name?": "string" })
|
|
184
|
+
* // Generates: Array<{ id: number; slug: string; name?: string }>
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
objectArray: (properties: Record<string, string>) => FieldBuilder;
|
|
152
188
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -5,8 +5,16 @@
|
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
7
|
* Supported field types
|
|
8
|
+
*
|
|
9
|
+
* Primitive types map directly to SQL column types.
|
|
10
|
+
* Typed JSON types (string[], object[], etc.) store as TEXT in SQL
|
|
11
|
+
* but generate accurate TypeScript types.
|
|
12
|
+
*/
|
|
13
|
+
export type FieldType = "id" | "string" | "text" | "int" | "decimal" | "bool" | "timestamp" | "json" | "string[]" | "number[]" | "boolean[]" | "object" | "object[]";
|
|
14
|
+
/**
|
|
15
|
+
* Check if a field type is JSON-like (stored as TEXT in SQL, serialized as JSON)
|
|
8
16
|
*/
|
|
9
|
-
export type FieldType
|
|
17
|
+
export declare function isJsonType(type: FieldType): boolean;
|
|
10
18
|
/**
|
|
11
19
|
* Foreign key reference definition (MongoDB-style)
|
|
12
20
|
*/
|
|
@@ -26,6 +34,7 @@ export interface FieldDefinition {
|
|
|
26
34
|
primary: boolean;
|
|
27
35
|
default?: unknown;
|
|
28
36
|
translatable: boolean;
|
|
37
|
+
properties?: Record<string, string>;
|
|
29
38
|
ref?: string | FieldReference;
|
|
30
39
|
required?: boolean;
|
|
31
40
|
trim?: boolean;
|
|
@@ -74,11 +83,21 @@ export interface SchemaInput {
|
|
|
74
83
|
languages: string[] | LanguageConfig;
|
|
75
84
|
tables: Record<string, Record<string, FieldBuilderLike>>;
|
|
76
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* JSON-native type syntax for schema.json
|
|
88
|
+
*
|
|
89
|
+
* Accepts:
|
|
90
|
+
* - string: primitive type ("string", "int", "json", etc.)
|
|
91
|
+
* - ["string"]: primitive array
|
|
92
|
+
* - [{ id: "number", name: "string" }]: typed object array
|
|
93
|
+
* - { key: "string", value: "number" }: typed object
|
|
94
|
+
*/
|
|
95
|
+
export type JSONFieldType = FieldType | [string] | [Record<string, string>] | Record<string, string>;
|
|
77
96
|
/**
|
|
78
97
|
* JSON Schema Field Definition
|
|
79
98
|
*/
|
|
80
99
|
export interface JSONFieldDefinition {
|
|
81
|
-
type:
|
|
100
|
+
type: JSONFieldType;
|
|
82
101
|
translatable?: boolean;
|
|
83
102
|
required?: boolean;
|
|
84
103
|
unique?: boolean;
|
|
@@ -136,6 +155,8 @@ export interface WhereCondition {
|
|
|
136
155
|
$notLike?: string;
|
|
137
156
|
$between?: [unknown, unknown];
|
|
138
157
|
$isNull?: boolean;
|
|
158
|
+
$contains?: unknown;
|
|
159
|
+
$containsAny?: unknown[];
|
|
139
160
|
$not?: WhereCondition;
|
|
140
161
|
$and?: WhereCondition[];
|
|
141
162
|
$or?: WhereCondition[];
|
package/package.json
CHANGED
package/src/ORM.ts
CHANGED
|
@@ -100,7 +100,10 @@ export class ORM {
|
|
|
100
100
|
|
|
101
101
|
// Resolve populate if schema is available
|
|
102
102
|
if (populate && this.schema) {
|
|
103
|
-
records = await this.resolvePopulateForRecords(records, table, populate
|
|
103
|
+
records = await this.resolvePopulateForRecords(records, table, populate, {
|
|
104
|
+
lang: opts.lang,
|
|
105
|
+
fallbackLang: opts.fallbackLang,
|
|
106
|
+
});
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
return records;
|
|
@@ -132,7 +135,8 @@ export class ORM {
|
|
|
132
135
|
const [populated] = await this.resolvePopulateForRecords(
|
|
133
136
|
[record],
|
|
134
137
|
table,
|
|
135
|
-
populate
|
|
138
|
+
populate,
|
|
139
|
+
{ lang: opts.lang, fallbackLang: opts.fallbackLang }
|
|
136
140
|
);
|
|
137
141
|
return populated;
|
|
138
142
|
}
|
|
@@ -346,7 +350,8 @@ export class ORM {
|
|
|
346
350
|
private async resolvePopulateForRecords<T>(
|
|
347
351
|
records: T[],
|
|
348
352
|
table: string,
|
|
349
|
-
populate: QueryOptions["populate"]
|
|
353
|
+
populate: QueryOptions["populate"],
|
|
354
|
+
queryOptions?: { lang?: string; fallbackLang?: string }
|
|
350
355
|
): Promise<T[]> {
|
|
351
356
|
if (!this.schema) return records;
|
|
352
357
|
|
|
@@ -356,13 +361,17 @@ export class ORM {
|
|
|
356
361
|
console.warn("Populate validation warnings:", validation.errors);
|
|
357
362
|
}
|
|
358
363
|
|
|
359
|
-
// Create adapter wrapper for populate resolver
|
|
364
|
+
// Create adapter wrapper for populate resolver (passes lang for translations)
|
|
360
365
|
const adapterWrapper = {
|
|
361
366
|
findMany: <R = unknown>(
|
|
362
367
|
t: string,
|
|
363
368
|
opts?: { where?: Record<string, unknown> }
|
|
364
369
|
): Promise<R[]> => {
|
|
365
|
-
return this.adapter.list<R>(t,
|
|
370
|
+
return this.adapter.list<R>(t, {
|
|
371
|
+
...opts,
|
|
372
|
+
lang: queryOptions?.lang,
|
|
373
|
+
fallbackLang: queryOptions?.fallbackLang,
|
|
374
|
+
});
|
|
366
375
|
},
|
|
367
376
|
};
|
|
368
377
|
|
package/src/index.ts
CHANGED
|
@@ -116,6 +116,9 @@ export type { PopulateAdapter } from "./utils/populateResolver";
|
|
|
116
116
|
// JSON Schema Converter
|
|
117
117
|
export { parseJSONSchema } from "./utils/jsonConverter";
|
|
118
118
|
|
|
119
|
+
// Type Helpers
|
|
120
|
+
export { isJsonType } from "./types";
|
|
121
|
+
|
|
119
122
|
// Row Serializer/Deserializer
|
|
120
123
|
export { deserializeRow, serializeRow } from "./utils/deserializer";
|
|
121
124
|
|
|
@@ -134,6 +137,7 @@ export type {
|
|
|
134
137
|
SchemaInput,
|
|
135
138
|
|
|
136
139
|
// JSON Schema types (AI-friendly)
|
|
140
|
+
JSONFieldType,
|
|
137
141
|
JSONFieldDefinition,
|
|
138
142
|
JSONTableDefinition,
|
|
139
143
|
JSONSchemaDefinition,
|
|
@@ -237,8 +237,57 @@ export const f = {
|
|
|
237
237
|
timestamp: (): FieldBuilder => new FieldBuilder("timestamp"),
|
|
238
238
|
|
|
239
239
|
/**
|
|
240
|
-
* JSON field (TEXT with JSON serialization)
|
|
240
|
+
* JSON field (TEXT with JSON serialization) — untyped, generates Record<string, unknown>
|
|
241
241
|
*/
|
|
242
242
|
json: (): FieldBuilder => new FieldBuilder("json"),
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* String array field — stored as JSON TEXT, generates string[]
|
|
246
|
+
*/
|
|
247
|
+
stringArray: (): FieldBuilder => new FieldBuilder("string[]"),
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Number array field — stored as JSON TEXT, generates number[]
|
|
251
|
+
*/
|
|
252
|
+
numberArray: (): FieldBuilder => new FieldBuilder("number[]"),
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Boolean array field — stored as JSON TEXT, generates boolean[]
|
|
256
|
+
*/
|
|
257
|
+
boolArray: (): FieldBuilder => new FieldBuilder("boolean[]"),
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Typed object field — stored as JSON TEXT, generates typed interface
|
|
261
|
+
* @param properties - Property definitions. Keys ending with "?" are optional.
|
|
262
|
+
* Values: "string" | "number" | "boolean"
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* ```typescript
|
|
266
|
+
* f.object({ key: "string", value: "number" })
|
|
267
|
+
* // Generates: { key: string; value: number }
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
object: (properties: Record<string, string>): FieldBuilder => {
|
|
271
|
+
const builder = new FieldBuilder("object");
|
|
272
|
+
builder["definition"].properties = properties;
|
|
273
|
+
return builder;
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Typed object array field — stored as JSON TEXT, generates Array<{...}>
|
|
278
|
+
* @param properties - Property definitions. Keys ending with "?" are optional.
|
|
279
|
+
* Values: "string" | "number" | "boolean"
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* ```typescript
|
|
283
|
+
* f.objectArray({ id: "number", slug: "string", "name?": "string" })
|
|
284
|
+
* // Generates: Array<{ id: number; slug: string; name?: string }>
|
|
285
|
+
* ```
|
|
286
|
+
*/
|
|
287
|
+
objectArray: (properties: Record<string, string>): FieldBuilder => {
|
|
288
|
+
const builder = new FieldBuilder("object[]");
|
|
289
|
+
builder["definition"].properties = properties;
|
|
290
|
+
return builder;
|
|
291
|
+
},
|
|
243
292
|
};
|
|
244
293
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { TableDefinition, FieldDefinition, FieldReference } from "../types";
|
|
8
|
+
import { isJsonType } from "../types";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Get names of translatable fields in a table
|
|
@@ -55,7 +56,7 @@ export function getReferenceFields(
|
|
|
55
56
|
table: TableDefinition
|
|
56
57
|
): [string, FieldReference][] {
|
|
57
58
|
return Object.entries(table.fields)
|
|
58
|
-
.filter(([_, field]) => field.ref)
|
|
59
|
+
.filter(([_, field]) => field.ref && !isJsonType(field.type))
|
|
59
60
|
.map(([name, field]) => {
|
|
60
61
|
const ref = field.ref!;
|
|
61
62
|
// Normalize string to FieldReference
|
package/src/types.ts
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Supported field types
|
|
11
|
+
*
|
|
12
|
+
* Primitive types map directly to SQL column types.
|
|
13
|
+
* Typed JSON types (string[], object[], etc.) store as TEXT in SQL
|
|
14
|
+
* but generate accurate TypeScript types.
|
|
11
15
|
*/
|
|
12
16
|
export type FieldType =
|
|
13
17
|
| "id"
|
|
@@ -17,7 +21,19 @@ export type FieldType =
|
|
|
17
21
|
| "decimal"
|
|
18
22
|
| "bool"
|
|
19
23
|
| "timestamp"
|
|
20
|
-
| "json"
|
|
24
|
+
| "json"
|
|
25
|
+
| "string[]"
|
|
26
|
+
| "number[]"
|
|
27
|
+
| "boolean[]"
|
|
28
|
+
| "object"
|
|
29
|
+
| "object[]";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if a field type is JSON-like (stored as TEXT in SQL, serialized as JSON)
|
|
33
|
+
*/
|
|
34
|
+
export function isJsonType(type: FieldType): boolean {
|
|
35
|
+
return type === "json" || type === "string[]" || type === "number[]" || type === "boolean[]" || type === "object" || type === "object[]";
|
|
36
|
+
}
|
|
21
37
|
|
|
22
38
|
/**
|
|
23
39
|
* Foreign key reference definition (MongoDB-style)
|
|
@@ -40,6 +56,10 @@ export interface FieldDefinition {
|
|
|
40
56
|
default?: unknown;
|
|
41
57
|
translatable: boolean;
|
|
42
58
|
|
|
59
|
+
// Typed JSON properties (for "object" and "object[]" types)
|
|
60
|
+
// Keys ending with "?" are optional. Values: "string" | "number" | "boolean"
|
|
61
|
+
properties?: Record<string, string>;
|
|
62
|
+
|
|
43
63
|
// Reference (MongoDB-style)
|
|
44
64
|
ref?: string | FieldReference;
|
|
45
65
|
|
|
@@ -101,11 +121,26 @@ export interface SchemaInput {
|
|
|
101
121
|
|
|
102
122
|
// ==================== JSON Schema Types (AI Agent Friendly) ====================
|
|
103
123
|
|
|
124
|
+
/**
|
|
125
|
+
* JSON-native type syntax for schema.json
|
|
126
|
+
*
|
|
127
|
+
* Accepts:
|
|
128
|
+
* - string: primitive type ("string", "int", "json", etc.)
|
|
129
|
+
* - ["string"]: primitive array
|
|
130
|
+
* - [{ id: "number", name: "string" }]: typed object array
|
|
131
|
+
* - { key: "string", value: "number" }: typed object
|
|
132
|
+
*/
|
|
133
|
+
export type JSONFieldType =
|
|
134
|
+
| FieldType
|
|
135
|
+
| [string]
|
|
136
|
+
| [Record<string, string>]
|
|
137
|
+
| Record<string, string>;
|
|
138
|
+
|
|
104
139
|
/**
|
|
105
140
|
* JSON Schema Field Definition
|
|
106
141
|
*/
|
|
107
142
|
export interface JSONFieldDefinition {
|
|
108
|
-
type:
|
|
143
|
+
type: JSONFieldType;
|
|
109
144
|
translatable?: boolean;
|
|
110
145
|
required?: boolean;
|
|
111
146
|
unique?: boolean;
|
|
@@ -176,6 +211,8 @@ export interface WhereCondition {
|
|
|
176
211
|
$notLike?: string;
|
|
177
212
|
$between?: [unknown, unknown];
|
|
178
213
|
$isNull?: boolean;
|
|
214
|
+
$contains?: unknown;
|
|
215
|
+
$containsAny?: unknown[];
|
|
179
216
|
$not?: WhereCondition;
|
|
180
217
|
$and?: WhereCondition[];
|
|
181
218
|
$or?: WhereCondition[];
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { FieldType, FieldDefinition } from "../types";
|
|
10
|
+
import { isJsonType } from "../types";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Deserialize a single value from DB storage type to application type
|
|
@@ -18,7 +19,7 @@ function deserializeValue(value: unknown, fieldType: FieldType): unknown {
|
|
|
18
19
|
return value === 1 || value === true;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
if (fieldType
|
|
22
|
+
if (isJsonType(fieldType) && typeof value === "string") {
|
|
22
23
|
try {
|
|
23
24
|
return JSON.parse(value);
|
|
24
25
|
} catch {
|
|
@@ -61,7 +62,7 @@ function serializeValue(value: unknown, fieldType: FieldType): unknown {
|
|
|
61
62
|
return value ? 1 : 0;
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
if (fieldType
|
|
65
|
+
if (isJsonType(fieldType) && typeof value !== "string") {
|
|
65
66
|
return JSON.stringify(value);
|
|
66
67
|
}
|
|
67
68
|
|
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
import type {
|
|
9
9
|
JSONSchemaDefinition,
|
|
10
10
|
JSONFieldDefinition,
|
|
11
|
+
JSONFieldType,
|
|
11
12
|
SchemaDefinition,
|
|
12
13
|
FieldDefinition,
|
|
14
|
+
FieldType,
|
|
13
15
|
TableDefinition,
|
|
14
16
|
LanguageConfig,
|
|
15
17
|
} from "../types";
|
|
@@ -63,6 +65,40 @@ export function parseJSONSchema(json: JSONSchemaDefinition): SchemaDefinition {
|
|
|
63
65
|
};
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Normalize JSON-native type syntax to internal FieldType + properties
|
|
70
|
+
*
|
|
71
|
+
* - ["string"] → type: "string[]"
|
|
72
|
+
* - ["number"] → type: "number[]"
|
|
73
|
+
* - ["boolean"] → type: "boolean[]"
|
|
74
|
+
* - [{ id: "number", ... }] → type: "object[]", properties: { id: "number", ... }
|
|
75
|
+
* - { key: "string", ... } → type: "object", properties: { key: "string", ... }
|
|
76
|
+
* - "string" / "json" / ... → pass through as-is
|
|
77
|
+
*/
|
|
78
|
+
function normalizeJsonType(type: JSONFieldType): { type: FieldType; properties?: Record<string, string> } {
|
|
79
|
+
// Array syntax: ["string"] or [{ ... }]
|
|
80
|
+
if (Array.isArray(type)) {
|
|
81
|
+
const first = type[0];
|
|
82
|
+
if (typeof first === "string") {
|
|
83
|
+
// Primitive array: ["string"] → "string[]"
|
|
84
|
+
const arrayType = `${first}[]` as FieldType;
|
|
85
|
+
return { type: arrayType };
|
|
86
|
+
}
|
|
87
|
+
if (typeof first === "object" && first !== null) {
|
|
88
|
+
// Object array: [{ id: "number", ... }] → "object[]" + properties
|
|
89
|
+
return { type: "object[]", properties: first as Record<string, string> };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Object syntax: { key: "string", ... } → "object" + properties
|
|
94
|
+
if (typeof type === "object" && type !== null && !Array.isArray(type)) {
|
|
95
|
+
return { type: "object", properties: type as Record<string, string> };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// String: pass through as-is
|
|
99
|
+
return { type: type as FieldType };
|
|
100
|
+
}
|
|
101
|
+
|
|
66
102
|
/**
|
|
67
103
|
* Convert single JSON field to internal FieldDefinition
|
|
68
104
|
*/
|
|
@@ -82,13 +118,16 @@ function convertJSONField(jsonField: JSONFieldDefinition): FieldDefinition {
|
|
|
82
118
|
}
|
|
83
119
|
}
|
|
84
120
|
|
|
121
|
+
const { type, properties } = normalizeJsonType(jsonField.type);
|
|
122
|
+
|
|
85
123
|
return {
|
|
86
|
-
type
|
|
124
|
+
type,
|
|
87
125
|
nullable: jsonField.nullable ?? !jsonField.required,
|
|
88
126
|
unique: jsonField.unique ?? false,
|
|
89
127
|
primary: jsonField.primary ?? false,
|
|
90
128
|
default: jsonField.default,
|
|
91
129
|
translatable: jsonField.translatable ?? false,
|
|
130
|
+
properties,
|
|
92
131
|
ref,
|
|
93
132
|
};
|
|
94
133
|
}
|
|
@@ -31,6 +31,7 @@ import type {
|
|
|
31
31
|
PopulateOption,
|
|
32
32
|
PopulateNested,
|
|
33
33
|
} from "../types";
|
|
34
|
+
import { isJsonType } from "../types";
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* Adapter interface for fetching records
|
|
@@ -79,7 +80,7 @@ function getRefInfo(
|
|
|
79
80
|
field: isString ? "id" : ref.field || "id",
|
|
80
81
|
localField: fieldName,
|
|
81
82
|
targetField: isString ? "id" : ref.field || "id",
|
|
82
|
-
isArray: Array.isArray(field.default) || fieldName.endsWith("Ids"),
|
|
83
|
+
isArray: isJsonType(field.type) || Array.isArray(field.default) || fieldName.endsWith("Ids"),
|
|
83
84
|
};
|
|
84
85
|
}
|
|
85
86
|
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* - Null: $isNull
|
|
13
13
|
* - Negation: $not (field-level)
|
|
14
14
|
* - Logical: $and, $or, $nor
|
|
15
|
+
* - JSON Array: $contains, $containsAny (for JSON array fields stored as TEXT)
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
export interface WhereResult {
|
|
@@ -68,6 +69,22 @@ function processFieldOperators(
|
|
|
68
69
|
params.push(value[0], value[1]);
|
|
69
70
|
} else if (op === "$isNull") {
|
|
70
71
|
parts.push(value ? `${field} IS NULL` : `${field} IS NOT NULL`);
|
|
72
|
+
} else if (op === "$contains") {
|
|
73
|
+
// JSON array contains a single value: EXISTS (SELECT 1 FROM json_each(field) WHERE value = ?)
|
|
74
|
+
parts.push(`EXISTS (SELECT 1 FROM json_each(${field}) WHERE value = ?)`);
|
|
75
|
+
params.push(value);
|
|
76
|
+
} else if (op === "$containsAny") {
|
|
77
|
+
// JSON array contains any of the values: EXISTS (SELECT 1 FROM json_each(field) WHERE value IN (?, ?))
|
|
78
|
+
if (!Array.isArray(value)) {
|
|
79
|
+
throw new Error("$containsAny operator requires an array value");
|
|
80
|
+
}
|
|
81
|
+
if (value.length === 0) {
|
|
82
|
+
parts.push("0 = 1");
|
|
83
|
+
} else {
|
|
84
|
+
const placeholders = value.map(() => "?").join(", ");
|
|
85
|
+
parts.push(`EXISTS (SELECT 1 FROM json_each(${field}) WHERE value IN (${placeholders}))`);
|
|
86
|
+
params.push(...value);
|
|
87
|
+
}
|
|
71
88
|
} else if (op === "$not") {
|
|
72
89
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
73
90
|
throw new Error("$not operator requires an object with operators");
|