@statezero/core 0.1.57 → 0.1.59

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.
@@ -37,51 +37,6 @@ export function processIncludedEntities(modelStoreRegistry, included, ModelClass
37
37
  throw new Error(`Failed to process included entities: ${error.message}`);
38
38
  }
39
39
  }
40
- /**
41
- * Recursively processes an object to replace FileObject instances with their file paths.
42
- * Throws an error if any FileObject is not yet uploaded.
43
- *
44
- * @param {any} obj - The object to process
45
- * @returns {any} The processed object with FileObjects replaced by paths
46
- */
47
- function processFileObjects(obj) {
48
- if (obj === null || obj === undefined) {
49
- return obj;
50
- }
51
- // Handle FileObject instances
52
- if (obj instanceof FileObject) {
53
- const status = obj.status;
54
- if (status === 'uploaded' && obj.filePath) {
55
- return obj.filePath;
56
- }
57
- else if (status === 'error') {
58
- throw new Error(`Cannot use FileObject in query - upload failed: ${obj.uploadError}`);
59
- }
60
- else if (status === 'uploading') {
61
- throw new Error(`Cannot use FileObject in query - file is still uploading. Wait for upload to complete before executing the query.`);
62
- }
63
- else if (status === 'pending') {
64
- throw new Error(`Cannot use FileObject in query - file upload has not started yet.`);
65
- }
66
- else {
67
- throw new Error(`Cannot use FileObject in query - unexpected status: ${status}`);
68
- }
69
- }
70
- // Handle arrays
71
- if (Array.isArray(obj)) {
72
- return obj.map(item => processFileObjects(item));
73
- }
74
- // Handle plain objects
75
- if (typeof obj === 'object' && obj.constructor === Object) {
76
- const processedObj = {};
77
- for (const [key, value] of Object.entries(obj)) {
78
- processedObj[key] = processFileObjects(value);
79
- }
80
- return processedObj;
81
- }
82
- // Return primitive values as-is
83
- return obj;
84
- }
85
40
  /**
86
41
  * Makes an API call to the backend with the given QuerySet.
87
42
  * Automatically handles FileObject replacement with file paths for write operations.
@@ -129,15 +84,6 @@ export async function makeApiCall(querySet, operationType, args = {}, operationI
129
84
  "get_or_create", "update_or_create"
130
85
  ];
131
86
  const isWriteOperation = writeOperations.includes(operationType);
132
- // Process FileObjects for write operations
133
- if (isWriteOperation) {
134
- try {
135
- payload = processFileObjects(payload);
136
- }
137
- catch (error) {
138
- throw new Error(`Failed to process file uploads: ${error.message}`);
139
- }
140
- }
141
87
  const baseUrl = backend.API_URL.replace(/\/+$/, "");
142
88
  const finalUrl = `${baseUrl}/${ModelClass.modelName}/`;
143
89
  const headers = backend.getAuthHeaders ? backend.getAuthHeaders() : {};
@@ -47,6 +47,7 @@ export class Model {
47
47
  _data: {};
48
48
  _pk: any;
49
49
  __version: number;
50
+ serializer: ModelSerializer;
50
51
  touch(): void;
51
52
  /**
52
53
  * Sets the primary key of the model instance.
@@ -74,13 +75,6 @@ export class Model {
74
75
  * @param {any} value - The field value to set
75
76
  */
76
77
  setField(field: string, value: any): void;
77
- /**
78
- * Serializes a field value for API transmission
79
- *
80
- * @param {string} field - The field name
81
- * @returns {any} The serialized field value
82
- */
83
- serializeField(field: string): any;
84
78
  /**
85
79
  * Serializes the model instance.
86
80
  *
@@ -125,3 +119,4 @@ export class Model {
125
119
  * A constructor for a Model.
126
120
  */
127
121
  export type ModelConstructor = Function;
122
+ import { ModelSerializer } from "./serializers.js";
@@ -1,7 +1,3 @@
1
- var __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) {
2
- if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : "";
3
- return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name });
4
- };
5
1
  import { Manager } from "./manager.js";
6
2
  import { ValidationError } from "./errors.js";
7
3
  import { modelStoreRegistry } from "../../syncEngine/registries/modelStoreRegistry.js";
@@ -11,6 +7,7 @@ import { wrapReactiveModel } from "../../reactiveAdaptor.js";
11
7
  import { DateParsingHelpers } from "./dates.js";
12
8
  import { FileObject } from './files.js';
13
9
  import { configInstance } from "../../config.js";
10
+ import { ModelSerializer } from "./serializers.js";
14
11
  import { parseStateZeroError, MultipleObjectsReturned, DoesNotExist, } from "./errors.js";
15
12
  import axios from "axios";
16
13
  /**
@@ -36,6 +33,7 @@ export class Model {
36
33
  this._data = data;
37
34
  this._pk = data[this.constructor.primaryKeyField] || undefined;
38
35
  this.__version = 0;
36
+ this.serializer = new ModelSerializer(this.constructor);
39
37
  return wrapReactiveModel(this);
40
38
  }
41
39
  touch() {
@@ -78,7 +76,6 @@ export class Model {
78
76
  * @returns {any} The field value
79
77
  */
80
78
  getField(field) {
81
- var _a;
82
79
  // Access the reactive __version property to establish dependency for vue integration
83
80
  const trackVersion = this.__version;
84
81
  const ModelClass = this.constructor;
@@ -92,57 +89,8 @@ export class Model {
92
89
  if (storedValue)
93
90
  value = storedValue[field]; // if stops null -> undefined
94
91
  }
95
- // Date/DateTime fields need special handling - convert to Date objects
96
- const dateFormats = ["date", "datetime", "date-time"];
97
- if (ModelClass.schema &&
98
- dateFormats.includes(ModelClass.schema.properties[field]?.format) &&
99
- value) {
100
- // Let DateParsingHelpers.parseDate throw if it fails
101
- return DateParsingHelpers.parseDate(value, field, ModelClass.schema);
102
- }
103
- // File/Image fields need special handling - wrap as FileObject
104
- const fileFormats = ["file-path", "image-path"];
105
- if (ModelClass.schema &&
106
- fileFormats.includes(ModelClass.schema.properties[field]?.format) &&
107
- value) {
108
- // Check if it's already a FileObject
109
- if (value instanceof FileObject) {
110
- return value;
111
- }
112
- // If it's stored file data from API, wrap it as FileObject
113
- if (typeof value === "object" && value.file_path) {
114
- // Create anonymous subclass with correct configKey
115
- const BackendFileObject = (_a = class extends FileObject {
116
- },
117
- __setFunctionName(_a, "BackendFileObject"),
118
- _a.configKey = ModelClass.configKey,
119
- _a);
120
- return new BackendFileObject(value);
121
- }
122
- }
123
- // relationship fields need special handling
124
- if (ModelClass.relationshipFields.has(field) && value) {
125
- // fetch the stored value
126
- let fieldInfo = ModelClass.relationshipFields.get(field);
127
- // footgun - fieldInfo.ModelClass() calls the arrow function that lazily gets the model class
128
- let relPkField = fieldInfo.ModelClass().primaryKeyField;
129
- switch (fieldInfo.relationshipType) {
130
- case "many-to-many":
131
- // value is an array
132
- if (!Array.isArray(value) && value)
133
- throw new Error(`Data corruption: m2m field for ${ModelClass.modelName} stored as ${value}`);
134
- // set each pk to the full model object for that pk
135
- value = value.map((pk) => fieldInfo.ModelClass().fromPk(pk));
136
- break;
137
- case "one-to-one":
138
- case "foreign-key":
139
- // footgun - fieldInfo.ModelClass() calls the arrow function that lazily gets the model class
140
- if (!isNil(value))
141
- value = fieldInfo.ModelClass().fromPk(value);
142
- break;
143
- }
144
- }
145
- return value;
92
+ // Use serializer to convert internal format to live format
93
+ return this.serializer.toLiveField(field, value);
146
94
  }
147
95
  /**
148
96
  * Sets a field value in the internal data store
@@ -152,11 +100,13 @@ export class Model {
152
100
  */
153
101
  setField(field, value) {
154
102
  const ModelClass = this.constructor;
103
+ // Use serializer to convert live format to internal format
104
+ const internalValue = this.serializer.toInternalField(field, value);
155
105
  if (ModelClass.primaryKeyField === field) {
156
- this._pk = value;
106
+ this._pk = internalValue;
157
107
  }
158
108
  else {
159
- this._data[field] = value;
109
+ this._data[field] = internalValue;
160
110
  }
161
111
  }
162
112
  /**
@@ -184,40 +134,6 @@ export class Model {
184
134
  }
185
135
  }
186
136
  }
187
- /**
188
- * Serializes a field value for API transmission
189
- *
190
- * @param {string} field - The field name
191
- * @returns {any} The serialized field value
192
- */
193
- serializeField(field) {
194
- const ModelClass = this.constructor;
195
- if (ModelClass.primaryKeyField === field)
196
- return this._pk;
197
- // check local overrides
198
- let value = this._data[field];
199
- // if it's not been overridden, get it from the store
200
- if (value === undefined && !isNil(this._pk)) {
201
- let storedValue = modelStoreRegistry.getEntity(ModelClass, this._pk);
202
- if (storedValue)
203
- value = storedValue[field];
204
- }
205
- // Date/DateTime fields need special handling - convert Date objects to strings for API
206
- const dateFormats = ["date", "date-time"];
207
- if (ModelClass.schema &&
208
- dateFormats.includes(ModelClass.schema.properties[field]?.format) &&
209
- value instanceof Date) {
210
- // Let DateParsingHelpers.serializeDate throw if it fails
211
- return DateParsingHelpers.serializeDate(value, field, ModelClass.schema);
212
- }
213
- const fileFormats = ["file-path", "image-path"];
214
- if (ModelClass.schema &&
215
- fileFormats.includes(ModelClass.schema.properties[field]?.format) &&
216
- value) {
217
- return value.filePath || value.file_path || value;
218
- }
219
- return value;
220
- }
221
137
  /**
222
138
  * Serializes the model instance.
223
139
  *
@@ -227,13 +143,27 @@ export class Model {
227
143
  * @returns {Object} The serialized model data.
228
144
  */
229
145
  serialize() {
230
- const serialized = {};
231
146
  const ModelClass = this.constructor;
232
- // Include all fields defined in the model
147
+ const data = {};
148
+ // Collect all field values (already in internal format)
233
149
  for (const field of ModelClass.fields) {
234
- serialized[field] = this.serializeField(field);
150
+ if (field === ModelClass.primaryKeyField) {
151
+ data[field] = this._pk;
152
+ }
153
+ else {
154
+ let value = this._data[field];
155
+ // Get from store if not in local data
156
+ if (value === undefined && !isNil(this._pk)) {
157
+ const storedData = modelStoreRegistry.getEntity(ModelClass, this._pk);
158
+ if (storedData) {
159
+ value = storedData[field];
160
+ }
161
+ }
162
+ data[field] = value;
163
+ }
235
164
  }
236
- return serialized;
165
+ // Data is already in internal format, so return as-is for API transmission
166
+ return data;
237
167
  }
238
168
  /**
239
169
  * Saves the model instance by either creating a new record or updating an existing one.
@@ -1,70 +1,3 @@
1
- /**
2
- * @typedef {'count'|'sum'|'avg'|'min'|'max'} AggregateFunction
3
- */
4
- /**
5
- * @typedef {Object} Aggregation
6
- * @property {AggregateFunction} function - The aggregation function.
7
- * @property {string} field - The field to aggregate.
8
- * @property {string} [alias] - Optional alias for the aggregated field.
9
- */
10
- /**
11
- * @typedef {'filter'|'or'|'and'|'not'|'exclude'|'get'|'create'|'update'|'delete'|'get_or_create'|'update_or_create'|'first'|'last'|'exists'|'search'} QueryOperationType
12
- */
13
- /**
14
- * @typedef {Object} QueryNode
15
- * @property {QueryOperationType} type - The operation type.
16
- * @property {Object.<string, any>} [conditions] - Filter conditions.
17
- * @property {QueryNode[]} [children] - Child query nodes.
18
- * @property {any} [lookup] - Extra parameter for operations that need it.
19
- * @property {Partial<any>} [defaults] - Default values for create operations.
20
- * @property {number} [pk] - Primary key value.
21
- * @property {any} [data] - Data payload.
22
- * @property {string} [searchQuery] - Search term for search operations.
23
- * @property {string[]} [searchFields] - Optional array of field names for search.
24
- */
25
- /**
26
- * @typedef {Object} SerializerOptions
27
- * @property {number} [depth] - How deep to serialize nested objects.
28
- * @property {string[]} [fields] - Fields to include.
29
- * @property {number} [limit] - Limit for pagination.
30
- * @property {number} [offset] - Offset for pagination.
31
- * @property {number} [overfetch] - Overfetch additional items, for smooth optimistic delete replacement.
32
- * @property {string} [namespace] - Custom namespace for real-time updates.
33
- */
34
- /**
35
- * @template T
36
- * @typedef {Object.<string, any>} FieldLookup
37
- */
38
- /**
39
- * @template T
40
- * @typedef {Object.<string, any>} ObjectLookup
41
- */
42
- /**
43
- * Django-specific Q helper type.
44
- *
45
- * A QCondition is either a partial object of type T or a combination
46
- * of partial field and object lookups.
47
- *
48
- * @template T
49
- * @typedef {Partial<T> | (Partial<FieldLookup<T>> & Partial<ObjectLookup<T>>)} QCondition
50
- */
51
- /**
52
- * Django-specific Q helper type representing a logical grouping of conditions.
53
- *
54
- * @template T
55
- * @typedef {Object} QObject
56
- * @property {'AND'|'OR'} operator - The logical operator.
57
- * @property {Array<QCondition<T>|QObject<T>>} conditions - An array of conditions or nested Q objects.
58
- */
59
- /**
60
- * Creates a Q object for combining conditions.
61
- *
62
- * @template T
63
- * @param {'AND'|'OR'} operator - The operator to combine conditions.
64
- * @param {...(QCondition<T>|QObject<T>)} conditions - The conditions to combine.
65
- * @returns {QObject<T>} The combined Q object.
66
- */
67
- export function Q<T>(operator: "AND" | "OR", ...conditions: (QCondition<T> | QObject<T>)[]): QObject<T>;
68
1
  /**
69
2
  * A QuerySet provides a fluent API for constructing and executing queries.
70
3
  *
@@ -93,7 +26,7 @@ export class QuerySet<T> {
93
26
  fields?: Set<string> | undefined;
94
27
  aggregations?: Aggregation[] | undefined;
95
28
  initialQueryset?: string | undefined;
96
- serializerOptions?: SerializerOptions | undefined;
29
+ serializerOptions?: any;
97
30
  materialized?: boolean | undefined;
98
31
  }, parent?: null);
99
32
  ModelClass: ModelConstructor;
@@ -105,11 +38,12 @@ export class QuerySet<T> {
105
38
  _fields: Set<string>;
106
39
  _aggregations: Aggregation[];
107
40
  _initialQueryset: string | undefined;
108
- _serializerOptions: SerializerOptions;
41
+ _serializerOptions: any;
109
42
  _materialized: boolean;
110
43
  __uuid: string;
111
44
  __parent: any;
112
45
  __reactivityId: any;
46
+ _serializer: ModelSerializer;
113
47
  /**
114
48
  * Clones this QuerySet, creating a new instance with the same configuration.
115
49
  *
@@ -138,6 +72,14 @@ export class QuerySet<T> {
138
72
  * @returns {QuerySet} This QuerySet instance for chaining.
139
73
  */
140
74
  setSerializerOptions(options: SerializerOptions): QuerySet<any>;
75
+ /**
76
+ * Serializes filter conditions using the model serializer.
77
+ *
78
+ * @private
79
+ * @param {Object} conditions - The filter conditions to serialize.
80
+ * @returns {Object} The serialized conditions.
81
+ */
82
+ private _serializeConditions;
141
83
  /**
142
84
  * Filters the QuerySet with the provided conditions.
143
85
  *
@@ -275,6 +217,22 @@ export class QuerySet<T> {
275
217
  * @throws {DoesNotExist} If no records are found.
276
218
  */
277
219
  get(filters?: Object, serializerOptions?: SerializerOptions): Promise<T>;
220
+ /**
221
+ * Gets or creates a record based on the provided lookup parameters.
222
+ *
223
+ * @param {Object} lookupParams - The lookup parameters to find the record.
224
+ * @param {Object} [defaults={}] - Default values to use when creating a new record.
225
+ * @returns {Promise<[T, boolean]>} A promise that resolves to a tuple of [instance, created].
226
+ */
227
+ getOrCreate(lookupParams: Object, defaults?: Object): Promise<[T, boolean]>;
228
+ /**
229
+ * Updates or creates a record based on the provided lookup parameters.
230
+ *
231
+ * @param {Object} lookupParams - The lookup parameters to find the record.
232
+ * @param {Object} [defaults={}] - Default values to use when creating or updating.
233
+ * @returns {Promise<[T, boolean]>} A promise that resolves to a tuple of [instance, created].
234
+ */
235
+ updateOrCreate(lookupParams: Object, defaults?: Object): Promise<[T, boolean]>;
278
236
  /**
279
237
  * Builds the final query object to be sent to the backend (simple jsonable object format).
280
238
  *
@@ -302,111 +260,4 @@ export class QuerySet<T> {
302
260
  */
303
261
  [Symbol.asyncIterator](): AsyncIterator<T>;
304
262
  }
305
- export type AggregateFunction = "count" | "sum" | "avg" | "min" | "max";
306
- export type Aggregation = {
307
- /**
308
- * - The aggregation function.
309
- */
310
- function: AggregateFunction;
311
- /**
312
- * - The field to aggregate.
313
- */
314
- field: string;
315
- /**
316
- * - Optional alias for the aggregated field.
317
- */
318
- alias?: string | undefined;
319
- };
320
- export type QueryOperationType = "filter" | "or" | "and" | "not" | "exclude" | "get" | "create" | "update" | "delete" | "get_or_create" | "update_or_create" | "first" | "last" | "exists" | "search";
321
- export type QueryNode = {
322
- /**
323
- * - The operation type.
324
- */
325
- type: QueryOperationType;
326
- /**
327
- * - Filter conditions.
328
- */
329
- conditions?: {
330
- [x: string]: any;
331
- } | undefined;
332
- /**
333
- * - Child query nodes.
334
- */
335
- children?: QueryNode[] | undefined;
336
- /**
337
- * - Extra parameter for operations that need it.
338
- */
339
- lookup?: any;
340
- /**
341
- * - Default values for create operations.
342
- */
343
- defaults?: Partial<any> | undefined;
344
- /**
345
- * - Primary key value.
346
- */
347
- pk?: number | undefined;
348
- /**
349
- * - Data payload.
350
- */
351
- data?: any;
352
- /**
353
- * - Search term for search operations.
354
- */
355
- searchQuery?: string | undefined;
356
- /**
357
- * - Optional array of field names for search.
358
- */
359
- searchFields?: string[] | undefined;
360
- };
361
- export type SerializerOptions = {
362
- /**
363
- * - How deep to serialize nested objects.
364
- */
365
- depth?: number | undefined;
366
- /**
367
- * - Fields to include.
368
- */
369
- fields?: string[] | undefined;
370
- /**
371
- * - Limit for pagination.
372
- */
373
- limit?: number | undefined;
374
- /**
375
- * - Offset for pagination.
376
- */
377
- offset?: number | undefined;
378
- /**
379
- * - Overfetch additional items, for smooth optimistic delete replacement.
380
- */
381
- overfetch?: number | undefined;
382
- /**
383
- * - Custom namespace for real-time updates.
384
- */
385
- namespace?: string | undefined;
386
- };
387
- export type FieldLookup<T> = {
388
- [x: string]: any;
389
- };
390
- export type ObjectLookup<T> = {
391
- [x: string]: any;
392
- };
393
- /**
394
- * Django-specific Q helper type.
395
- *
396
- * A QCondition is either a partial object of type T or a combination
397
- * of partial field and object lookups.
398
- */
399
- export type QCondition<T> = Partial<T> | (Partial<FieldLookup<T>> & Partial<ObjectLookup<T>>);
400
- /**
401
- * Django-specific Q helper type representing a logical grouping of conditions.
402
- */
403
- export type QObject<T> = {
404
- /**
405
- * - The logical operator.
406
- */
407
- operator: "AND" | "OR";
408
- /**
409
- * - An array of conditions or nested Q objects.
410
- */
411
- conditions: Array<QCondition<T> | QObject<T>>;
412
- };
263
+ import { ModelSerializer } from "./serializers.js";
@@ -1,83 +1,12 @@
1
- /**
2
- * @typedef {'count'|'sum'|'avg'|'min'|'max'} AggregateFunction
3
- */
4
- /**
5
- * @typedef {Object} Aggregation
6
- * @property {AggregateFunction} function - The aggregation function.
7
- * @property {string} field - The field to aggregate.
8
- * @property {string} [alias] - Optional alias for the aggregated field.
9
- */
10
- /**
11
- * @typedef {'filter'|'or'|'and'|'not'|'exclude'|'get'|'create'|'update'|'delete'|'get_or_create'|'update_or_create'|'first'|'last'|'exists'|'search'} QueryOperationType
12
- */
13
- /**
14
- * @typedef {Object} QueryNode
15
- * @property {QueryOperationType} type - The operation type.
16
- * @property {Object.<string, any>} [conditions] - Filter conditions.
17
- * @property {QueryNode[]} [children] - Child query nodes.
18
- * @property {any} [lookup] - Extra parameter for operations that need it.
19
- * @property {Partial<any>} [defaults] - Default values for create operations.
20
- * @property {number} [pk] - Primary key value.
21
- * @property {any} [data] - Data payload.
22
- * @property {string} [searchQuery] - Search term for search operations.
23
- * @property {string[]} [searchFields] - Optional array of field names for search.
24
- */
25
- /**
26
- * @typedef {Object} SerializerOptions
27
- * @property {number} [depth] - How deep to serialize nested objects.
28
- * @property {string[]} [fields] - Fields to include.
29
- * @property {number} [limit] - Limit for pagination.
30
- * @property {number} [offset] - Offset for pagination.
31
- * @property {number} [overfetch] - Overfetch additional items, for smooth optimistic delete replacement.
32
- * @property {string} [namespace] - Custom namespace for real-time updates.
33
- */
34
- /**
35
- * @template T
36
- * @typedef {Object.<string, any>} FieldLookup
37
- */
38
- /**
39
- * @template T
40
- * @typedef {Object.<string, any>} ObjectLookup
41
- */
42
- /**
43
- * Django-specific Q helper type.
44
- *
45
- * A QCondition is either a partial object of type T or a combination
46
- * of partial field and object lookups.
47
- *
48
- * @template T
49
- * @typedef {Partial<T> | (Partial<FieldLookup<T>> & Partial<ObjectLookup<T>>)} QCondition
50
- */
51
- /**
52
- * Django-specific Q helper type representing a logical grouping of conditions.
53
- *
54
- * @template T
55
- * @typedef {Object} QObject
56
- * @property {'AND'|'OR'} operator - The logical operator.
57
- * @property {Array<QCondition<T>|QObject<T>>} conditions - An array of conditions or nested Q objects.
58
- */
59
- /**
60
- * Creates a Q object for combining conditions.
61
- *
62
- * @template T
63
- * @param {'AND'|'OR'} operator - The operator to combine conditions.
64
- * @param {...(QCondition<T>|QObject<T>)} conditions - The conditions to combine.
65
- * @returns {QObject<T>} The combined Q object.
66
- */
67
- export function Q(operator, ...conditions) {
68
- return {
69
- operator,
70
- conditions,
71
- };
72
- }
73
- import { MultipleObjectsReturned, DoesNotExist, parseStateZeroError } from './errors.js';
74
- import { Model } from './model.js';
75
- import axios from 'axios';
76
- import { QueryExecutor } from './queryExecutor.js';
77
- import { json } from 'stream/consumers';
78
- import { v7 } from 'uuid';
79
- import hash from 'object-hash';
80
- import rfdc from 'rfdc';
1
+ import { MultipleObjectsReturned, DoesNotExist, parseStateZeroError, } from "./errors.js";
2
+ import { Model } from "./model.js";
3
+ import { ModelSerializer } from "./serializers.js"; // Import the ModelSerializer
4
+ import axios from "axios";
5
+ import { QueryExecutor } from "./queryExecutor.js";
6
+ import { json } from "stream/consumers";
7
+ import { v7 } from "uuid";
8
+ import hash from "object-hash";
9
+ import rfdc from "rfdc";
81
10
  const clone = rfdc();
82
11
  /**
83
12
  * A QuerySet provides a fluent API for constructing and executing queries.
@@ -110,6 +39,8 @@ export class QuerySet {
110
39
  this.__uuid = v7();
111
40
  this.__parent = parent;
112
41
  this.__reactivityId = parent?.__reactivityId;
42
+ // Initialize the serializer for this model
43
+ this._serializer = new ModelSerializer(this.ModelClass);
113
44
  }
114
45
  /**
115
46
  * Clones this QuerySet, creating a new instance with the same configuration.
@@ -168,6 +99,20 @@ export class QuerySet {
168
99
  this._serializerOptions = { ...this._serializerOptions, ...options };
169
100
  return this;
170
101
  }
102
+ /**
103
+ * Serializes filter conditions using the model serializer.
104
+ *
105
+ * @private
106
+ * @param {Object} conditions - The filter conditions to serialize.
107
+ * @returns {Object} The serialized conditions.
108
+ */
109
+ _serializeConditions(conditions) {
110
+ if (!conditions || typeof conditions !== "object") {
111
+ return conditions;
112
+ }
113
+ // Use the model serializer to convert conditions to internal format
114
+ return this._serializer.toInternal(conditions);
115
+ }
171
116
  /**
172
117
  * Filters the QuerySet with the provided conditions.
173
118
  *
@@ -179,20 +124,22 @@ export class QuerySet {
179
124
  const { Q: qConditions, ...filters } = conditions;
180
125
  const newNodes = [...this.nodes];
181
126
  if (Object.keys(filters).length > 0) {
127
+ // Serialize the filter conditions before adding to the node
128
+ const serializedFilters = this._serializeConditions(filters);
182
129
  newNodes.push({
183
- type: 'filter',
184
- conditions: filters
130
+ type: "filter",
131
+ conditions: serializedFilters,
185
132
  });
186
133
  }
187
134
  if (qConditions && qConditions.length) {
188
135
  newNodes.push({
189
- type: 'and',
190
- children: qConditions.map(q => this.processQObject(q))
136
+ type: "and",
137
+ children: qConditions.map((q) => this.processQObject(q)),
191
138
  });
192
139
  }
193
140
  return new QuerySet(this.ModelClass, {
194
141
  ...this._getConfig(),
195
- nodes: newNodes
142
+ nodes: newNodes,
196
143
  }, this);
197
144
  }
198
145
  /**
@@ -207,34 +154,41 @@ export class QuerySet {
207
154
  const newNodes = [...this.nodes];
208
155
  let childNode = null;
209
156
  if (Object.keys(filters).length > 0 && qConditions && qConditions.length) {
157
+ // Serialize the filter conditions
158
+ const serializedFilters = this._serializeConditions(filters);
210
159
  childNode = {
211
- type: 'and',
160
+ type: "and",
212
161
  children: [
213
- { type: 'filter', conditions: filters },
214
- { type: 'and', children: qConditions.map(q => this.processQObject(q)) }
215
- ]
162
+ { type: "filter", conditions: serializedFilters },
163
+ {
164
+ type: "and",
165
+ children: qConditions.map((q) => this.processQObject(q)),
166
+ },
167
+ ],
216
168
  };
217
169
  }
218
170
  else if (Object.keys(filters).length > 0) {
171
+ // Serialize the filter conditions
172
+ const serializedFilters = this._serializeConditions(filters);
219
173
  childNode = {
220
- type: 'filter',
221
- conditions: filters
174
+ type: "filter",
175
+ conditions: serializedFilters,
222
176
  };
223
177
  }
224
178
  else if (qConditions && qConditions.length) {
225
179
  childNode = {
226
- type: 'and',
227
- children: qConditions.map(q => this.processQObject(q))
180
+ type: "and",
181
+ children: qConditions.map((q) => this.processQObject(q)),
228
182
  };
229
183
  }
230
184
  const excludeNode = {
231
- type: 'exclude',
232
- child: childNode
185
+ type: "exclude",
186
+ child: childNode,
233
187
  };
234
188
  newNodes.push(excludeNode);
235
189
  return new QuerySet(this.ModelClass, {
236
190
  ...this._getConfig(),
237
- nodes: newNodes
191
+ nodes: newNodes,
238
192
  }, this);
239
193
  }
240
194
  /**
@@ -247,7 +201,7 @@ export class QuerySet {
247
201
  this.ensureNotMaterialized();
248
202
  return new QuerySet(this.ModelClass, {
249
203
  ...this._getConfig(),
250
- orderBy: fields
204
+ orderBy: fields,
251
205
  }, this);
252
206
  }
253
207
  /**
@@ -261,13 +215,13 @@ export class QuerySet {
261
215
  this.ensureNotMaterialized();
262
216
  const newNodes = [...this.nodes];
263
217
  newNodes.push({
264
- type: 'search',
218
+ type: "search",
265
219
  searchQuery,
266
- searchFields: searchFields
220
+ searchFields: searchFields,
267
221
  });
268
222
  return new QuerySet(this.ModelClass, {
269
223
  ...this._getConfig(),
270
- nodes: newNodes
224
+ nodes: newNodes,
271
225
  }, this);
272
226
  }
273
227
  /**
@@ -278,18 +232,20 @@ export class QuerySet {
278
232
  * @returns {QueryNode} The processed QueryNode.
279
233
  */
280
234
  processQObject(q) {
281
- if ('operator' in q && 'conditions' in q) {
235
+ if ("operator" in q && "conditions" in q) {
282
236
  return {
283
- type: q.operator === 'AND' ? 'and' : 'or',
237
+ type: q.operator === "AND" ? "and" : "or",
284
238
  children: Array.isArray(q.conditions)
285
- ? q.conditions.map(c => this.processQObject(c))
286
- : []
239
+ ? q.conditions.map((c) => this.processQObject(c))
240
+ : [],
287
241
  };
288
242
  }
289
243
  else {
244
+ // Serialize the conditions in Q objects as well
245
+ const serializedConditions = this._serializeConditions(q);
290
246
  return {
291
- type: 'filter',
292
- conditions: q
247
+ type: "filter",
248
+ conditions: serializedConditions,
293
249
  };
294
250
  }
295
251
  }
@@ -305,11 +261,14 @@ export class QuerySet {
305
261
  this.ensureNotMaterialized();
306
262
  return new QuerySet(this.ModelClass, {
307
263
  ...this._getConfig(),
308
- aggregations: [...this._aggregations, {
264
+ aggregations: [
265
+ ...this._aggregations,
266
+ {
309
267
  function: fn,
310
268
  field: field,
311
- alias
312
- }]
269
+ alias,
270
+ },
271
+ ],
313
272
  }, this);
314
273
  }
315
274
  /**
@@ -322,9 +281,11 @@ export class QuerySet {
322
281
  this.ensureNotMaterialized();
323
282
  const newQs = new QuerySet(this.ModelClass, {
324
283
  ...this._getConfig(),
325
- materialized: true
284
+ materialized: true,
326
285
  }, this);
327
- return QueryExecutor.execute(newQs, 'count', { field: field || this.ModelClass.primaryKeyField });
286
+ return QueryExecutor.execute(newQs, "count", {
287
+ field: field || this.ModelClass.primaryKeyField,
288
+ });
328
289
  }
329
290
  /**
330
291
  * Executes a sum aggregation on the QuerySet.
@@ -336,9 +297,9 @@ export class QuerySet {
336
297
  this.ensureNotMaterialized();
337
298
  const newQs = new QuerySet(this.ModelClass, {
338
299
  ...this._getConfig(),
339
- materialized: true
300
+ materialized: true,
340
301
  }, this);
341
- return QueryExecutor.execute(newQs, 'sum', { field });
302
+ return QueryExecutor.execute(newQs, "sum", { field });
342
303
  }
343
304
  /**
344
305
  * Executes an average aggregation on the QuerySet.
@@ -350,9 +311,9 @@ export class QuerySet {
350
311
  this.ensureNotMaterialized();
351
312
  const newQs = new QuerySet(this.ModelClass, {
352
313
  ...this._getConfig(),
353
- materialized: true
314
+ materialized: true,
354
315
  }, this);
355
- return QueryExecutor.execute(newQs, 'avg', { field });
316
+ return QueryExecutor.execute(newQs, "avg", { field });
356
317
  }
357
318
  /**
358
319
  * Executes a min aggregation on the QuerySet.
@@ -364,9 +325,9 @@ export class QuerySet {
364
325
  this.ensureNotMaterialized();
365
326
  const newQs = new QuerySet(this.ModelClass, {
366
327
  ...this._getConfig(),
367
- materialized: true
328
+ materialized: true,
368
329
  }, this);
369
- return QueryExecutor.execute(newQs, 'min', { field });
330
+ return QueryExecutor.execute(newQs, "min", { field });
370
331
  }
371
332
  /**
372
333
  * Executes a max aggregation on the QuerySet.
@@ -378,9 +339,9 @@ export class QuerySet {
378
339
  this.ensureNotMaterialized();
379
340
  const newQs = new QuerySet(this.ModelClass, {
380
341
  ...this._getConfig(),
381
- materialized: true
342
+ materialized: true,
382
343
  }, this);
383
- return QueryExecutor.execute(newQs, 'max', { field });
344
+ return QueryExecutor.execute(newQs, "max", { field });
384
345
  }
385
346
  /**
386
347
  * Retrieves the first record of the QuerySet.
@@ -392,10 +353,12 @@ export class QuerySet {
392
353
  this.ensureNotMaterialized();
393
354
  const newQs = new QuerySet(this.ModelClass, {
394
355
  ...this._getConfig(),
395
- serializerOptions: serializerOptions ? { ...this._serializerOptions, ...serializerOptions } : this._serializerOptions,
396
- materialized: true
356
+ serializerOptions: serializerOptions
357
+ ? { ...this._serializerOptions, ...serializerOptions }
358
+ : this._serializerOptions,
359
+ materialized: true,
397
360
  }, this);
398
- return QueryExecutor.execute(newQs, 'first');
361
+ return QueryExecutor.execute(newQs, "first");
399
362
  }
400
363
  /**
401
364
  * Retrieves the last record of the QuerySet.
@@ -407,10 +370,12 @@ export class QuerySet {
407
370
  this.ensureNotMaterialized();
408
371
  const newQs = new QuerySet(this.ModelClass, {
409
372
  ...this._getConfig(),
410
- serializerOptions: serializerOptions ? { ...this._serializerOptions, ...serializerOptions } : this._serializerOptions,
411
- materialized: true
373
+ serializerOptions: serializerOptions
374
+ ? { ...this._serializerOptions, ...serializerOptions }
375
+ : this._serializerOptions,
376
+ materialized: true,
412
377
  }, this);
413
- return QueryExecutor.execute(newQs, 'last');
378
+ return QueryExecutor.execute(newQs, "last");
414
379
  }
415
380
  /**
416
381
  * Checks if any records exist in the QuerySet.
@@ -421,9 +386,9 @@ export class QuerySet {
421
386
  this.ensureNotMaterialized();
422
387
  const newQs = new QuerySet(this.ModelClass, {
423
388
  ...this._getConfig(),
424
- materialized: true
389
+ materialized: true,
425
390
  }, this);
426
- return QueryExecutor.execute(newQs, 'exists');
391
+ return QueryExecutor.execute(newQs, "exists");
427
392
  }
428
393
  /**
429
394
  * Applies serializer options to the QuerySet.
@@ -436,7 +401,10 @@ export class QuerySet {
436
401
  if (serializerOptions) {
437
402
  return new QuerySet(this.ModelClass, {
438
403
  ...this._getConfig(),
439
- serializerOptions: { ...this._serializerOptions, ...serializerOptions }
404
+ serializerOptions: {
405
+ ...this._serializerOptions,
406
+ ...serializerOptions,
407
+ },
440
408
  }, this);
441
409
  }
442
410
  return this;
@@ -448,12 +416,14 @@ export class QuerySet {
448
416
  */
449
417
  async create(data) {
450
418
  this.ensureNotMaterialized();
419
+ // Serialize the data before sending to backend
420
+ const serializedData = this._serializer.toInternal(data);
451
421
  // Materialize for create
452
422
  const newQs = new QuerySet(this.ModelClass, {
453
423
  ...this._getConfig(),
454
- materialized: true
424
+ materialized: true,
455
425
  }, this);
456
- return QueryExecutor.execute(newQs, 'create', { data });
426
+ return QueryExecutor.execute(newQs, "create", { data: serializedData });
457
427
  }
458
428
  /**
459
429
  * Updates records in the QuerySet.
@@ -463,14 +433,16 @@ export class QuerySet {
463
433
  */
464
434
  update(updates) {
465
435
  if (arguments.length > 1) {
466
- throw new Error('Update accepts only accepts an object of the updates to apply. Use filter() before calling update() to select elements.');
436
+ throw new Error("Update accepts only accepts an object of the updates to apply. Use filter() before calling update() to select elements.");
467
437
  }
468
438
  this.ensureNotMaterialized();
439
+ // Serialize the updates before sending to backend
440
+ const serializedUpdates = this._serializer.toInternal(updates);
469
441
  const newQs = new QuerySet(this.ModelClass, {
470
442
  ...this._getConfig(),
471
- materialized: true
443
+ materialized: true,
472
444
  });
473
- return QueryExecutor.execute(newQs, 'update', { data: updates });
445
+ return QueryExecutor.execute(newQs, "update", { data: serializedUpdates });
474
446
  }
475
447
  /**
476
448
  * Deletes records in the QuerySet.
@@ -479,14 +451,14 @@ export class QuerySet {
479
451
  */
480
452
  delete() {
481
453
  if (arguments.length > 0) {
482
- throw new Error('delete() does not accept arguments and will delete the entire queryset. Use filter() before calling delete() to select elements.');
454
+ throw new Error("delete() does not accept arguments and will delete the entire queryset. Use filter() before calling delete() to select elements.");
483
455
  }
484
456
  this.ensureNotMaterialized();
485
457
  const newQs = new QuerySet(this.ModelClass, {
486
458
  ...this._getConfig(),
487
- materialized: true
459
+ materialized: true,
488
460
  }, this);
489
- return QueryExecutor.execute(newQs, 'delete');
461
+ return QueryExecutor.execute(newQs, "delete");
490
462
  }
491
463
  /**
492
464
  * Retrieves a single record from the QuerySet.
@@ -506,19 +478,64 @@ export class QuerySet {
506
478
  if (serializerOptions) {
507
479
  newQs = new QuerySet(this.ModelClass, {
508
480
  ...newQs._getConfig(),
509
- serializerOptions: { ...newQs._serializerOptions, ...serializerOptions }
481
+ serializerOptions: {
482
+ ...newQs._serializerOptions,
483
+ ...serializerOptions,
484
+ },
510
485
  }, this);
511
486
  }
512
487
  const materializedQs = new QuerySet(this.ModelClass, {
513
488
  ...newQs._getConfig(),
514
- materialized: true
489
+ materialized: true,
515
490
  }, this);
516
- const result = QueryExecutor.execute(materializedQs, 'get');
491
+ const result = QueryExecutor.execute(materializedQs, "get");
517
492
  if (result === null) {
518
493
  throw new DoesNotExist();
519
494
  }
520
495
  return result;
521
496
  }
497
+ /**
498
+ * Gets or creates a record based on the provided lookup parameters.
499
+ *
500
+ * @param {Object} lookupParams - The lookup parameters to find the record.
501
+ * @param {Object} [defaults={}] - Default values to use when creating a new record.
502
+ * @returns {Promise<[T, boolean]>} A promise that resolves to a tuple of [instance, created].
503
+ */
504
+ async getOrCreate(lookupParams, defaults = {}) {
505
+ this.ensureNotMaterialized();
506
+ // Serialize both lookup params and defaults
507
+ const serializedLookup = this._serializer.toInternal(lookupParams);
508
+ const serializedDefaults = this._serializer.toInternal(defaults);
509
+ const newQs = new QuerySet(this.ModelClass, {
510
+ ...this._getConfig(),
511
+ materialized: true,
512
+ }, this);
513
+ return QueryExecutor.execute(newQs, "get_or_create", {
514
+ lookup: serializedLookup,
515
+ defaults: serializedDefaults,
516
+ });
517
+ }
518
+ /**
519
+ * Updates or creates a record based on the provided lookup parameters.
520
+ *
521
+ * @param {Object} lookupParams - The lookup parameters to find the record.
522
+ * @param {Object} [defaults={}] - Default values to use when creating or updating.
523
+ * @returns {Promise<[T, boolean]>} A promise that resolves to a tuple of [instance, created].
524
+ */
525
+ async updateOrCreate(lookupParams, defaults = {}) {
526
+ this.ensureNotMaterialized();
527
+ // Serialize both lookup params and defaults
528
+ const serializedLookup = this._serializer.toInternal(lookupParams);
529
+ const serializedDefaults = this._serializer.toInternal(defaults);
530
+ const newQs = new QuerySet(this.ModelClass, {
531
+ ...this._getConfig(),
532
+ materialized: true,
533
+ }, this);
534
+ return QueryExecutor.execute(newQs, "update_or_create", {
535
+ lookup: serializedLookup,
536
+ defaults: serializedDefaults,
537
+ });
538
+ }
522
539
  /**
523
540
  * Builds the final query object to be sent to the backend (simple jsonable object format).
524
541
  *
@@ -528,27 +545,30 @@ export class QuerySet {
528
545
  let searchData = null;
529
546
  const nonSearchNodes = [];
530
547
  for (const node of this.nodes) {
531
- if (node.type === 'search') {
548
+ if (node.type === "search") {
532
549
  searchData = {
533
- searchQuery: node.searchQuery || '',
534
- searchFields: node.searchFields
550
+ searchQuery: node.searchQuery || "",
551
+ searchFields: node.searchFields,
535
552
  };
536
553
  }
537
554
  else {
538
555
  nonSearchNodes.push(node);
539
556
  }
540
557
  }
541
- const filterNode = nonSearchNodes.length === 0 ? null :
542
- nonSearchNodes.length === 1 ? nonSearchNodes[0] : {
543
- type: 'and',
544
- children: nonSearchNodes
545
- };
558
+ const filterNode = nonSearchNodes.length === 0
559
+ ? null
560
+ : nonSearchNodes.length === 1
561
+ ? nonSearchNodes[0]
562
+ : {
563
+ type: "and",
564
+ children: nonSearchNodes,
565
+ };
546
566
  return clone({
547
567
  filter: filterNode,
548
568
  search: searchData,
549
569
  aggregations: this._aggregations,
550
570
  orderBy: this._orderBy,
551
- serializerOptions: this._serializerOptions
571
+ serializerOptions: this._serializerOptions,
552
572
  });
553
573
  }
554
574
  /**
@@ -564,7 +584,7 @@ export class QuerySet {
564
584
  fields: this._fields,
565
585
  aggregations: this._aggregations,
566
586
  initialQueryset: this._initialQueryset,
567
- serializerOptions: this._serializerOptions
587
+ serializerOptions: this._serializerOptions,
568
588
  };
569
589
  }
570
590
  /**
@@ -578,14 +598,17 @@ export class QuerySet {
578
598
  if (serializerOptions) {
579
599
  querySet = new QuerySet(this.ModelClass, {
580
600
  ...this._getConfig(),
581
- serializerOptions: { ...this._serializerOptions, ...serializerOptions }
601
+ serializerOptions: {
602
+ ...this._serializerOptions,
603
+ ...serializerOptions,
604
+ },
582
605
  }, this);
583
606
  }
584
607
  const materializedQs = new QuerySet(this.ModelClass, {
585
608
  ...querySet._getConfig(),
586
- materialized: true
609
+ materialized: true,
587
610
  }, this);
588
- return QueryExecutor.execute(materializedQs, 'list');
611
+ return QueryExecutor.execute(materializedQs, "list");
589
612
  }
590
613
  /**
591
614
  * Implements the async iterator protocol so that you can iterate over the QuerySet.
@@ -0,0 +1,30 @@
1
+ export namespace fileFieldSerializer {
2
+ function toInternal(value: any, context?: {}): any;
3
+ function toLive(value: any, context?: {}): any;
4
+ }
5
+ export namespace dateFieldSerializer {
6
+ export function toInternal_1(value: any, context?: {}): any;
7
+ export { toInternal_1 as toInternal };
8
+ export function toLive_1(value: any, context?: {}): any;
9
+ export { toLive_1 as toLive };
10
+ }
11
+ export namespace relationshipFieldSerializer {
12
+ export function toInternal_2(value: any, context?: {}): string | number | null | undefined;
13
+ export { toInternal_2 as toInternal };
14
+ export function toLive_2(value: any, context?: {}): any;
15
+ export { toLive_2 as toLive };
16
+ }
17
+ export namespace m2mFieldSerializer {
18
+ export function toInternal_3(value: any, context?: {}): any;
19
+ export { toInternal_3 as toInternal };
20
+ export function toLive_3(value: any, context?: {}): any;
21
+ export { toLive_3 as toLive };
22
+ }
23
+ export class ModelSerializer {
24
+ constructor(modelClass: any);
25
+ modelClass: any;
26
+ toInternalField(field: any, value: any, context?: {}): any;
27
+ toInternal(data: any): {};
28
+ toLiveField(field: any, value: any, context?: {}): any;
29
+ toLive(data: any): {};
30
+ }
@@ -0,0 +1,217 @@
1
+ import { configInstance } from "../../config.js";
2
+ import { isNil } from "lodash-es";
3
+ import { DateParsingHelpers } from "./dates.js";
4
+ /**
5
+ * File field serializer - handles both camelCase (frontend) and snake_case (backend) formats
6
+ */
7
+ export const fileFieldSerializer = {
8
+ // Store backend snake_case format internally for consistency with API
9
+ toInternal: (value, context = {}) => {
10
+ // Handle plain strings - throw error
11
+ if (typeof value === "string") {
12
+ throw new Error("File field expects a file object, not a string path");
13
+ }
14
+ if (isNil(value)) {
15
+ return null;
16
+ }
17
+ value = {
18
+ file_path: value.filePath || value.file_path,
19
+ file_name: value.fileName || value.file_name,
20
+ file_url: value.fileUrl || value.file_url,
21
+ size: value.size,
22
+ mime_type: value.mimeType || value.mime_type,
23
+ };
24
+ return value;
25
+ },
26
+ // Return object with both formats for maximum compatibility
27
+ toLive: (value, context = {}) => {
28
+ if (!value || typeof value !== "object")
29
+ return value;
30
+ // Build the proper file URL using the same logic as FileObject
31
+ const backendKey = context.model?.constructor?.configKey || "default";
32
+ const fullFileUrl = value.file_url
33
+ ? configInstance.buildFileUrl(value.file_url, backendKey)
34
+ : null;
35
+ return {
36
+ // snake_case (backend format)
37
+ file_path: value.file_path,
38
+ file_name: value.file_name,
39
+ file_url: fullFileUrl, // Use the built URL here
40
+ size: value.size,
41
+ mime_type: value.mime_type,
42
+ // camelCase (frontend format) - for compatibility
43
+ filePath: value.file_path,
44
+ fileName: value.file_name,
45
+ fileUrl: fullFileUrl, // Use the built URL here too
46
+ mimeType: value.mime_type,
47
+ };
48
+ },
49
+ };
50
+ /**
51
+ * Date/DateTime field serializer - uses DateParsingHelpers like the model does
52
+ */
53
+ export const dateFieldSerializer = {
54
+ toInternal: (value, context = {}) => {
55
+ if (isNil(value))
56
+ return value;
57
+ // If it's already a string, keep it (from API)
58
+ if (typeof value === "string")
59
+ return value;
60
+ // If it's a Date object, serialize it using DateParsingHelpers
61
+ if (value instanceof Date) {
62
+ const { model, field } = context;
63
+ if (model?.schema) {
64
+ return DateParsingHelpers.serializeDate(value, field, model.schema);
65
+ }
66
+ // Fallback if no schema context
67
+ return value.toISOString();
68
+ }
69
+ return value;
70
+ },
71
+ toLive: (value, context = {}) => {
72
+ if (isNil(value) || typeof value !== "string")
73
+ return value;
74
+ const { model, field } = context;
75
+ if (model?.schema) {
76
+ // Use DateParsingHelpers like the model does
77
+ return DateParsingHelpers.parseDate(value, field, model.schema);
78
+ }
79
+ // Fallback parsing if no schema context
80
+ try {
81
+ return new Date(value);
82
+ }
83
+ catch (e) {
84
+ console.warn(`Failed to parse date: ${value}`);
85
+ return value;
86
+ }
87
+ },
88
+ };
89
+ /**
90
+ * Foreign Key / One-to-One field serializer - follows existing relationship logic
91
+ */
92
+ export const relationshipFieldSerializer = {
93
+ toInternal: (value, context = {}) => {
94
+ if (isNil(value))
95
+ return value;
96
+ // Extract PK or use value directly
97
+ const pk = value.pk || value;
98
+ // Assert it's a valid PK type
99
+ if (typeof pk !== "string" && typeof pk !== "number") {
100
+ throw new Error(`Invalid primary key type for relationship field: expected string or number, got ${typeof pk}`);
101
+ }
102
+ return pk;
103
+ },
104
+ toLive: (value, context = {}) => {
105
+ if (isNil(value))
106
+ return value;
107
+ // Follow the exact same pattern as the original getField code
108
+ const { model, field } = context;
109
+ if (model.relationshipFields.has(field) && value) {
110
+ const fieldInfo = model.relationshipFields.get(field);
111
+ // For foreign-key and one-to-one, value is guaranteed to be a plain PK
112
+ return fieldInfo.ModelClass().fromPk(value);
113
+ }
114
+ return value;
115
+ },
116
+ };
117
+ /**
118
+ * Many-to-Many field serializer - follows existing m2m logic
119
+ */
120
+ export const m2mFieldSerializer = {
121
+ toInternal: (value, context = {}) => {
122
+ if (isNil(value) || !Array.isArray(value))
123
+ return value;
124
+ return value.map((item) => {
125
+ // Extract PK or use item directly
126
+ const pk = item.pk || item;
127
+ // Assert it's a valid PK type
128
+ if (typeof pk !== "string" && typeof pk !== "number") {
129
+ throw new Error(`Invalid primary key type for m2m field: expected string or number, got ${typeof pk}`);
130
+ }
131
+ return pk;
132
+ });
133
+ },
134
+ toLive: (value, context = {}) => {
135
+ if (isNil(value))
136
+ return value;
137
+ // Follow the exact same pattern as the original getField code
138
+ const { model, field } = context;
139
+ if (model.relationshipFields.has(field) && value) {
140
+ const fieldInfo = model.relationshipFields.get(field);
141
+ // Data corruption check like the original
142
+ if (!Array.isArray(value) && value) {
143
+ throw new Error(`Data corruption: m2m field for ${model.modelName} stored as ${value}`);
144
+ }
145
+ // Map each pk to the full model object - exactly like original
146
+ return value.map((pk) => fieldInfo.ModelClass().fromPk(pk));
147
+ }
148
+ return value;
149
+ },
150
+ };
151
+ const serializers = {
152
+ string: {
153
+ "file-path": fileFieldSerializer,
154
+ "image-path": fileFieldSerializer,
155
+ "date": dateFieldSerializer,
156
+ "date-time": dateFieldSerializer,
157
+ "foreign-key": relationshipFieldSerializer,
158
+ "one-to-one": relationshipFieldSerializer,
159
+ },
160
+ integer: {
161
+ "foreign-key": relationshipFieldSerializer,
162
+ "one-to-one": relationshipFieldSerializer,
163
+ },
164
+ // Add other PK types as needed
165
+ uuid: {
166
+ "foreign-key": relationshipFieldSerializer,
167
+ "one-to-one": relationshipFieldSerializer,
168
+ },
169
+ array: {
170
+ "many-to-many": m2mFieldSerializer,
171
+ },
172
+ };
173
+ export class ModelSerializer {
174
+ constructor(modelClass) {
175
+ this.modelClass = modelClass;
176
+ }
177
+ toInternalField(field, value, context = {}) {
178
+ const fieldType = this.modelClass.schema?.properties[field]?.type;
179
+ const fieldFormat = this.modelClass.schema?.properties[field]?.format;
180
+ if (serializers[fieldType] && serializers[fieldType][fieldFormat]) {
181
+ return serializers[fieldType][fieldFormat].toInternal(value, {
182
+ model: this.modelClass,
183
+ field,
184
+ ...context,
185
+ });
186
+ }
187
+ // Fallback to default serialization
188
+ return value;
189
+ }
190
+ toInternal(data) {
191
+ const serializedData = {};
192
+ for (const field in data) {
193
+ serializedData[field] = this.toInternalField(field, data[field]);
194
+ }
195
+ return serializedData;
196
+ }
197
+ toLiveField(field, value, context = {}) {
198
+ const fieldType = this.modelClass.schema?.properties[field]?.type;
199
+ const fieldFormat = this.modelClass.schema?.properties[field]?.format;
200
+ if (serializers[fieldType] && serializers[fieldType][fieldFormat]) {
201
+ return serializers[fieldType][fieldFormat].toLive(value, {
202
+ model: this.modelClass,
203
+ field,
204
+ ...context,
205
+ });
206
+ }
207
+ // Fallback to default serialization
208
+ return value;
209
+ }
210
+ toLive(data) {
211
+ const serializedData = {};
212
+ for (const field in data) {
213
+ serializedData[field] = this.toLiveField(field, data[field]);
214
+ }
215
+ return serializedData;
216
+ }
217
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statezero/core",
3
- "version": "0.1.57",
3
+ "version": "0.1.59",
4
4
  "type": "module",
5
5
  "module": "ESNext",
6
6
  "description": "The type-safe frontend client for StateZero - connect directly to your backend models with zero boilerplate",