@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.
Files changed (89) hide show
  1. package/dist/adaptors/react/composables.d.ts +1 -0
  2. package/dist/adaptors/react/composables.js +4 -0
  3. package/dist/adaptors/react/index.d.ts +1 -0
  4. package/dist/adaptors/react/index.js +1 -0
  5. package/dist/adaptors/vue/composables.d.ts +2 -0
  6. package/dist/adaptors/vue/composables.js +36 -0
  7. package/dist/adaptors/vue/index.d.ts +2 -0
  8. package/dist/adaptors/vue/index.js +2 -0
  9. package/dist/adaptors/vue/reactivity.d.ts +18 -0
  10. package/dist/adaptors/vue/reactivity.js +125 -0
  11. package/dist/cli/commands/syncModels.d.ts +132 -0
  12. package/dist/cli/commands/syncModels.js +1040 -0
  13. package/dist/cli/configFileLoader.d.ts +10 -0
  14. package/dist/cli/configFileLoader.js +85 -0
  15. package/dist/cli/index.d.ts +2 -0
  16. package/dist/cli/index.js +14 -0
  17. package/dist/config.d.ts +52 -0
  18. package/dist/config.js +242 -0
  19. package/dist/core/eventReceivers.d.ts +179 -0
  20. package/dist/core/eventReceivers.js +210 -0
  21. package/dist/core/utils.d.ts +8 -0
  22. package/dist/core/utils.js +62 -0
  23. package/dist/filtering/localFiltering.d.ts +116 -0
  24. package/dist/filtering/localFiltering.js +834 -0
  25. package/dist/flavours/django/dates.d.ts +33 -0
  26. package/dist/flavours/django/dates.js +99 -0
  27. package/dist/flavours/django/errors.d.ts +138 -0
  28. package/dist/flavours/django/errors.js +187 -0
  29. package/dist/flavours/django/f.d.ts +6 -0
  30. package/dist/flavours/django/f.js +91 -0
  31. package/dist/flavours/django/files.d.ts +76 -0
  32. package/dist/flavours/django/files.js +338 -0
  33. package/dist/flavours/django/makeApiCall.d.ts +20 -0
  34. package/dist/flavours/django/makeApiCall.js +169 -0
  35. package/dist/flavours/django/manager.d.ts +197 -0
  36. package/dist/flavours/django/manager.js +222 -0
  37. package/dist/flavours/django/model.d.ts +112 -0
  38. package/dist/flavours/django/model.js +253 -0
  39. package/dist/flavours/django/operationFactory.d.ts +65 -0
  40. package/dist/flavours/django/operationFactory.js +216 -0
  41. package/dist/flavours/django/q.d.ts +70 -0
  42. package/dist/flavours/django/q.js +43 -0
  43. package/dist/flavours/django/queryExecutor.d.ts +131 -0
  44. package/dist/flavours/django/queryExecutor.js +468 -0
  45. package/dist/flavours/django/querySet.d.ts +412 -0
  46. package/dist/flavours/django/querySet.js +601 -0
  47. package/dist/flavours/django/tempPk.d.ts +19 -0
  48. package/dist/flavours/django/tempPk.js +48 -0
  49. package/dist/flavours/django/utils.d.ts +19 -0
  50. package/dist/flavours/django/utils.js +29 -0
  51. package/dist/index.d.ts +38 -0
  52. package/dist/index.js +38 -0
  53. package/dist/react-entry.d.ts +2 -0
  54. package/dist/react-entry.js +2 -0
  55. package/dist/reactiveAdaptor.d.ts +24 -0
  56. package/dist/reactiveAdaptor.js +38 -0
  57. package/dist/setup.d.ts +15 -0
  58. package/dist/setup.js +22 -0
  59. package/dist/syncEngine/cache/cache.d.ts +75 -0
  60. package/dist/syncEngine/cache/cache.js +341 -0
  61. package/dist/syncEngine/metrics/metricOptCalcs.d.ts +79 -0
  62. package/dist/syncEngine/metrics/metricOptCalcs.js +284 -0
  63. package/dist/syncEngine/registries/metricRegistry.d.ts +53 -0
  64. package/dist/syncEngine/registries/metricRegistry.js +162 -0
  65. package/dist/syncEngine/registries/modelStoreRegistry.d.ts +11 -0
  66. package/dist/syncEngine/registries/modelStoreRegistry.js +56 -0
  67. package/dist/syncEngine/registries/querysetStoreRegistry.d.ts +55 -0
  68. package/dist/syncEngine/registries/querysetStoreRegistry.js +244 -0
  69. package/dist/syncEngine/stores/metricStore.d.ts +55 -0
  70. package/dist/syncEngine/stores/metricStore.js +222 -0
  71. package/dist/syncEngine/stores/modelStore.d.ts +40 -0
  72. package/dist/syncEngine/stores/modelStore.js +405 -0
  73. package/dist/syncEngine/stores/operation.d.ts +99 -0
  74. package/dist/syncEngine/stores/operation.js +224 -0
  75. package/dist/syncEngine/stores/operationEventHandlers.d.ts +8 -0
  76. package/dist/syncEngine/stores/operationEventHandlers.js +239 -0
  77. package/dist/syncEngine/stores/querysetStore.d.ts +32 -0
  78. package/dist/syncEngine/stores/querysetStore.js +200 -0
  79. package/dist/syncEngine/stores/reactivity.d.ts +3 -0
  80. package/dist/syncEngine/stores/reactivity.js +4 -0
  81. package/dist/syncEngine/stores/utils.d.ts +14 -0
  82. package/dist/syncEngine/stores/utils.js +32 -0
  83. package/dist/syncEngine/sync.d.ts +32 -0
  84. package/dist/syncEngine/sync.js +169 -0
  85. package/dist/vue-entry.d.ts +6 -0
  86. package/dist/vue-entry.js +2 -0
  87. package/license.md +116 -0
  88. package/package.json +123 -0
  89. 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
+ }