@statezero/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adaptors/react/composables.d.ts +1 -0
- package/dist/adaptors/react/composables.js +4 -0
- package/dist/adaptors/react/index.d.ts +1 -0
- package/dist/adaptors/react/index.js +1 -0
- package/dist/adaptors/vue/composables.d.ts +2 -0
- package/dist/adaptors/vue/composables.js +36 -0
- package/dist/adaptors/vue/index.d.ts +2 -0
- package/dist/adaptors/vue/index.js +2 -0
- package/dist/adaptors/vue/reactivity.d.ts +18 -0
- package/dist/adaptors/vue/reactivity.js +125 -0
- package/dist/cli/commands/syncModels.d.ts +132 -0
- package/dist/cli/commands/syncModels.js +1040 -0
- package/dist/cli/configFileLoader.d.ts +10 -0
- package/dist/cli/configFileLoader.js +85 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +14 -0
- package/dist/config.d.ts +52 -0
- package/dist/config.js +242 -0
- package/dist/core/eventReceivers.d.ts +179 -0
- package/dist/core/eventReceivers.js +210 -0
- package/dist/core/utils.d.ts +8 -0
- package/dist/core/utils.js +62 -0
- package/dist/filtering/localFiltering.d.ts +116 -0
- package/dist/filtering/localFiltering.js +834 -0
- package/dist/flavours/django/dates.d.ts +33 -0
- package/dist/flavours/django/dates.js +99 -0
- package/dist/flavours/django/errors.d.ts +138 -0
- package/dist/flavours/django/errors.js +187 -0
- package/dist/flavours/django/f.d.ts +6 -0
- package/dist/flavours/django/f.js +91 -0
- package/dist/flavours/django/files.d.ts +76 -0
- package/dist/flavours/django/files.js +338 -0
- package/dist/flavours/django/makeApiCall.d.ts +20 -0
- package/dist/flavours/django/makeApiCall.js +169 -0
- package/dist/flavours/django/manager.d.ts +197 -0
- package/dist/flavours/django/manager.js +222 -0
- package/dist/flavours/django/model.d.ts +112 -0
- package/dist/flavours/django/model.js +253 -0
- package/dist/flavours/django/operationFactory.d.ts +65 -0
- package/dist/flavours/django/operationFactory.js +216 -0
- package/dist/flavours/django/q.d.ts +70 -0
- package/dist/flavours/django/q.js +43 -0
- package/dist/flavours/django/queryExecutor.d.ts +131 -0
- package/dist/flavours/django/queryExecutor.js +468 -0
- package/dist/flavours/django/querySet.d.ts +412 -0
- package/dist/flavours/django/querySet.js +601 -0
- package/dist/flavours/django/tempPk.d.ts +19 -0
- package/dist/flavours/django/tempPk.js +48 -0
- package/dist/flavours/django/utils.d.ts +19 -0
- package/dist/flavours/django/utils.js +29 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +38 -0
- package/dist/react-entry.d.ts +2 -0
- package/dist/react-entry.js +2 -0
- package/dist/reactiveAdaptor.d.ts +24 -0
- package/dist/reactiveAdaptor.js +38 -0
- package/dist/setup.d.ts +15 -0
- package/dist/setup.js +22 -0
- package/dist/syncEngine/cache/cache.d.ts +75 -0
- package/dist/syncEngine/cache/cache.js +341 -0
- package/dist/syncEngine/metrics/metricOptCalcs.d.ts +79 -0
- package/dist/syncEngine/metrics/metricOptCalcs.js +284 -0
- package/dist/syncEngine/registries/metricRegistry.d.ts +53 -0
- package/dist/syncEngine/registries/metricRegistry.js +162 -0
- package/dist/syncEngine/registries/modelStoreRegistry.d.ts +11 -0
- package/dist/syncEngine/registries/modelStoreRegistry.js +56 -0
- package/dist/syncEngine/registries/querysetStoreRegistry.d.ts +55 -0
- package/dist/syncEngine/registries/querysetStoreRegistry.js +244 -0
- package/dist/syncEngine/stores/metricStore.d.ts +55 -0
- package/dist/syncEngine/stores/metricStore.js +222 -0
- package/dist/syncEngine/stores/modelStore.d.ts +40 -0
- package/dist/syncEngine/stores/modelStore.js +405 -0
- package/dist/syncEngine/stores/operation.d.ts +99 -0
- package/dist/syncEngine/stores/operation.js +224 -0
- package/dist/syncEngine/stores/operationEventHandlers.d.ts +8 -0
- package/dist/syncEngine/stores/operationEventHandlers.js +239 -0
- package/dist/syncEngine/stores/querysetStore.d.ts +32 -0
- package/dist/syncEngine/stores/querysetStore.js +200 -0
- package/dist/syncEngine/stores/reactivity.d.ts +3 -0
- package/dist/syncEngine/stores/reactivity.js +4 -0
- package/dist/syncEngine/stores/utils.d.ts +14 -0
- package/dist/syncEngine/stores/utils.js +32 -0
- package/dist/syncEngine/sync.d.ts +32 -0
- package/dist/syncEngine/sync.js +169 -0
- package/dist/vue-entry.d.ts +6 -0
- package/dist/vue-entry.js +2 -0
- package/license.md +116 -0
- package/package.json +123 -0
- package/readme.md +222 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { Manager } from './manager.js';
|
|
2
|
+
import { ValidationError } from './errors.js';
|
|
3
|
+
import { modelStoreRegistry } from '../../syncEngine/registries/modelStoreRegistry.js';
|
|
4
|
+
import { isNil } from 'lodash-es';
|
|
5
|
+
import { QueryExecutor } from './queryExecutor';
|
|
6
|
+
import { wrapReactiveModel } from '../../reactiveAdaptor.js';
|
|
7
|
+
import { DateParsingHelpers } from './dates.js';
|
|
8
|
+
/**
|
|
9
|
+
* A constructor for a Model.
|
|
10
|
+
*
|
|
11
|
+
* @typedef {Function} ModelConstructor
|
|
12
|
+
* @param {any} data - Data to initialize the model.
|
|
13
|
+
* @returns {Model}
|
|
14
|
+
*
|
|
15
|
+
* @property {Manager} objects - The model's manager.
|
|
16
|
+
* @property {string} configKey - The configuration key.
|
|
17
|
+
* @property {string} modelName - The model name.
|
|
18
|
+
* @property {string} primaryKeyField - The primary key field (default 'id').
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Base Model class with integrated API implementation.
|
|
22
|
+
*
|
|
23
|
+
* @abstract
|
|
24
|
+
*/
|
|
25
|
+
export class Model {
|
|
26
|
+
constructor(data = {}) {
|
|
27
|
+
// Initialize internal data store
|
|
28
|
+
this._data = data;
|
|
29
|
+
this._pk = data[this.constructor.primaryKeyField] || undefined;
|
|
30
|
+
this.__version = 0;
|
|
31
|
+
return wrapReactiveModel(this);
|
|
32
|
+
}
|
|
33
|
+
touch() {
|
|
34
|
+
this.__version++;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Returns the primary key of the model instance.
|
|
38
|
+
*
|
|
39
|
+
* @returns {number|undefined} The primary key.
|
|
40
|
+
*/
|
|
41
|
+
get pk() {
|
|
42
|
+
return this._pk;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Sets the primary key of the model instance.
|
|
46
|
+
*
|
|
47
|
+
* @param {number|undefined} value - The new primary key value.
|
|
48
|
+
*/
|
|
49
|
+
set pk(value) {
|
|
50
|
+
this._pk = value;
|
|
51
|
+
this.touch();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Instantiate from pk using queryset scoped singletons
|
|
55
|
+
*/
|
|
56
|
+
static fromPk(pk, querySet) {
|
|
57
|
+
let qsId = querySet ? querySet.__uuid : '';
|
|
58
|
+
let key = `${qsId}__${this.configKey}__${this.modelName}__${pk}`;
|
|
59
|
+
if (!this.instanceCache.has(key)) {
|
|
60
|
+
const instance = new this();
|
|
61
|
+
instance.pk = pk;
|
|
62
|
+
this.instanceCache.set(key, instance);
|
|
63
|
+
}
|
|
64
|
+
return this.instanceCache.get(key);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Gets a field value from the internal data store
|
|
68
|
+
*
|
|
69
|
+
* @param {string} field - The field name
|
|
70
|
+
* @returns {any} The field value
|
|
71
|
+
*/
|
|
72
|
+
getField(field) {
|
|
73
|
+
// Access the reactive __version property to establish dependency for vue integration
|
|
74
|
+
const trackVersion = this.__version;
|
|
75
|
+
const ModelClass = this.constructor;
|
|
76
|
+
if (ModelClass.primaryKeyField === field)
|
|
77
|
+
return this._pk;
|
|
78
|
+
// check local overrides
|
|
79
|
+
let value = this._data[field];
|
|
80
|
+
// if its not been overridden, get it from the store
|
|
81
|
+
if (isNil(value) && !isNil(this._pk)) {
|
|
82
|
+
let storedValue = modelStoreRegistry.getEntity(ModelClass, this._pk);
|
|
83
|
+
if (storedValue)
|
|
84
|
+
value = storedValue[field]; // if stops null -> undefined
|
|
85
|
+
}
|
|
86
|
+
// relationship fields need special handling
|
|
87
|
+
if (ModelClass.relationshipFields.has(field) && value) {
|
|
88
|
+
// fetch the stored value
|
|
89
|
+
let fieldInfo = ModelClass.relationshipFields.get(field);
|
|
90
|
+
// footgun - fieldInfo.ModelClass() calls the arrow function that lazily gets the model class
|
|
91
|
+
let relPkField = fieldInfo.ModelClass().primaryKeyField;
|
|
92
|
+
switch (fieldInfo.relationshipType) {
|
|
93
|
+
case 'many-to-many':
|
|
94
|
+
// value is an array
|
|
95
|
+
if (!Array.isArray(value) && value)
|
|
96
|
+
throw new Error(`Data corruption: m2m field for ${ModelClass.modelName} stored as ${value}`);
|
|
97
|
+
// set each pk to the full model object for that pk
|
|
98
|
+
value = value.map(pk => fieldInfo.ModelClass().fromPk(pk));
|
|
99
|
+
break;
|
|
100
|
+
case 'one-to-one':
|
|
101
|
+
case 'foreign-key':
|
|
102
|
+
// footgun - fieldInfo.ModelClass() calls the arrow function that lazily gets the model class
|
|
103
|
+
if (!isNil(value))
|
|
104
|
+
value = fieldInfo.ModelClass().fromPk(value);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Sets a field value in the internal data store
|
|
112
|
+
*
|
|
113
|
+
* @param {string} field - The field name
|
|
114
|
+
* @param {any} value - The field value to set
|
|
115
|
+
*/
|
|
116
|
+
setField(field, value) {
|
|
117
|
+
const ModelClass = this.constructor;
|
|
118
|
+
if (ModelClass.primaryKeyField === field) {
|
|
119
|
+
this._pk = value;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
this._data[field] = value;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Validates that the provided data object only contains keys
|
|
127
|
+
* defined in the model's allowed fields. Supports nested fields
|
|
128
|
+
* using double underscore notation (e.g., author__name).
|
|
129
|
+
*
|
|
130
|
+
* @param {Object} data - The object to validate.
|
|
131
|
+
* @throws {ValidationError} If an unknown key is found.
|
|
132
|
+
*/
|
|
133
|
+
static validateFields(data) {
|
|
134
|
+
if (isNil(data))
|
|
135
|
+
return;
|
|
136
|
+
const allowedFields = this.fields;
|
|
137
|
+
for (const key of Object.keys(data)) {
|
|
138
|
+
if (key === 'repr' || key === 'type')
|
|
139
|
+
continue;
|
|
140
|
+
// Handle nested fields by splitting on double underscore
|
|
141
|
+
// and taking just the base field name
|
|
142
|
+
const baseField = key.split('__')[0];
|
|
143
|
+
if (!allowedFields.includes(baseField)) {
|
|
144
|
+
let errorMsg = `Invalid field: ${baseField}. Allowed fields are: ${allowedFields.join(', ')}`;
|
|
145
|
+
console.error(errorMsg);
|
|
146
|
+
throw new ValidationError(errorMsg);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Serializes a field value for API transmission
|
|
152
|
+
*
|
|
153
|
+
* @param {string} field - The field name
|
|
154
|
+
* @returns {any} The serialized field value
|
|
155
|
+
*/
|
|
156
|
+
serializeField(field) {
|
|
157
|
+
const ModelClass = this.constructor;
|
|
158
|
+
if (ModelClass.primaryKeyField === field)
|
|
159
|
+
return this._pk;
|
|
160
|
+
// check local overrides
|
|
161
|
+
let value = this._data[field];
|
|
162
|
+
// if it's not been overridden, get it from the store
|
|
163
|
+
if (isNil(value) && !isNil(this._pk)) {
|
|
164
|
+
let storedValue = modelStoreRegistry.getEntity(ModelClass, this._pk);
|
|
165
|
+
if (storedValue)
|
|
166
|
+
value = storedValue[field];
|
|
167
|
+
}
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Serializes the model instance.
|
|
172
|
+
*
|
|
173
|
+
* By default, it returns all enumerable own properties.
|
|
174
|
+
* Subclasses should override this to return specific keys.
|
|
175
|
+
*
|
|
176
|
+
* @returns {Object} The serialized model data.
|
|
177
|
+
*/
|
|
178
|
+
serialize() {
|
|
179
|
+
const serialized = {};
|
|
180
|
+
const ModelClass = this.constructor;
|
|
181
|
+
// Include all fields defined in the model
|
|
182
|
+
for (const field of ModelClass.fields) {
|
|
183
|
+
serialized[field] = this.serializeField(field);
|
|
184
|
+
}
|
|
185
|
+
return serialized;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Saves the model instance by either creating a new record or updating an existing one.
|
|
189
|
+
*
|
|
190
|
+
* @returns {Promise<Model>} A promise that resolves to the updated model instance.
|
|
191
|
+
*/
|
|
192
|
+
async save() {
|
|
193
|
+
const ModelClass = this.constructor;
|
|
194
|
+
const pkField = ModelClass.primaryKeyField;
|
|
195
|
+
const querySet = !this.pk ? ModelClass.objects.newQuerySet() : ModelClass.objects.filter({ [pkField]: this.pk });
|
|
196
|
+
const data = this.serialize();
|
|
197
|
+
let instance;
|
|
198
|
+
if (!this.pk) {
|
|
199
|
+
// Create new instance
|
|
200
|
+
instance = await QueryExecutor.execute(querySet, 'create', { data });
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
// Update existing instance
|
|
204
|
+
instance = await QueryExecutor.execute(querySet, 'update_instance', { data });
|
|
205
|
+
}
|
|
206
|
+
this._pk = instance.pk;
|
|
207
|
+
this._data = {};
|
|
208
|
+
return this;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Deletes the instance from the database.
|
|
212
|
+
*
|
|
213
|
+
* Returns a tuple with the number of objects deleted and an object mapping
|
|
214
|
+
* model names to the number of objects deleted, matching Django's behavior.
|
|
215
|
+
*
|
|
216
|
+
* @returns {Promise<[number, Object]>} A promise that resolves to the deletion result.
|
|
217
|
+
* @throws {Error} If the instance has not been saved (no primary key).
|
|
218
|
+
*/
|
|
219
|
+
async delete() {
|
|
220
|
+
if (!this.pk) {
|
|
221
|
+
throw new Error('Cannot delete unsaved instance');
|
|
222
|
+
}
|
|
223
|
+
const ModelClass = this.constructor;
|
|
224
|
+
const pkField = ModelClass.primaryKeyField;
|
|
225
|
+
const querySet = ModelClass.objects.filter({ [pkField]: this.pk });
|
|
226
|
+
// Pass the instance data with primary key as the args
|
|
227
|
+
const args = { [pkField]: this.pk };
|
|
228
|
+
const result = await QueryExecutor.execute(querySet, 'delete_instance', args);
|
|
229
|
+
// result -> [deletedCount, { [modelName]: deletedCount }];
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Refreshes the model instance with data from the database.
|
|
234
|
+
*
|
|
235
|
+
* @returns {Promise<void>} A promise that resolves when the instance has been refreshed.
|
|
236
|
+
* @throws {Error} If the instance has not been saved (no primary key).
|
|
237
|
+
*/
|
|
238
|
+
async refreshFromDb() {
|
|
239
|
+
if (!this.pk) {
|
|
240
|
+
throw new Error('Cannot refresh unsaved instance');
|
|
241
|
+
}
|
|
242
|
+
const ModelClass = this.constructor;
|
|
243
|
+
const fresh = await ModelClass.objects.get({ [ModelClass.primaryKeyField]: this.pk });
|
|
244
|
+
// clear the current data and fresh data will flow
|
|
245
|
+
this._data = {};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Creates a new Model instance.
|
|
250
|
+
*
|
|
251
|
+
* @param {any} [data={}] - The data for initialization.
|
|
252
|
+
*/
|
|
253
|
+
Model.instanceCache = new Map();
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory for creating Operation instances with consistent behavior
|
|
3
|
+
* across QueryExecutor and hotpath event handling
|
|
4
|
+
*/
|
|
5
|
+
export class OperationFactory {
|
|
6
|
+
/**
|
|
7
|
+
* Create a CREATE operation
|
|
8
|
+
* @param {QuerySet} queryset - The queryset context
|
|
9
|
+
* @param {Object} data - The data for the new instance
|
|
10
|
+
* @param {string} [operationId] - Optional operation ID (for hotpath events)
|
|
11
|
+
* @returns {Operation} The created operation
|
|
12
|
+
*/
|
|
13
|
+
static createCreateOperation(queryset: QuerySet, data?: Object, operationId?: string): Operation;
|
|
14
|
+
/**
|
|
15
|
+
* Create an UPDATE operation with optimistic instance updates
|
|
16
|
+
* @param {QuerySet} queryset - The queryset context
|
|
17
|
+
* @param {Object} data - The update data
|
|
18
|
+
* @param {Object} filter - Optional filter for the update
|
|
19
|
+
* @param {string} [operationId] - Optional operation ID (for hotpath events)
|
|
20
|
+
* @returns {Operation} The created operation
|
|
21
|
+
*/
|
|
22
|
+
static createUpdateOperation(queryset: QuerySet, data?: Object, filter?: Object, operationId?: string): Operation;
|
|
23
|
+
/**
|
|
24
|
+
* Create a DELETE operation
|
|
25
|
+
* @param {QuerySet} queryset - The queryset context
|
|
26
|
+
* @param {string} [operationId] - Optional operation ID (for hotpath events)
|
|
27
|
+
* @returns {Operation} The created operation
|
|
28
|
+
*/
|
|
29
|
+
static createDeleteOperation(queryset: QuerySet, operationId?: string): Operation;
|
|
30
|
+
/**
|
|
31
|
+
* Create an UPDATE_INSTANCE operation
|
|
32
|
+
* @param {QuerySet} queryset - The queryset context
|
|
33
|
+
* @param {Object} data - The update data
|
|
34
|
+
* @param {string} [operationId] - Optional operation ID (for hotpath events)
|
|
35
|
+
* @returns {Operation} The created operation
|
|
36
|
+
*/
|
|
37
|
+
static createUpdateInstanceOperation(queryset: QuerySet, data?: Object, operationId?: string): Operation;
|
|
38
|
+
/**
|
|
39
|
+
* Create a DELETE_INSTANCE operation
|
|
40
|
+
* @param {QuerySet} queryset - The queryset context
|
|
41
|
+
* @param {Object} instanceData - The instance to delete (object with PK)
|
|
42
|
+
* @param {string} [operationId] - Optional operation ID (for hotpath events)
|
|
43
|
+
* @returns {Operation} The created operation
|
|
44
|
+
*/
|
|
45
|
+
static createDeleteInstanceOperation(queryset: QuerySet, instanceData: Object, operationId?: string): Operation;
|
|
46
|
+
/**
|
|
47
|
+
* Create a GET_OR_CREATE operation with local filtering logic
|
|
48
|
+
* @param {QuerySet} queryset - The queryset context
|
|
49
|
+
* @param {Object} lookup - The lookup criteria
|
|
50
|
+
* @param {Object} defaults - The default values for creation
|
|
51
|
+
* @param {string} [operationId] - Optional operation ID (for hotpath events)
|
|
52
|
+
* @returns {Operation} The created operation
|
|
53
|
+
*/
|
|
54
|
+
static createGetOrCreateOperation(queryset: QuerySet, lookup?: Object, defaults?: Object, operationId?: string): Operation;
|
|
55
|
+
/**
|
|
56
|
+
* Create an UPDATE_OR_CREATE operation with local filtering logic
|
|
57
|
+
* @param {QuerySet} queryset - The queryset context
|
|
58
|
+
* @param {Object} lookup - The lookup criteria
|
|
59
|
+
* @param {Object} defaults - The default values for creation/update
|
|
60
|
+
* @param {string} [operationId] - Optional operation ID (for hotpath events)
|
|
61
|
+
* @returns {Operation} The created operation
|
|
62
|
+
*/
|
|
63
|
+
static createUpdateOrCreateOperation(queryset: QuerySet, lookup?: Object, defaults?: Object, operationId?: string): Operation;
|
|
64
|
+
}
|
|
65
|
+
import { Operation } from '../../syncEngine/stores/operation';
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { Operation, Type, Status } from '../../syncEngine/stores/operation';
|
|
2
|
+
import { v7 as uuid7 } from 'uuid';
|
|
3
|
+
import { createTempPk } from './tempPk.js';
|
|
4
|
+
import { getRequiredFields, pickRequiredFields, processQuery } from '../../filtering/localFiltering.js';
|
|
5
|
+
import { evaluateExpression } from './f.js';
|
|
6
|
+
import { modelStoreRegistry } from '../../syncEngine/registries/modelStoreRegistry.js';
|
|
7
|
+
import { querysetStoreRegistry } from '../../syncEngine/registries/querysetStoreRegistry.js';
|
|
8
|
+
import { isNil } from 'lodash-es';
|
|
9
|
+
/**
|
|
10
|
+
* Factory for creating Operation instances with consistent behavior
|
|
11
|
+
* across QueryExecutor and hotpath event handling
|
|
12
|
+
*/
|
|
13
|
+
export class OperationFactory {
|
|
14
|
+
/**
|
|
15
|
+
* Create a CREATE operation
|
|
16
|
+
* @param {QuerySet} queryset - The queryset context
|
|
17
|
+
* @param {Object} data - The data for the new instance
|
|
18
|
+
* @param {string} [operationId] - Optional operation ID (for hotpath events)
|
|
19
|
+
* @returns {Operation} The created operation
|
|
20
|
+
*/
|
|
21
|
+
static createCreateOperation(queryset, data = {}, operationId = null) {
|
|
22
|
+
const ModelClass = queryset.ModelClass;
|
|
23
|
+
const primaryKeyField = ModelClass.primaryKeyField;
|
|
24
|
+
const opId = operationId || `${uuid7()}`;
|
|
25
|
+
const tempPk = createTempPk(opId);
|
|
26
|
+
return new Operation({
|
|
27
|
+
operationId: opId,
|
|
28
|
+
type: Type.CREATE,
|
|
29
|
+
instances: [{ ...data, [primaryKeyField]: tempPk }],
|
|
30
|
+
queryset: queryset,
|
|
31
|
+
args: { data },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Create an UPDATE operation with optimistic instance updates
|
|
36
|
+
* @param {QuerySet} queryset - The queryset context
|
|
37
|
+
* @param {Object} data - The update data
|
|
38
|
+
* @param {Object} filter - Optional filter for the update
|
|
39
|
+
* @param {string} [operationId] - Optional operation ID (for hotpath events)
|
|
40
|
+
* @returns {Operation} The created operation
|
|
41
|
+
*/
|
|
42
|
+
static createUpdateOperation(queryset, data = {}, filter = null, operationId = null) {
|
|
43
|
+
const ModelClass = queryset.ModelClass;
|
|
44
|
+
const primaryKeyField = ModelClass.primaryKeyField;
|
|
45
|
+
const store = querysetStoreRegistry.getStore(queryset);
|
|
46
|
+
const querysetPks = store.render();
|
|
47
|
+
const opId = operationId || `${uuid7()}`;
|
|
48
|
+
// Create optimistic instances with F expression evaluation
|
|
49
|
+
const optimisticInstances = querysetPks.map(pk => {
|
|
50
|
+
const instance = modelStoreRegistry.getEntity(ModelClass, pk);
|
|
51
|
+
const updatedInstance = { ...instance };
|
|
52
|
+
updatedInstance[primaryKeyField] = pk;
|
|
53
|
+
for (const [key, value] of Object.entries(data)) {
|
|
54
|
+
if (value && typeof value === 'object' && value.__f_expr) {
|
|
55
|
+
const evaluatedValue = evaluateExpression(value, instance);
|
|
56
|
+
if (evaluatedValue !== null) {
|
|
57
|
+
updatedInstance[key] = evaluatedValue;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
updatedInstance[key] = instance[key];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
updatedInstance[key] = value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return updatedInstance;
|
|
68
|
+
});
|
|
69
|
+
return new Operation({
|
|
70
|
+
operationId: opId,
|
|
71
|
+
type: Type.UPDATE,
|
|
72
|
+
instances: optimisticInstances,
|
|
73
|
+
queryset: queryset,
|
|
74
|
+
args: { filter, data },
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Create a DELETE operation
|
|
79
|
+
* @param {QuerySet} queryset - The queryset context
|
|
80
|
+
* @param {string} [operationId] - Optional operation ID (for hotpath events)
|
|
81
|
+
* @returns {Operation} The created operation
|
|
82
|
+
*/
|
|
83
|
+
static createDeleteOperation(queryset, operationId = null) {
|
|
84
|
+
const ModelClass = queryset.ModelClass;
|
|
85
|
+
const primaryKeyField = ModelClass.primaryKeyField;
|
|
86
|
+
const store = querysetStoreRegistry.getStore(queryset);
|
|
87
|
+
const querysetPks = store.render();
|
|
88
|
+
const opId = operationId || `${uuid7()}`;
|
|
89
|
+
const instances = querysetPks.map((pk) => ({ [primaryKeyField]: pk }));
|
|
90
|
+
return new Operation({
|
|
91
|
+
operationId: opId,
|
|
92
|
+
type: Type.DELETE,
|
|
93
|
+
instances: instances,
|
|
94
|
+
queryset: queryset,
|
|
95
|
+
args: {},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Create an UPDATE_INSTANCE operation
|
|
100
|
+
* @param {QuerySet} queryset - The queryset context
|
|
101
|
+
* @param {Object} data - The update data
|
|
102
|
+
* @param {string} [operationId] - Optional operation ID (for hotpath events)
|
|
103
|
+
* @returns {Operation} The created operation
|
|
104
|
+
*/
|
|
105
|
+
static createUpdateInstanceOperation(queryset, data = {}, operationId = null) {
|
|
106
|
+
const ModelClass = queryset.ModelClass;
|
|
107
|
+
const primaryKeyField = ModelClass.primaryKeyField;
|
|
108
|
+
const store = querysetStoreRegistry.getStore(queryset);
|
|
109
|
+
const querysetPks = store.render();
|
|
110
|
+
const opId = operationId || `${uuid7()}`;
|
|
111
|
+
const instances = querysetPks.map(pk => ({ ...data, [primaryKeyField]: pk }));
|
|
112
|
+
return new Operation({
|
|
113
|
+
operationId: opId,
|
|
114
|
+
type: Type.UPDATE_INSTANCE,
|
|
115
|
+
instances: instances,
|
|
116
|
+
queryset: queryset,
|
|
117
|
+
args: { data },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Create a DELETE_INSTANCE operation
|
|
122
|
+
* @param {QuerySet} queryset - The queryset context
|
|
123
|
+
* @param {Object} instanceData - The instance to delete (object with PK)
|
|
124
|
+
* @param {string} [operationId] - Optional operation ID (for hotpath events)
|
|
125
|
+
* @returns {Operation} The created operation
|
|
126
|
+
*/
|
|
127
|
+
static createDeleteInstanceOperation(queryset, instanceData, operationId = null) {
|
|
128
|
+
const opId = operationId || `${uuid7()}`;
|
|
129
|
+
return new Operation({
|
|
130
|
+
operationId: opId,
|
|
131
|
+
type: Type.DELETE_INSTANCE,
|
|
132
|
+
instances: [instanceData],
|
|
133
|
+
queryset: queryset,
|
|
134
|
+
args: instanceData,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Create a GET_OR_CREATE operation with local filtering logic
|
|
139
|
+
* @param {QuerySet} queryset - The queryset context
|
|
140
|
+
* @param {Object} lookup - The lookup criteria
|
|
141
|
+
* @param {Object} defaults - The default values for creation
|
|
142
|
+
* @param {string} [operationId] - Optional operation ID (for hotpath events)
|
|
143
|
+
* @returns {Operation} The created operation
|
|
144
|
+
*/
|
|
145
|
+
static createGetOrCreateOperation(queryset, lookup = {}, defaults = {}, operationId = null) {
|
|
146
|
+
const ModelClass = queryset.ModelClass;
|
|
147
|
+
const primaryKeyField = ModelClass.primaryKeyField;
|
|
148
|
+
const opId = operationId || `${uuid7()}`;
|
|
149
|
+
// Get all current instances from the store for local filtering
|
|
150
|
+
const modelStore = modelStoreRegistry.getStore(ModelClass);
|
|
151
|
+
const allInstances = modelStore.render();
|
|
152
|
+
// Create a queryset filter for the lookup criteria
|
|
153
|
+
const lookupFilter = { ...lookup };
|
|
154
|
+
const lookupQuerySet = new queryset.constructor(ModelClass).filter(lookupFilter);
|
|
155
|
+
const lookupQuery = lookupQuerySet.build();
|
|
156
|
+
// Use local filtering to find matching instances
|
|
157
|
+
const requiredPaths = getRequiredFields(lookupQuery, ModelClass);
|
|
158
|
+
const prunedData = allInstances.map(inst => pickRequiredFields(requiredPaths, ModelClass.fromPk(inst[primaryKeyField], queryset)));
|
|
159
|
+
const matchingPks = processQuery(prunedData, lookupQuery, ModelClass);
|
|
160
|
+
// Find the corresponding instances
|
|
161
|
+
const matchingInstances = allInstances.filter(inst => matchingPks.includes(inst[primaryKeyField]));
|
|
162
|
+
const isCreatingNew = matchingInstances.length === 0;
|
|
163
|
+
const effectiveType = isCreatingNew ? Type.CREATE : Type.UPDATE;
|
|
164
|
+
// Create the instance data
|
|
165
|
+
const instanceData = isCreatingNew
|
|
166
|
+
? { ...lookup, ...defaults, [primaryKeyField]: opId }
|
|
167
|
+
: matchingInstances[0];
|
|
168
|
+
return new Operation({
|
|
169
|
+
operationId: opId,
|
|
170
|
+
type: effectiveType,
|
|
171
|
+
instances: [instanceData],
|
|
172
|
+
queryset: queryset,
|
|
173
|
+
args: { lookup, defaults },
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Create an UPDATE_OR_CREATE operation with local filtering logic
|
|
178
|
+
* @param {QuerySet} queryset - The queryset context
|
|
179
|
+
* @param {Object} lookup - The lookup criteria
|
|
180
|
+
* @param {Object} defaults - The default values for creation/update
|
|
181
|
+
* @param {string} [operationId] - Optional operation ID (for hotpath events)
|
|
182
|
+
* @returns {Operation} The created operation
|
|
183
|
+
*/
|
|
184
|
+
static createUpdateOrCreateOperation(queryset, lookup = {}, defaults = {}, operationId = null) {
|
|
185
|
+
const ModelClass = queryset.ModelClass;
|
|
186
|
+
const primaryKeyField = ModelClass.primaryKeyField;
|
|
187
|
+
const opId = operationId || `${uuid7()}`;
|
|
188
|
+
// Get all current instances from the store for local filtering
|
|
189
|
+
const modelStore = modelStoreRegistry.getStore(ModelClass);
|
|
190
|
+
const allInstances = modelStore.render();
|
|
191
|
+
// Create a queryset filter for the lookup criteria
|
|
192
|
+
const lookupFilter = { ...lookup };
|
|
193
|
+
const lookupQuerySet = new queryset.constructor(ModelClass).filter(lookupFilter);
|
|
194
|
+
const lookupQuery = lookupQuerySet.build();
|
|
195
|
+
// Use local filtering to find matching instances
|
|
196
|
+
const requiredPaths = getRequiredFields(lookupQuery, ModelClass);
|
|
197
|
+
const prunedData = allInstances.map(inst => pickRequiredFields(requiredPaths, ModelClass.fromPk(inst[primaryKeyField], queryset)));
|
|
198
|
+
const matchingPks = processQuery(prunedData, lookupQuery, ModelClass);
|
|
199
|
+
// Find the corresponding instances
|
|
200
|
+
const matchingInstances = allInstances.filter(inst => matchingPks.includes(inst[primaryKeyField]));
|
|
201
|
+
const isCreatingNew = matchingInstances.length === 0;
|
|
202
|
+
const isUpdating = !isCreatingNew;
|
|
203
|
+
const effectiveType = isCreatingNew ? Type.CREATE : Type.UPDATE;
|
|
204
|
+
// Create the instance data
|
|
205
|
+
const instanceData = isCreatingNew
|
|
206
|
+
? { ...lookup, ...defaults, [primaryKeyField]: opId }
|
|
207
|
+
: { ...matchingInstances[0], ...defaults };
|
|
208
|
+
return new Operation({
|
|
209
|
+
operationId: opId,
|
|
210
|
+
type: effectiveType,
|
|
211
|
+
instances: [instanceData],
|
|
212
|
+
queryset: queryset,
|
|
213
|
+
args: { lookup, defaults },
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal type definitions for field lookups.
|
|
3
|
+
*
|
|
4
|
+
* @template T
|
|
5
|
+
* @typedef {Object.<string, any>} FieldLookup
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Minimal type definitions for object lookups.
|
|
9
|
+
*
|
|
10
|
+
* @template T
|
|
11
|
+
* @typedef {Object.<string, any>} ObjectLookup
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Django-specific Q helper type.
|
|
15
|
+
*
|
|
16
|
+
* A QCondition is either a partial object of type T or a combination
|
|
17
|
+
* of partial field and object lookups.
|
|
18
|
+
*
|
|
19
|
+
* @template T
|
|
20
|
+
* @typedef {Partial<T> | (Partial<FieldLookup<T>> & Partial<ObjectLookup<T>>)} QCondition
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Django-specific Q helper type representing a logical grouping of conditions.
|
|
24
|
+
*
|
|
25
|
+
* @template T
|
|
26
|
+
* @typedef {Object} QObject
|
|
27
|
+
* @property {'AND'|'OR'} operator - The logical operator.
|
|
28
|
+
* @property {Array<QCondition<T>|QObject<T>>} conditions - An array of conditions or nested Q objects.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Creates a Q object for combining conditions.
|
|
32
|
+
*
|
|
33
|
+
* @template T
|
|
34
|
+
* @param {'AND'|'OR'} operator - The operator to combine conditions.
|
|
35
|
+
* @param {...(QCondition<T>|QObject<T>)} conditions - The conditions to combine.
|
|
36
|
+
* @returns {QObject<T>} The combined Q object.
|
|
37
|
+
*/
|
|
38
|
+
export function Q<T>(operator: "AND" | "OR", ...conditions: (QCondition<T> | QObject<T>)[]): QObject<T>;
|
|
39
|
+
/**
|
|
40
|
+
* Minimal type definitions for field lookups.
|
|
41
|
+
*/
|
|
42
|
+
export type FieldLookup<T> = {
|
|
43
|
+
[x: string]: any;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Minimal type definitions for object lookups.
|
|
47
|
+
*/
|
|
48
|
+
export type ObjectLookup<T> = {
|
|
49
|
+
[x: string]: any;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Django-specific Q helper type.
|
|
53
|
+
*
|
|
54
|
+
* A QCondition is either a partial object of type T or a combination
|
|
55
|
+
* of partial field and object lookups.
|
|
56
|
+
*/
|
|
57
|
+
export type QCondition<T> = Partial<T> | (Partial<FieldLookup<T>> & Partial<ObjectLookup<T>>);
|
|
58
|
+
/**
|
|
59
|
+
* Django-specific Q helper type representing a logical grouping of conditions.
|
|
60
|
+
*/
|
|
61
|
+
export type QObject<T> = {
|
|
62
|
+
/**
|
|
63
|
+
* - The logical operator.
|
|
64
|
+
*/
|
|
65
|
+
operator: "AND" | "OR";
|
|
66
|
+
/**
|
|
67
|
+
* - An array of conditions or nested Q objects.
|
|
68
|
+
*/
|
|
69
|
+
conditions: Array<QCondition<T> | QObject<T>>;
|
|
70
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal type definitions for field lookups.
|
|
3
|
+
*
|
|
4
|
+
* @template T
|
|
5
|
+
* @typedef {Object.<string, any>} FieldLookup
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Minimal type definitions for object lookups.
|
|
9
|
+
*
|
|
10
|
+
* @template T
|
|
11
|
+
* @typedef {Object.<string, any>} ObjectLookup
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Django-specific Q helper type.
|
|
15
|
+
*
|
|
16
|
+
* A QCondition is either a partial object of type T or a combination
|
|
17
|
+
* of partial field and object lookups.
|
|
18
|
+
*
|
|
19
|
+
* @template T
|
|
20
|
+
* @typedef {Partial<T> | (Partial<FieldLookup<T>> & Partial<ObjectLookup<T>>)} QCondition
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Django-specific Q helper type representing a logical grouping of conditions.
|
|
24
|
+
*
|
|
25
|
+
* @template T
|
|
26
|
+
* @typedef {Object} QObject
|
|
27
|
+
* @property {'AND'|'OR'} operator - The logical operator.
|
|
28
|
+
* @property {Array<QCondition<T>|QObject<T>>} conditions - An array of conditions or nested Q objects.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Creates a Q object for combining conditions.
|
|
32
|
+
*
|
|
33
|
+
* @template T
|
|
34
|
+
* @param {'AND'|'OR'} operator - The operator to combine conditions.
|
|
35
|
+
* @param {...(QCondition<T>|QObject<T>)} conditions - The conditions to combine.
|
|
36
|
+
* @returns {QObject<T>} The combined Q object.
|
|
37
|
+
*/
|
|
38
|
+
export function Q(operator, ...conditions) {
|
|
39
|
+
return {
|
|
40
|
+
operator,
|
|
41
|
+
conditions,
|
|
42
|
+
};
|
|
43
|
+
}
|