@statezero/core 0.1.58 → 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.
- package/dist/flavours/django/model.d.ts +2 -7
- package/dist/flavours/django/model.js +26 -96
- package/dist/flavours/django/querySet.d.ts +28 -177
- package/dist/flavours/django/querySet.js +175 -152
- package/dist/flavours/django/serializers.d.ts +30 -0
- package/dist/flavours/django/serializers.js +217 -0
- package/package.json +1 -1
|
@@ -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
|
-
//
|
|
96
|
-
|
|
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 =
|
|
106
|
+
this._pk = internalValue;
|
|
157
107
|
}
|
|
158
108
|
else {
|
|
159
|
-
this._data[field] =
|
|
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
|
-
|
|
147
|
+
const data = {};
|
|
148
|
+
// Collect all field values (already in internal format)
|
|
233
149
|
for (const field of ModelClass.fields) {
|
|
234
|
-
|
|
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
|
|
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?:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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:
|
|
184
|
-
conditions:
|
|
130
|
+
type: "filter",
|
|
131
|
+
conditions: serializedFilters,
|
|
185
132
|
});
|
|
186
133
|
}
|
|
187
134
|
if (qConditions && qConditions.length) {
|
|
188
135
|
newNodes.push({
|
|
189
|
-
type:
|
|
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:
|
|
160
|
+
type: "and",
|
|
212
161
|
children: [
|
|
213
|
-
{ type:
|
|
214
|
-
{
|
|
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:
|
|
221
|
-
conditions:
|
|
174
|
+
type: "filter",
|
|
175
|
+
conditions: serializedFilters,
|
|
222
176
|
};
|
|
223
177
|
}
|
|
224
178
|
else if (qConditions && qConditions.length) {
|
|
225
179
|
childNode = {
|
|
226
|
-
type:
|
|
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:
|
|
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:
|
|
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 (
|
|
235
|
+
if ("operator" in q && "conditions" in q) {
|
|
282
236
|
return {
|
|
283
|
-
type: q.operator ===
|
|
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:
|
|
292
|
-
conditions:
|
|
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: [
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
396
|
-
|
|
356
|
+
serializerOptions: serializerOptions
|
|
357
|
+
? { ...this._serializerOptions, ...serializerOptions }
|
|
358
|
+
: this._serializerOptions,
|
|
359
|
+
materialized: true,
|
|
397
360
|
}, this);
|
|
398
|
-
return QueryExecutor.execute(newQs,
|
|
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
|
|
411
|
-
|
|
373
|
+
serializerOptions: serializerOptions
|
|
374
|
+
? { ...this._serializerOptions, ...serializerOptions }
|
|
375
|
+
: this._serializerOptions,
|
|
376
|
+
materialized: true,
|
|
412
377
|
}, this);
|
|
413
|
-
return QueryExecutor.execute(newQs,
|
|
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,
|
|
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: {
|
|
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,
|
|
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(
|
|
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,
|
|
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(
|
|
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,
|
|
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: {
|
|
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,
|
|
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 ===
|
|
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
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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: {
|
|
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,
|
|
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