@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 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, opts);
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: jsonField.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 === "json" && typeof value === "string") {
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 === "json" && typeof value !== "string") {
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 = "id" | "string" | "text" | "int" | "decimal" | "bool" | "timestamp" | "json";
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: FieldType;
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[];
@@ -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
  export interface WhereResult {
17
18
  sql: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promakeai/orm",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Database-agnostic ORM core - works in browser and Node.js",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
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, opts);
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: FieldType;
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 === "json" && typeof value === "string") {
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 === "json" && typeof value !== "string") {
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: jsonField.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");