@statezero/core 0.2.37 → 0.2.38
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/vue/components/LayoutRenderer.js +166 -0
- package/dist/adaptors/vue/components/defaults/AlertElement.js +31 -0
- package/dist/adaptors/vue/components/defaults/DisplayElement.js +44 -0
- package/dist/adaptors/vue/components/defaults/DividerElement.js +10 -0
- package/dist/adaptors/vue/components/defaults/ErrorBlock.js +24 -0
- package/dist/adaptors/vue/components/defaults/GroupElement.js +41 -0
- package/dist/adaptors/vue/components/defaults/LabelElement.js +21 -0
- package/dist/adaptors/vue/components/defaults/TabsElement.js +38 -0
- package/package.json +6 -4
- package/dist/actions/backend1/django_app/calculate-hash.d.ts +0 -57
- package/dist/actions/backend1/django_app/calculate-hash.js +0 -80
- package/dist/actions/backend1/django_app/calculate-hash.schema.json +0 -148
- package/dist/actions/backend1/django_app/get-current-username.d.ts +0 -29
- package/dist/actions/backend1/django_app/get-current-username.js +0 -65
- package/dist/actions/backend1/django_app/get-current-username.schema.json +0 -47
- package/dist/actions/backend1/django_app/get-server-status.d.ts +0 -38
- package/dist/actions/backend1/django_app/get-server-status.js +0 -68
- package/dist/actions/backend1/django_app/get-server-status.schema.json +0 -93
- package/dist/actions/backend1/django_app/get-user-info.d.ts +0 -44
- package/dist/actions/backend1/django_app/get-user-info.js +0 -70
- package/dist/actions/backend1/django_app/get-user-info.schema.json +0 -127
- package/dist/actions/backend1/django_app/index.d.ts +0 -1
- package/dist/actions/backend1/django_app/index.js +0 -6
- package/dist/actions/backend1/django_app/process-data.d.ts +0 -51
- package/dist/actions/backend1/django_app/process-data.js +0 -78
- package/dist/actions/backend1/django_app/process-data.schema.json +0 -117
- package/dist/actions/backend1/django_app/send-notification.d.ts +0 -55
- package/dist/actions/backend1/django_app/send-notification.js +0 -81
- package/dist/actions/backend1/django_app/send-notification.schema.json +0 -175
- package/dist/actions/backend1/index.d.ts +0 -1
- package/dist/actions/backend1/index.js +0 -1
- package/dist/actions/default/django_app/calculate-hash.d.ts +0 -57
- package/dist/actions/default/django_app/calculate-hash.js +0 -80
- package/dist/actions/default/django_app/calculate-hash.schema.json +0 -148
- package/dist/actions/default/django_app/get-current-username.d.ts +0 -29
- package/dist/actions/default/django_app/get-current-username.js +0 -65
- package/dist/actions/default/django_app/get-current-username.schema.json +0 -47
- package/dist/actions/default/django_app/get-server-status.d.ts +0 -38
- package/dist/actions/default/django_app/get-server-status.js +0 -68
- package/dist/actions/default/django_app/get-server-status.schema.json +0 -93
- package/dist/actions/default/django_app/get-user-info.d.ts +0 -44
- package/dist/actions/default/django_app/get-user-info.js +0 -70
- package/dist/actions/default/django_app/get-user-info.schema.json +0 -127
- package/dist/actions/default/django_app/index.d.ts +0 -1
- package/dist/actions/default/django_app/index.js +0 -6
- package/dist/actions/default/django_app/process-data.d.ts +0 -51
- package/dist/actions/default/django_app/process-data.js +0 -78
- package/dist/actions/default/django_app/process-data.schema.json +0 -117
- package/dist/actions/default/django_app/send-notification.d.ts +0 -55
- package/dist/actions/default/django_app/send-notification.js +0 -81
- package/dist/actions/default/django_app/send-notification.schema.json +0 -175
- package/dist/actions/default/index.d.ts +0 -1
- package/dist/actions/default/index.js +0 -1
- package/dist/actions/index.d.ts +0 -1
- package/dist/actions/index.js +0 -5
- package/dist/adaptors/react/composables.d.ts +0 -1
- package/dist/adaptors/react/composables.js +0 -4
- package/dist/adaptors/react/index.d.ts +0 -1
- package/dist/adaptors/react/index.js +0 -1
- package/dist/adaptors/vue/components/LayoutRenderer.vue +0 -361
- package/dist/adaptors/vue/components/defaults/AlertElement.vue +0 -38
- package/dist/adaptors/vue/components/defaults/DisplayElement.vue +0 -57
- package/dist/adaptors/vue/components/defaults/DividerElement.vue +0 -13
- package/dist/adaptors/vue/components/defaults/ErrorBlock.vue +0 -28
- package/dist/adaptors/vue/components/defaults/GroupElement.vue +0 -53
- package/dist/adaptors/vue/components/defaults/LabelElement.vue +0 -25
- package/dist/adaptors/vue/components/defaults/TabsElement.vue +0 -54
- package/dist/adaptors/vue/components/defaults/index.d.ts +0 -7
- package/dist/adaptors/vue/components/defaults/index.js +0 -31
- package/dist/adaptors/vue/components/index.d.ts +0 -1
- package/dist/adaptors/vue/components/index.js +0 -7
- package/dist/adaptors/vue/composables.d.ts +0 -2
- package/dist/adaptors/vue/composables.js +0 -44
- package/dist/adaptors/vue/index.d.ts +0 -3
- package/dist/adaptors/vue/index.js +0 -4
- package/dist/adaptors/vue/reactivity.d.ts +0 -18
- package/dist/adaptors/vue/reactivity.js +0 -132
- package/dist/cli/commands/sync.d.ts +0 -6
- package/dist/cli/commands/sync.js +0 -30
- package/dist/cli/commands/syncActions.d.ts +0 -46
- package/dist/cli/commands/syncActions.js +0 -717
- package/dist/cli/commands/syncModels.d.ts +0 -132
- package/dist/cli/commands/syncModels.js +0 -1120
- package/dist/cli/configFileLoader.d.ts +0 -10
- package/dist/cli/configFileLoader.js +0 -85
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.js +0 -22
- package/dist/config.d.ts +0 -57
- package/dist/config.js +0 -273
- package/dist/core/eventReceivers.d.ts +0 -185
- package/dist/core/eventReceivers.js +0 -266
- package/dist/core/utils.d.ts +0 -8
- package/dist/core/utils.js +0 -62
- package/dist/errorHandler.d.ts +0 -21
- package/dist/errorHandler.js +0 -27
- package/dist/filtering/localFiltering.d.ts +0 -110
- package/dist/filtering/localFiltering.js +0 -1080
- package/dist/flavours/django/dates.d.ts +0 -34
- package/dist/flavours/django/dates.js +0 -113
- package/dist/flavours/django/errors.d.ts +0 -138
- package/dist/flavours/django/errors.js +0 -195
- package/dist/flavours/django/f.d.ts +0 -6
- package/dist/flavours/django/f.js +0 -91
- package/dist/flavours/django/files.d.ts +0 -62
- package/dist/flavours/django/files.js +0 -355
- package/dist/flavours/django/makeApiCall.d.ts +0 -36
- package/dist/flavours/django/makeApiCall.js +0 -169
- package/dist/flavours/django/manager.d.ts +0 -204
- package/dist/flavours/django/manager.js +0 -222
- package/dist/flavours/django/model.d.ts +0 -137
- package/dist/flavours/django/model.js +0 -366
- package/dist/flavours/django/operationFactory.d.ts +0 -73
- package/dist/flavours/django/operationFactory.js +0 -248
- package/dist/flavours/django/q.d.ts +0 -70
- package/dist/flavours/django/q.js +0 -43
- package/dist/flavours/django/queryExecutor.d.ts +0 -149
- package/dist/flavours/django/queryExecutor.js +0 -590
- package/dist/flavours/django/querySet.d.ts +0 -301
- package/dist/flavours/django/querySet.js +0 -736
- package/dist/flavours/django/serializers.d.ts +0 -39
- package/dist/flavours/django/serializers.js +0 -296
- package/dist/flavours/django/tempPk.d.ts +0 -31
- package/dist/flavours/django/tempPk.js +0 -92
- package/dist/flavours/django/utils.d.ts +0 -19
- package/dist/flavours/django/utils.js +0 -29
- package/dist/index.d.ts +0 -46
- package/dist/index.js +0 -48
- package/dist/models/backend1/django_app/comprehensivemodel.d.ts +0 -894
- package/dist/models/backend1/django_app/comprehensivemodel.js +0 -71
- package/dist/models/backend1/django_app/comprehensivemodel.schema.json +0 -870
- package/dist/models/backend1/django_app/custompkmodel.d.ts +0 -92
- package/dist/models/backend1/django_app/custompkmodel.js +0 -69
- package/dist/models/backend1/django_app/custompkmodel.schema.json +0 -71
- package/dist/models/backend1/django_app/dailyrate.d.ts +0 -230
- package/dist/models/backend1/django_app/dailyrate.js +0 -71
- package/dist/models/backend1/django_app/dailyrate.schema.json +0 -212
- package/dist/models/backend1/django_app/deepmodellevel1.d.ts +0 -140
- package/dist/models/backend1/django_app/deepmodellevel1.js +0 -72
- package/dist/models/backend1/django_app/deepmodellevel1.schema.json +0 -114
- package/dist/models/backend1/django_app/deepmodellevel2.d.ts +0 -118
- package/dist/models/backend1/django_app/deepmodellevel2.js +0 -71
- package/dist/models/backend1/django_app/deepmodellevel2.schema.json +0 -92
- package/dist/models/backend1/django_app/deepmodellevel3.d.ts +0 -92
- package/dist/models/backend1/django_app/deepmodellevel3.js +0 -69
- package/dist/models/backend1/django_app/deepmodellevel3.schema.json +0 -69
- package/dist/models/backend1/django_app/dummymodel.d.ts +0 -134
- package/dist/models/backend1/django_app/dummymodel.js +0 -71
- package/dist/models/backend1/django_app/dummymodel.schema.json +0 -109
- package/dist/models/backend1/django_app/dummyrelatedmodel.d.ts +0 -92
- package/dist/models/backend1/django_app/dummyrelatedmodel.js +0 -69
- package/dist/models/backend1/django_app/dummyrelatedmodel.schema.json +0 -69
- package/dist/models/backend1/django_app/filetest.d.ts +0 -140
- package/dist/models/backend1/django_app/filetest.js +0 -69
- package/dist/models/backend1/django_app/filetest.schema.json +0 -111
- package/dist/models/backend1/django_app/index.d.ts +0 -1
- package/dist/models/backend1/django_app/index.js +0 -21
- package/dist/models/backend1/django_app/m2mdepthtestlevel1.d.ts +0 -118
- package/dist/models/backend1/django_app/m2mdepthtestlevel1.js +0 -71
- package/dist/models/backend1/django_app/m2mdepthtestlevel1.schema.json +0 -94
- package/dist/models/backend1/django_app/m2mdepthtestlevel2.d.ts +0 -118
- package/dist/models/backend1/django_app/m2mdepthtestlevel2.js +0 -71
- package/dist/models/backend1/django_app/m2mdepthtestlevel2.schema.json +0 -94
- package/dist/models/backend1/django_app/m2mdepthtestlevel3.d.ts +0 -134
- package/dist/models/backend1/django_app/m2mdepthtestlevel3.js +0 -71
- package/dist/models/backend1/django_app/m2mdepthtestlevel3.schema.json +0 -112
- package/dist/models/backend1/django_app/modelwithcustompkrelation.d.ts +0 -118
- package/dist/models/backend1/django_app/modelwithcustompkrelation.js +0 -71
- package/dist/models/backend1/django_app/modelwithcustompkrelation.schema.json +0 -93
- package/dist/models/backend1/django_app/modelwithrestrictedfields.d.ts +0 -134
- package/dist/models/backend1/django_app/modelwithrestrictedfields.js +0 -71
- package/dist/models/backend1/django_app/modelwithrestrictedfields.schema.json +0 -111
- package/dist/models/backend1/django_app/namefiltercustompkmodel.d.ts +0 -92
- package/dist/models/backend1/django_app/namefiltercustompkmodel.js +0 -69
- package/dist/models/backend1/django_app/namefiltercustompkmodel.schema.json +0 -71
- package/dist/models/backend1/django_app/order.d.ts +0 -220
- package/dist/models/backend1/django_app/order.js +0 -71
- package/dist/models/backend1/django_app/order.schema.json +0 -203
- package/dist/models/backend1/django_app/orderitem.d.ts +0 -172
- package/dist/models/backend1/django_app/orderitem.js +0 -72
- package/dist/models/backend1/django_app/orderitem.schema.json +0 -149
- package/dist/models/backend1/django_app/product.d.ts +0 -254
- package/dist/models/backend1/django_app/product.js +0 -71
- package/dist/models/backend1/django_app/product.schema.json +0 -277
- package/dist/models/backend1/django_app/productcategory.d.ts +0 -92
- package/dist/models/backend1/django_app/productcategory.js +0 -69
- package/dist/models/backend1/django_app/productcategory.schema.json +0 -70
- package/dist/models/backend1/django_app/rateplan.d.ts +0 -92
- package/dist/models/backend1/django_app/rateplan.js +0 -69
- package/dist/models/backend1/django_app/rateplan.schema.json +0 -70
- package/dist/models/backend1/django_app/restrictedfieldrelatedmodel.d.ts +0 -108
- package/dist/models/backend1/django_app/restrictedfieldrelatedmodel.js +0 -69
- package/dist/models/backend1/django_app/restrictedfieldrelatedmodel.schema.json +0 -87
- package/dist/models/backend1/fileobject.d.ts +0 -4
- package/dist/models/backend1/fileobject.js +0 -9
- package/dist/models/backend1/index.d.ts +0 -2
- package/dist/models/backend1/index.js +0 -2
- package/dist/models/default/django_app/comprehensivemodel.d.ts +0 -894
- package/dist/models/default/django_app/comprehensivemodel.js +0 -71
- package/dist/models/default/django_app/comprehensivemodel.schema.json +0 -870
- package/dist/models/default/django_app/custompkmodel.d.ts +0 -92
- package/dist/models/default/django_app/custompkmodel.js +0 -69
- package/dist/models/default/django_app/custompkmodel.schema.json +0 -71
- package/dist/models/default/django_app/dailyrate.d.ts +0 -230
- package/dist/models/default/django_app/dailyrate.js +0 -71
- package/dist/models/default/django_app/dailyrate.schema.json +0 -212
- package/dist/models/default/django_app/deepmodellevel1.d.ts +0 -128
- package/dist/models/default/django_app/deepmodellevel1.js +0 -72
- package/dist/models/default/django_app/deepmodellevel1.schema.json +0 -102
- package/dist/models/default/django_app/deepmodellevel2.d.ts +0 -106
- package/dist/models/default/django_app/deepmodellevel2.js +0 -71
- package/dist/models/default/django_app/deepmodellevel2.schema.json +0 -80
- package/dist/models/default/django_app/deepmodellevel3.d.ts +0 -80
- package/dist/models/default/django_app/deepmodellevel3.js +0 -69
- package/dist/models/default/django_app/deepmodellevel3.schema.json +0 -57
- package/dist/models/default/django_app/dummymodel.d.ts +0 -122
- package/dist/models/default/django_app/dummymodel.js +0 -71
- package/dist/models/default/django_app/dummymodel.schema.json +0 -97
- package/dist/models/default/django_app/dummyrelatedmodel.d.ts +0 -80
- package/dist/models/default/django_app/dummyrelatedmodel.js +0 -69
- package/dist/models/default/django_app/dummyrelatedmodel.schema.json +0 -57
- package/dist/models/default/django_app/filetest.d.ts +0 -128
- package/dist/models/default/django_app/filetest.js +0 -69
- package/dist/models/default/django_app/filetest.schema.json +0 -99
- package/dist/models/default/django_app/index.d.ts +0 -1
- package/dist/models/default/django_app/index.js +0 -21
- package/dist/models/default/django_app/m2mdepthtestlevel1.d.ts +0 -118
- package/dist/models/default/django_app/m2mdepthtestlevel1.js +0 -71
- package/dist/models/default/django_app/m2mdepthtestlevel1.schema.json +0 -94
- package/dist/models/default/django_app/m2mdepthtestlevel2.d.ts +0 -118
- package/dist/models/default/django_app/m2mdepthtestlevel2.js +0 -71
- package/dist/models/default/django_app/m2mdepthtestlevel2.schema.json +0 -94
- package/dist/models/default/django_app/m2mdepthtestlevel3.d.ts +0 -134
- package/dist/models/default/django_app/m2mdepthtestlevel3.js +0 -71
- package/dist/models/default/django_app/m2mdepthtestlevel3.schema.json +0 -112
- package/dist/models/default/django_app/modelwithcustompkrelation.d.ts +0 -118
- package/dist/models/default/django_app/modelwithcustompkrelation.js +0 -71
- package/dist/models/default/django_app/modelwithcustompkrelation.schema.json +0 -93
- package/dist/models/default/django_app/modelwithrestrictedfields.d.ts +0 -134
- package/dist/models/default/django_app/modelwithrestrictedfields.js +0 -71
- package/dist/models/default/django_app/modelwithrestrictedfields.schema.json +0 -111
- package/dist/models/default/django_app/namefiltercustompkmodel.d.ts +0 -92
- package/dist/models/default/django_app/namefiltercustompkmodel.js +0 -69
- package/dist/models/default/django_app/namefiltercustompkmodel.schema.json +0 -71
- package/dist/models/default/django_app/order.d.ts +0 -220
- package/dist/models/default/django_app/order.js +0 -71
- package/dist/models/default/django_app/order.schema.json +0 -203
- package/dist/models/default/django_app/orderitem.d.ts +0 -172
- package/dist/models/default/django_app/orderitem.js +0 -72
- package/dist/models/default/django_app/orderitem.schema.json +0 -149
- package/dist/models/default/django_app/product.d.ts +0 -254
- package/dist/models/default/django_app/product.js +0 -71
- package/dist/models/default/django_app/product.schema.json +0 -277
- package/dist/models/default/django_app/productcategory.d.ts +0 -92
- package/dist/models/default/django_app/productcategory.js +0 -69
- package/dist/models/default/django_app/productcategory.schema.json +0 -70
- package/dist/models/default/django_app/rateplan.d.ts +0 -92
- package/dist/models/default/django_app/rateplan.js +0 -69
- package/dist/models/default/django_app/rateplan.schema.json +0 -70
- package/dist/models/default/django_app/restrictedfieldrelatedmodel.d.ts +0 -108
- package/dist/models/default/django_app/restrictedfieldrelatedmodel.js +0 -69
- package/dist/models/default/django_app/restrictedfieldrelatedmodel.schema.json +0 -87
- package/dist/models/default/fileobject.d.ts +0 -4
- package/dist/models/default/fileobject.js +0 -9
- package/dist/models/default/index.d.ts +0 -2
- package/dist/models/default/index.js +0 -2
- package/dist/models/index.d.ts +0 -1
- package/dist/models/index.js +0 -5
- package/dist/react-entry.d.ts +0 -2
- package/dist/react-entry.js +0 -2
- package/dist/reactiveAdaptor.d.ts +0 -24
- package/dist/reactiveAdaptor.js +0 -38
- package/dist/reset.d.ts +0 -15
- package/dist/reset.js +0 -97
- package/dist/setup.d.ts +0 -15
- package/dist/setup.js +0 -33
- package/dist/syncEngine/cache/cache.d.ts +0 -75
- package/dist/syncEngine/cache/cache.js +0 -355
- package/dist/syncEngine/metrics/metricOptCalcs.d.ts +0 -79
- package/dist/syncEngine/metrics/metricOptCalcs.js +0 -284
- package/dist/syncEngine/registries/metricRegistry.d.ts +0 -58
- package/dist/syncEngine/registries/metricRegistry.js +0 -171
- package/dist/syncEngine/registries/modelStoreRegistry.d.ts +0 -11
- package/dist/syncEngine/registries/modelStoreRegistry.js +0 -63
- package/dist/syncEngine/registries/querysetStoreGraph.d.ts +0 -41
- package/dist/syncEngine/registries/querysetStoreGraph.js +0 -174
- package/dist/syncEngine/registries/querysetStoreRegistry.d.ts +0 -72
- package/dist/syncEngine/registries/querysetStoreRegistry.js +0 -335
- package/dist/syncEngine/stores/metricStore.d.ts +0 -55
- package/dist/syncEngine/stores/metricStore.js +0 -222
- package/dist/syncEngine/stores/modelStore.d.ts +0 -53
- package/dist/syncEngine/stores/modelStore.js +0 -565
- package/dist/syncEngine/stores/operation.d.ts +0 -139
- package/dist/syncEngine/stores/operation.js +0 -291
- package/dist/syncEngine/stores/operationEventHandlers.d.ts +0 -8
- package/dist/syncEngine/stores/operationEventHandlers.js +0 -322
- package/dist/syncEngine/stores/querysetStore.d.ts +0 -60
- package/dist/syncEngine/stores/querysetStore.js +0 -294
- package/dist/syncEngine/stores/reactivity.d.ts +0 -3
- package/dist/syncEngine/stores/reactivity.js +0 -4
- package/dist/syncEngine/stores/utils.d.ts +0 -14
- package/dist/syncEngine/stores/utils.js +0 -32
- package/dist/syncEngine/sync.d.ts +0 -46
- package/dist/syncEngine/sync.js +0 -389
- package/dist/testing.d.ts +0 -63
- package/dist/testing.js +0 -175
- package/dist/vue-entry.d.ts +0 -15
- package/dist/vue-entry.js +0 -7
|
@@ -1,1080 +0,0 @@
|
|
|
1
|
-
import sift from 'sift';
|
|
2
|
-
const { createEqualsOperation } = sift;
|
|
3
|
-
import { configInstance } from '../config.js';
|
|
4
|
-
import { DateTime } from 'luxon';
|
|
5
|
-
import { ModelSerializer } from '../flavours/django/serializers.js';
|
|
6
|
-
/**
|
|
7
|
-
* Gets the backend timezone for a model class
|
|
8
|
-
* @param {Class} ModelClass - The model class
|
|
9
|
-
* @returns {string} The backend timezone or 'UTC' as fallback
|
|
10
|
-
*/
|
|
11
|
-
function getBackendTimezone(ModelClass) {
|
|
12
|
-
if (!ModelClass || !ModelClass.configKey) {
|
|
13
|
-
return 'UTC'; // Default fallback
|
|
14
|
-
}
|
|
15
|
-
const config = configInstance.getConfig();
|
|
16
|
-
const backendConfig = config.backendConfigs[ModelClass.configKey] || config.backendConfigs.default;
|
|
17
|
-
return backendConfig.BACKEND_TZ || 'UTC';
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Normalizes a filter value to match the live representation used in fetched data.
|
|
21
|
-
* This ensures filter values match the format of data returned by .fetch().
|
|
22
|
-
*
|
|
23
|
-
* @param {string} fieldName - The field name to check in the schema
|
|
24
|
-
* @param {any} value - The filter value to normalize
|
|
25
|
-
* @param {Class} ModelClass - The model class containing the field
|
|
26
|
-
* @returns {any} The normalized value in live format
|
|
27
|
-
*/
|
|
28
|
-
function normalizeFilterValue(fieldName, value, ModelClass) {
|
|
29
|
-
// If no schema or field name, return value as-is
|
|
30
|
-
if (!ModelClass?.schema || !fieldName)
|
|
31
|
-
return value;
|
|
32
|
-
// Use the model's serializer to convert to live format (what .fetch() returns)
|
|
33
|
-
const serializer = new ModelSerializer(ModelClass);
|
|
34
|
-
// Handle array values (for 'in' lookups)
|
|
35
|
-
if (Array.isArray(value)) {
|
|
36
|
-
return value.map(v => serializer.toLiveField(fieldName, v));
|
|
37
|
-
}
|
|
38
|
-
return serializer.toLiveField(fieldName, value);
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Process a Django-style field path with relationships to match Django ORM behavior.
|
|
42
|
-
* This handles nested relationships by traversing the model schema and properly
|
|
43
|
-
* resolving relationship fields to their primary keys.
|
|
44
|
-
*
|
|
45
|
-
* @param {string} fieldPath - The Django-style field path (e.g., 'level2__level3__name')
|
|
46
|
-
* @param {any} value - The value to filter by
|
|
47
|
-
* @param {Class} ModelClass - The root model class to start traversal from
|
|
48
|
-
* @param {Object} options - Additional options
|
|
49
|
-
* @returns {Object} An object with processed field path and operator
|
|
50
|
-
*/
|
|
51
|
-
function processFieldPath(fieldPath, value, ModelClass, options = {}) {
|
|
52
|
-
// Split the field path into parts
|
|
53
|
-
const parts = fieldPath.split('__');
|
|
54
|
-
// Check if the last part is a lookup operator
|
|
55
|
-
const knownLookups = [
|
|
56
|
-
'exact', 'iexact', 'contains', 'icontains', 'startswith',
|
|
57
|
-
'istartswith', 'endswith', 'iendswith', 'in', 'gt', 'gte',
|
|
58
|
-
'lt', 'lte', 'isnull', 'regex', 'iregex', 'year', 'month',
|
|
59
|
-
'day', 'week_day', 'hour', 'minute', 'second', 'date', 'time'
|
|
60
|
-
];
|
|
61
|
-
// Date part lookups that can be followed by comparison lookups
|
|
62
|
-
const dateParts = ['year', 'month', 'day', 'week_day', 'hour', 'minute', 'second', 'date', 'time'];
|
|
63
|
-
// Comparison lookups that can follow date parts
|
|
64
|
-
const comparisonLookups = ['gt', 'gte', 'lt', 'lte', 'exact'];
|
|
65
|
-
let lookupChain = [];
|
|
66
|
-
let fieldParts = [...parts];
|
|
67
|
-
// Check for date part + comparison operator pattern (e.g., hour__gt)
|
|
68
|
-
if (parts.length >= 3) {
|
|
69
|
-
const potentialDatePart = parts[parts.length - 2];
|
|
70
|
-
const potentialComparison = parts[parts.length - 1];
|
|
71
|
-
if (dateParts.includes(potentialDatePart) && comparisonLookups.includes(potentialComparison)) {
|
|
72
|
-
// We have a date part followed by a comparison (e.g., created_at__hour__gt)
|
|
73
|
-
lookupChain = [potentialDatePart, potentialComparison];
|
|
74
|
-
fieldParts = parts.slice(0, -2);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
// If no date part + comparison found, check for a single lookup
|
|
78
|
-
let lookup = null;
|
|
79
|
-
if (lookupChain.length === 0 && parts.length > 1 && knownLookups.includes(parts[parts.length - 1])) {
|
|
80
|
-
lookup = parts[parts.length - 1];
|
|
81
|
-
fieldParts = parts.slice(0, -1);
|
|
82
|
-
}
|
|
83
|
-
// Process the field parts to build the final path
|
|
84
|
-
let currentModel = ModelClass;
|
|
85
|
-
let processedPath = [];
|
|
86
|
-
let isRelationship = false;
|
|
87
|
-
let isM2M = false; // Track if this is a many-to-many relationship
|
|
88
|
-
let finalFieldName = null; // Track the actual field name for schema lookup
|
|
89
|
-
for (let i = 0; i < fieldParts.length; i++) {
|
|
90
|
-
let part = fieldParts[i];
|
|
91
|
-
if (part === 'pk' && currentModel)
|
|
92
|
-
part = currentModel.primaryKeyField;
|
|
93
|
-
const isLastPart = i === fieldParts.length - 1;
|
|
94
|
-
// Check if this part refers to a relationship field
|
|
95
|
-
if (currentModel && currentModel.relationshipFields &&
|
|
96
|
-
currentModel.relationshipFields instanceof Map &&
|
|
97
|
-
currentModel.relationshipFields.has(part)) {
|
|
98
|
-
// This is a relationship field
|
|
99
|
-
const relationship = currentModel.relationshipFields.get(part);
|
|
100
|
-
const relatedModel = relationship.ModelClass();
|
|
101
|
-
const relationshipType = relationship.relationshipType;
|
|
102
|
-
// If this is not the last part and it's M2M, recursively process the remaining path
|
|
103
|
-
if (!isLastPart && relationshipType === 'many-to-many') {
|
|
104
|
-
// Build the remaining path including any lookup operators
|
|
105
|
-
const remainingFieldParts = fieldParts.slice(i + 1);
|
|
106
|
-
let fullRemainingPath = remainingFieldParts.join('__');
|
|
107
|
-
if (lookupChain.length > 0) {
|
|
108
|
-
fullRemainingPath += '__' + lookupChain.join('__');
|
|
109
|
-
}
|
|
110
|
-
else if (lookup) {
|
|
111
|
-
fullRemainingPath += '__' + lookup;
|
|
112
|
-
}
|
|
113
|
-
// Recursively process the remaining path with the related model
|
|
114
|
-
const innerResult = processFieldPath(fullRemainingPath, value, relatedModel, options);
|
|
115
|
-
// Build the full field path including any FK traversal before this M2M field
|
|
116
|
-
// e.g., for owner__roles__name, processedPath=['owner'], part='roles'
|
|
117
|
-
// so fullFieldPath becomes 'owner.roles'
|
|
118
|
-
const fullFieldPath = processedPath.length > 0
|
|
119
|
-
? processedPath.join('.') + '.' + part
|
|
120
|
-
: part;
|
|
121
|
-
// Build the required path for data picking (full dot-notation path)
|
|
122
|
-
const innerRequiredPath = innerResult.requiredPath || innerResult.field;
|
|
123
|
-
const requiredPath = `${fullFieldPath}.${innerRequiredPath}`;
|
|
124
|
-
// Wrap the inner result in $elemMatch for this M2M field
|
|
125
|
-
return {
|
|
126
|
-
field: fullFieldPath,
|
|
127
|
-
operator: { $elemMatch: { [innerResult.field]: innerResult.operator } },
|
|
128
|
-
isM2M: true,
|
|
129
|
-
requiredPath // Full path for data picking
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
// Add this relationship field to the path
|
|
133
|
-
processedPath.push(part);
|
|
134
|
-
// If this is not the last part, update the current model to the related model
|
|
135
|
-
if (!isLastPart) {
|
|
136
|
-
currentModel = relatedModel;
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
// This is the last part and it's a relationship
|
|
140
|
-
isRelationship = true;
|
|
141
|
-
// For many-to-many relationships, don't append the primary key field
|
|
142
|
-
// M2M fields store an array of PKs directly, not objects
|
|
143
|
-
if (relationshipType === 'many-to-many') {
|
|
144
|
-
isM2M = true;
|
|
145
|
-
// Keep path as-is (e.g., 'comprehensive_models')
|
|
146
|
-
// Sift will handle array membership check
|
|
147
|
-
finalFieldName = part;
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
// For foreign key relationships, we need to append the primary key field
|
|
151
|
-
// to properly match Django's behavior
|
|
152
|
-
const pkField = relatedModel.primaryKeyField || 'id';
|
|
153
|
-
processedPath.push(pkField);
|
|
154
|
-
finalFieldName = pkField;
|
|
155
|
-
}
|
|
156
|
-
currentModel = relatedModel;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
else if (currentModel && currentModel.fields && currentModel.fields.includes(part)) {
|
|
160
|
-
// This is a regular field
|
|
161
|
-
processedPath.push(part);
|
|
162
|
-
finalFieldName = part;
|
|
163
|
-
// If it's the last part, we're done
|
|
164
|
-
if (isLastPart) {
|
|
165
|
-
break;
|
|
166
|
-
}
|
|
167
|
-
// Check if this is a JSON field - if so, we can traverse into it
|
|
168
|
-
const fieldSchema = currentModel.schema?.properties?.[part];
|
|
169
|
-
if (fieldSchema && fieldSchema.format === 'json') {
|
|
170
|
-
// This is a JSON field - add remaining parts as nested key access
|
|
171
|
-
// e.g., json_field__nested__active becomes json_field.nested.active
|
|
172
|
-
const remainingParts = fieldParts.slice(i + 1);
|
|
173
|
-
processedPath.push(...remainingParts);
|
|
174
|
-
// The final field name for schema lookup is still the JSON field itself
|
|
175
|
-
// (remaining parts are just keys within the JSON)
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
178
|
-
// If it's not the last part and not a JSON field,
|
|
179
|
-
// we can't continue traversal
|
|
180
|
-
throw new Error(`Field '${part}' in '${fieldPath}' is not a relationship field and cannot be traversed.`);
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
183
|
-
// Field not found in the model
|
|
184
|
-
throw new Error(`Field '${part}' in '${fieldPath}' not found in model ${currentModel.modelName}.`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
// Join the processed path parts, using dot notation for sift
|
|
188
|
-
const finalPath = processedPath.join('.');
|
|
189
|
-
// For date part lookups, don't normalize the value - keep it as-is for numeric/string comparison
|
|
190
|
-
// Date parts like 'year', 'month', etc. expect numeric values, not Date objects
|
|
191
|
-
const isDatePartLookup = dateParts.includes(lookup) || (lookupChain.length === 2 && dateParts.includes(lookupChain[0]));
|
|
192
|
-
// Normalize the value based on the field schema (skip for date part lookups)
|
|
193
|
-
const normalizedValue = isDatePartLookup ? value : normalizeFilterValue(finalFieldName, value, currentModel);
|
|
194
|
-
// Handle the date part + comparison chain if present
|
|
195
|
-
if (lookupChain.length === 2) {
|
|
196
|
-
const [datePart, comparisonOperator] = lookupChain;
|
|
197
|
-
return createDatePartComparisonOperator(finalPath, datePart, comparisonOperator, normalizedValue, isRelationship);
|
|
198
|
-
}
|
|
199
|
-
// Handle the single lookup operation if present
|
|
200
|
-
if (lookup) {
|
|
201
|
-
return createOperatorFromLookup(finalPath, lookup, normalizedValue, isRelationship, currentModel, finalFieldName, isM2M);
|
|
202
|
-
}
|
|
203
|
-
// If there's no explicit lookup and this is a relationship field,
|
|
204
|
-
// we've already appended the PK field name to the path
|
|
205
|
-
// so we just need to apply the equality operator to the value
|
|
206
|
-
if (isRelationship) {
|
|
207
|
-
// In case the user passed in a raw model as the query value
|
|
208
|
-
let raw = normalizedValue;
|
|
209
|
-
if (normalizedValue && typeof normalizedValue === 'object' && 'pk' in normalizedValue) {
|
|
210
|
-
raw = normalizedValue.pk;
|
|
211
|
-
}
|
|
212
|
-
// For M2M fields, use $elemMatch to check if any element's pk matches
|
|
213
|
-
if (isM2M) {
|
|
214
|
-
return { field: finalPath, operator: { $elemMatch: { pk: { $eq: raw } } }, isM2M: true };
|
|
215
|
-
}
|
|
216
|
-
return { field: finalPath, operator: { $eq: raw } };
|
|
217
|
-
}
|
|
218
|
-
// Default to direct equality
|
|
219
|
-
return { field: finalPath, operator: { $eq: normalizedValue } };
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* Creates a special operator for date part comparison (e.g., created_at__hour__gt: 12)
|
|
223
|
-
* @param {string} field - Processed field path
|
|
224
|
-
* @param {string} datePart - The date part to extract ('year', 'month', etc.)
|
|
225
|
-
* @param {string} comparisonOperator - The comparison operator ('gt', 'lt', etc.)
|
|
226
|
-
* @param {any} value - Value to filter by
|
|
227
|
-
* @param {boolean} isRelationship - Whether the field is a relationship
|
|
228
|
-
* @returns {Object} Object with field name and custom operator
|
|
229
|
-
*/
|
|
230
|
-
function createDatePartComparisonOperator(field, datePart, comparisonOperator, value, isRelationship) {
|
|
231
|
-
// If this is a relationship field, we handle it differently
|
|
232
|
-
if (isRelationship) {
|
|
233
|
-
console.warn(`Date part comparison on relationship fields may not work as expected: ${field}`);
|
|
234
|
-
// Fallback to direct equality as a safer option
|
|
235
|
-
return { field, operator: { $eq: value } };
|
|
236
|
-
}
|
|
237
|
-
// Create a custom operation name that combines the date part and comparison
|
|
238
|
-
// This will be handled by our custom operations in sift
|
|
239
|
-
return {
|
|
240
|
-
field,
|
|
241
|
-
operator: { [`$${datePart}_${comparisonOperator}`]: value }
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Creates a sift operator from a Django-style lookup
|
|
246
|
-
* @param {string} field - Processed field path
|
|
247
|
-
* @param {string} lookup - Django-style lookup (e.g., 'contains', 'iexact')
|
|
248
|
-
* @param {any} value - Value to filter by (already normalized)
|
|
249
|
-
* @param {boolean} isRelationship - Whether the field is a relationship
|
|
250
|
-
* @param {Class} ModelClass - The model class (unused, for future extensibility)
|
|
251
|
-
* @param {string} finalFieldName - The final field name (unused, for future extensibility)
|
|
252
|
-
* @param {boolean} isM2M - Whether the field is a many-to-many relationship
|
|
253
|
-
* @returns {Object} Object with field name and sift operator
|
|
254
|
-
*/
|
|
255
|
-
function createOperatorFromLookup(field, lookup, value, isRelationship, ModelClass, finalFieldName, isM2M = false) {
|
|
256
|
-
// Helper function to escape special characters in regex
|
|
257
|
-
function escapeRegExp(string) {
|
|
258
|
-
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
259
|
-
}
|
|
260
|
-
// Handle relationship fields differently
|
|
261
|
-
if (isRelationship) {
|
|
262
|
-
// For relationship fields with lookups, we need special handling
|
|
263
|
-
if (lookup === 'isnull') {
|
|
264
|
-
if (isM2M) {
|
|
265
|
-
// For M2M fields, isnull=True means null OR empty array
|
|
266
|
-
// isnull=False means has at least one item
|
|
267
|
-
// Use document-level $where to check the array itself (not iterate elements)
|
|
268
|
-
const m2mField = field;
|
|
269
|
-
const checkEmpty = value;
|
|
270
|
-
return {
|
|
271
|
-
field: '$where', // document-level operator
|
|
272
|
-
requiredPath: field, // need the M2M field itself for data picking
|
|
273
|
-
operator: function (doc) {
|
|
274
|
-
const fieldValue = doc[m2mField];
|
|
275
|
-
const isEmpty = fieldValue === null ||
|
|
276
|
-
(Array.isArray(fieldValue) && fieldValue.length === 0);
|
|
277
|
-
return checkEmpty ? isEmpty : !isEmpty;
|
|
278
|
-
}
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
// For FK/O2O, check for both undefined and null values
|
|
282
|
-
return {
|
|
283
|
-
field,
|
|
284
|
-
operator: value ? { $in: [null, undefined] } : { $nin: [null, undefined] }
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
else if (lookup === 'in') {
|
|
288
|
-
// For M2M, check if any element's pk is in the provided array
|
|
289
|
-
if (isM2M) {
|
|
290
|
-
return { field, operator: { $elemMatch: { pk: { $in: value } } }, isM2M: true };
|
|
291
|
-
}
|
|
292
|
-
return { field, operator: { $in: value } };
|
|
293
|
-
}
|
|
294
|
-
else {
|
|
295
|
-
// Default handling for relationship fields
|
|
296
|
-
if (isM2M) {
|
|
297
|
-
return { field, operator: { $elemMatch: { pk: { $eq: value } } }, isM2M: true };
|
|
298
|
-
}
|
|
299
|
-
return { field, operator: { $eq: value } };
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
// Handle date-related lookups
|
|
303
|
-
if (['year', 'month', 'day', 'week_day', 'hour', 'minute', 'second', 'date', 'time'].includes(lookup)) {
|
|
304
|
-
// For date part lookups, we'll use a custom operation
|
|
305
|
-
return {
|
|
306
|
-
field,
|
|
307
|
-
operator: { [`$${lookup}`]: value },
|
|
308
|
-
isDatePart: true // Add a flag to identify date part operators
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
// Regular field lookups (same as in the original code)
|
|
312
|
-
if (lookup === 'isnull') {
|
|
313
|
-
// Check for both undefined and null values
|
|
314
|
-
return {
|
|
315
|
-
field,
|
|
316
|
-
operator: value ? { $in: [null, undefined] } : { $nin: [null, undefined] }
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
else if (lookup === 'exact') {
|
|
320
|
-
return { field, operator: { $eq: value } };
|
|
321
|
-
}
|
|
322
|
-
else if (lookup === 'iexact' && typeof value === 'string') {
|
|
323
|
-
return {
|
|
324
|
-
field,
|
|
325
|
-
operator: { $regex: new RegExp(`^${escapeRegExp(value)}$`, 'i') }
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
else if (lookup === 'contains' && typeof value === 'string') {
|
|
329
|
-
return {
|
|
330
|
-
field,
|
|
331
|
-
operator: { $regex: new RegExp(escapeRegExp(value)) }
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
else if (lookup === 'icontains' && typeof value === 'string') {
|
|
335
|
-
return {
|
|
336
|
-
field,
|
|
337
|
-
operator: { $regex: new RegExp(escapeRegExp(value), 'i') }
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
else if (lookup === 'startswith' && typeof value === 'string') {
|
|
341
|
-
return {
|
|
342
|
-
field,
|
|
343
|
-
operator: { $regex: new RegExp(`^${escapeRegExp(value)}`) }
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
else if (lookup === 'istartswith' && typeof value === 'string') {
|
|
347
|
-
return {
|
|
348
|
-
field,
|
|
349
|
-
operator: { $regex: new RegExp(`^${escapeRegExp(value)}`, 'i') }
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
else if (lookup === 'endswith' && typeof value === 'string') {
|
|
353
|
-
return {
|
|
354
|
-
field,
|
|
355
|
-
operator: { $regex: new RegExp(`${escapeRegExp(value)}$`) }
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
else if (lookup === 'iendswith' && typeof value === 'string') {
|
|
359
|
-
return {
|
|
360
|
-
field,
|
|
361
|
-
operator: { $regex: new RegExp(`${escapeRegExp(value)}$`, 'i') }
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
else if (lookup === 'in') {
|
|
365
|
-
return { field, operator: { $in: value } };
|
|
366
|
-
}
|
|
367
|
-
else if (lookup === 'gt') {
|
|
368
|
-
// Exclude null/undefined to match Django (NULL comparisons return no results)
|
|
369
|
-
return { field, operator: { $nin: [null, undefined], $gt: value } };
|
|
370
|
-
}
|
|
371
|
-
else if (lookup === 'gte') {
|
|
372
|
-
return { field, operator: { $nin: [null, undefined], $gte: value } };
|
|
373
|
-
}
|
|
374
|
-
else if (lookup === 'lt') {
|
|
375
|
-
return { field, operator: { $nin: [null, undefined], $lt: value } };
|
|
376
|
-
}
|
|
377
|
-
else if (lookup === 'lte') {
|
|
378
|
-
return { field, operator: { $nin: [null, undefined], $lte: value } };
|
|
379
|
-
}
|
|
380
|
-
else {
|
|
381
|
-
// Default to direct equality if lookup not recognized
|
|
382
|
-
return { field, operator: { $eq: value } };
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
/**
|
|
386
|
-
* Creates custom operations for date parts to be used with Sift
|
|
387
|
-
* @param {string} timezone - The timezone to use for date operations
|
|
388
|
-
* @param {Class} ModelClass - The model class for serialization
|
|
389
|
-
* @returns {Object} Object containing custom operations
|
|
390
|
-
*/
|
|
391
|
-
function createDateOperations(timezone = 'UTC', ModelClass = null) {
|
|
392
|
-
const serializer = ModelClass ? new ModelSerializer(ModelClass) : null;
|
|
393
|
-
// Helper function to convert value to Date object using serializer
|
|
394
|
-
const toDateObject = (value, fieldName = null) => {
|
|
395
|
-
if (!value)
|
|
396
|
-
return null;
|
|
397
|
-
// If already a Date, return it
|
|
398
|
-
if (value instanceof Date)
|
|
399
|
-
return value;
|
|
400
|
-
// Use serializer to convert internal format to Date object
|
|
401
|
-
if (serializer && fieldName && typeof value === 'string') {
|
|
402
|
-
return serializer.toLiveField(fieldName, value);
|
|
403
|
-
}
|
|
404
|
-
// Fallback - should not happen in normal operation
|
|
405
|
-
console.warn('Date conversion without serializer context:', value);
|
|
406
|
-
return null;
|
|
407
|
-
};
|
|
408
|
-
// Helper function to extract date parts with Django-compatible behavior
|
|
409
|
-
const getDatePart = (value, partExtractor, fieldName = null) => {
|
|
410
|
-
const dateValue = toDateObject(value, fieldName);
|
|
411
|
-
if (!dateValue || !(dateValue instanceof Date) || isNaN(dateValue.getTime())) {
|
|
412
|
-
return null;
|
|
413
|
-
}
|
|
414
|
-
// Convert to timezone using Luxon
|
|
415
|
-
const luxonDate = DateTime.fromJSDate(dateValue).setZone(timezone);
|
|
416
|
-
// Extract the part using the provided function
|
|
417
|
-
return partExtractor(luxonDate);
|
|
418
|
-
};
|
|
419
|
-
// Create operations with Django-compatible extractors
|
|
420
|
-
const operations = {
|
|
421
|
-
// Year - same in both
|
|
422
|
-
$year(params, ownerQuery, options) {
|
|
423
|
-
const compareValue = typeof params === 'string' ? Number(params) : params;
|
|
424
|
-
return createEqualsOperation((value) => {
|
|
425
|
-
const year = getDatePart(value, dt => dt.year);
|
|
426
|
-
return year !== null && year === compareValue;
|
|
427
|
-
}, ownerQuery, options);
|
|
428
|
-
},
|
|
429
|
-
// Month - Luxon is 1-indexed like Django
|
|
430
|
-
$month(params, ownerQuery, options) {
|
|
431
|
-
const compareValue = typeof params === 'string' ? Number(params) : params;
|
|
432
|
-
return createEqualsOperation((value) => {
|
|
433
|
-
const month = getDatePart(value, dt => dt.month); // Already 1-indexed
|
|
434
|
-
return month !== null && month === compareValue;
|
|
435
|
-
}, ownerQuery, options);
|
|
436
|
-
},
|
|
437
|
-
// Day of month - same in both
|
|
438
|
-
$day(params, ownerQuery, options) {
|
|
439
|
-
const compareValue = typeof params === 'string' ? Number(params) : params;
|
|
440
|
-
return createEqualsOperation((value) => {
|
|
441
|
-
const day = getDatePart(value, dt => dt.day);
|
|
442
|
-
return day !== null && day === compareValue;
|
|
443
|
-
}, ownerQuery, options);
|
|
444
|
-
},
|
|
445
|
-
// Day of week - convert to Django's 1=Sunday format
|
|
446
|
-
$week_day(params, ownerQuery, options) {
|
|
447
|
-
const compareValue = typeof params === 'string' ? Number(params) : params;
|
|
448
|
-
return createEqualsOperation((value) => {
|
|
449
|
-
// Convert from Luxon (1=Monday, 7=Sunday) to Django (1=Sunday, 7=Saturday)
|
|
450
|
-
const weekDay = getDatePart(value, dt => dt.weekday === 7 ? 1 : dt.weekday + 1);
|
|
451
|
-
return weekDay !== null && weekDay === compareValue;
|
|
452
|
-
}, ownerQuery, options);
|
|
453
|
-
},
|
|
454
|
-
// Hour - same in both
|
|
455
|
-
$hour(params, ownerQuery, options) {
|
|
456
|
-
const compareValue = typeof params === 'string' ? Number(params) : params;
|
|
457
|
-
return createEqualsOperation((value) => {
|
|
458
|
-
const hour = getDatePart(value, dt => dt.hour);
|
|
459
|
-
return hour !== null && hour === compareValue;
|
|
460
|
-
}, ownerQuery, options);
|
|
461
|
-
},
|
|
462
|
-
// Minute - same in both
|
|
463
|
-
$minute(params, ownerQuery, options) {
|
|
464
|
-
const compareValue = typeof params === 'string' ? Number(params) : params;
|
|
465
|
-
return createEqualsOperation((value) => {
|
|
466
|
-
const minute = getDatePart(value, dt => dt.minute);
|
|
467
|
-
return minute !== null && minute === compareValue;
|
|
468
|
-
}, ownerQuery, options);
|
|
469
|
-
},
|
|
470
|
-
// Second - same in both
|
|
471
|
-
$second(params, ownerQuery, options) {
|
|
472
|
-
const compareValue = typeof params === 'string' ? Number(params) : params;
|
|
473
|
-
return createEqualsOperation((value) => {
|
|
474
|
-
const second = getDatePart(value, dt => dt.second);
|
|
475
|
-
return second !== null && second === compareValue;
|
|
476
|
-
}, ownerQuery, options);
|
|
477
|
-
},
|
|
478
|
-
// Date - extract date portion (ignore time)
|
|
479
|
-
$date(params, ownerQuery, options) {
|
|
480
|
-
return createEqualsOperation((value, fieldName) => {
|
|
481
|
-
const dateValue = toDateObject(value, fieldName);
|
|
482
|
-
if (!dateValue || !(dateValue instanceof Date) || isNaN(dateValue.getTime())) {
|
|
483
|
-
return false;
|
|
484
|
-
}
|
|
485
|
-
// Convert params to Date using serializer
|
|
486
|
-
// The serializer now parses date strings in the backend timezone
|
|
487
|
-
const paramDate = toDateObject(params, fieldName);
|
|
488
|
-
if (!paramDate || isNaN(paramDate.getTime()))
|
|
489
|
-
return false;
|
|
490
|
-
// Convert both to timezone and get date portions
|
|
491
|
-
const luxonDate = DateTime.fromJSDate(dateValue).setZone(timezone);
|
|
492
|
-
const luxonParam = DateTime.fromJSDate(paramDate).setZone(timezone);
|
|
493
|
-
// Compare year, month, and day
|
|
494
|
-
return luxonDate.year === luxonParam.year &&
|
|
495
|
-
luxonDate.month === luxonParam.month &&
|
|
496
|
-
luxonDate.day === luxonParam.day;
|
|
497
|
-
}, ownerQuery, options);
|
|
498
|
-
},
|
|
499
|
-
// Time - extract time portion (ignore date)
|
|
500
|
-
$time(params, ownerQuery, options) {
|
|
501
|
-
return createEqualsOperation((value, fieldName) => {
|
|
502
|
-
const dateValue = toDateObject(value, fieldName);
|
|
503
|
-
if (!dateValue || !(dateValue instanceof Date) || isNaN(dateValue.getTime())) {
|
|
504
|
-
return false;
|
|
505
|
-
}
|
|
506
|
-
// Convert to timezone
|
|
507
|
-
const luxonDate = DateTime.fromJSDate(dateValue).setZone(timezone);
|
|
508
|
-
// Parse the time string (format: "HH:MM:SS" or a Date object)
|
|
509
|
-
let paramHour, paramMinute, paramSecond;
|
|
510
|
-
if (typeof params === 'string') {
|
|
511
|
-
// Parse time string like "10:30:45"
|
|
512
|
-
const timeParts = params.split(':');
|
|
513
|
-
paramHour = parseInt(timeParts[0], 10);
|
|
514
|
-
paramMinute = parseInt(timeParts[1], 10);
|
|
515
|
-
paramSecond = parseInt(timeParts[2], 10);
|
|
516
|
-
}
|
|
517
|
-
else {
|
|
518
|
-
// Convert params to Date using serializer
|
|
519
|
-
const paramDate = toDateObject(params, fieldName);
|
|
520
|
-
if (!paramDate)
|
|
521
|
-
return false;
|
|
522
|
-
const luxonParam = DateTime.fromJSDate(paramDate).setZone(timezone);
|
|
523
|
-
paramHour = luxonParam.hour;
|
|
524
|
-
paramMinute = luxonParam.minute;
|
|
525
|
-
paramSecond = luxonParam.second;
|
|
526
|
-
}
|
|
527
|
-
// Compare hour, minute, and second
|
|
528
|
-
return luxonDate.hour === paramHour &&
|
|
529
|
-
luxonDate.minute === paramMinute &&
|
|
530
|
-
luxonDate.second === paramSecond;
|
|
531
|
-
}, ownerQuery, options);
|
|
532
|
-
}
|
|
533
|
-
};
|
|
534
|
-
// Define part extractors for each date part with Django compatibility
|
|
535
|
-
const partExtractors = {
|
|
536
|
-
'year': (dt) => dt.year,
|
|
537
|
-
'month': (dt) => dt.month, // Already 1-indexed in Luxon
|
|
538
|
-
'day': (dt) => dt.day,
|
|
539
|
-
'week_day': (dt) => dt.weekday === 7 ? 1 : dt.weekday + 1, // Convert to Django's format
|
|
540
|
-
'hour': (dt) => dt.hour,
|
|
541
|
-
'minute': (dt) => dt.minute,
|
|
542
|
-
'second': (dt) => dt.second,
|
|
543
|
-
'date': (dt) => dt.toISODate(),
|
|
544
|
-
'time': (dt) => dt.hour * 3600 + dt.minute * 60 + dt.second
|
|
545
|
-
};
|
|
546
|
-
// Generate comparison operations for each date part (year_gt, month_lt, etc.)
|
|
547
|
-
const datePartComparisons = ['gt', 'gte', 'lt', 'lte', 'exact'];
|
|
548
|
-
// For each date part, create operations with each comparison operator
|
|
549
|
-
Object.keys(partExtractors).forEach(part => {
|
|
550
|
-
const extractor = partExtractors[part];
|
|
551
|
-
datePartComparisons.forEach(op => {
|
|
552
|
-
// Create a custom operation for each combination (e.g., $year_gt, $month_lte)
|
|
553
|
-
operations[`$${part}_${op}`] = (params, ownerQuery, options) => {
|
|
554
|
-
return createEqualsOperation((value, fieldName) => {
|
|
555
|
-
const dateValue = toDateObject(value, fieldName);
|
|
556
|
-
if (!dateValue || !(dateValue instanceof Date) || isNaN(dateValue.getTime())) {
|
|
557
|
-
return false;
|
|
558
|
-
}
|
|
559
|
-
// Convert to timezone and extract part
|
|
560
|
-
const luxonDate = DateTime.fromJSDate(dateValue).setZone(timezone);
|
|
561
|
-
const partValue = extractor(luxonDate);
|
|
562
|
-
// Coerce params to number for numeric comparisons (all parts except 'date' which is a string)
|
|
563
|
-
// date part returns ISO date string like "2022-03-15", time returns number (seconds)
|
|
564
|
-
const compareValue = part === 'date' ? params : (typeof params === 'string' ? Number(params) : params);
|
|
565
|
-
// Apply the appropriate comparison
|
|
566
|
-
switch (op) {
|
|
567
|
-
case 'gt': return partValue > compareValue;
|
|
568
|
-
case 'gte': return partValue >= compareValue;
|
|
569
|
-
case 'lt': return partValue < compareValue;
|
|
570
|
-
case 'lte': return partValue <= compareValue;
|
|
571
|
-
case 'exact': return partValue === compareValue;
|
|
572
|
-
default: return false;
|
|
573
|
-
}
|
|
574
|
-
}, ownerQuery, options);
|
|
575
|
-
};
|
|
576
|
-
});
|
|
577
|
-
});
|
|
578
|
-
return operations;
|
|
579
|
-
}
|
|
580
|
-
/**
|
|
581
|
-
* Process a Django-style filter query to use with sift, including date part operations
|
|
582
|
-
* @param {Object} criteria - Sift criteria with possible date operations
|
|
583
|
-
* @param {Class} ModelClass - The model class for schema traversal
|
|
584
|
-
* @returns {Function} Sift filter function with date operations support
|
|
585
|
-
*/
|
|
586
|
-
function createFilterWithDateOperations(criteria, ModelClass) {
|
|
587
|
-
const timezone = getBackendTimezone(ModelClass);
|
|
588
|
-
return sift(criteria, {
|
|
589
|
-
operations: createDateOperations(timezone, ModelClass)
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
/**
|
|
593
|
-
* Convert Django-style filter conditions to Sift-compatible criteria
|
|
594
|
-
* @param {Object} conditions - Filter conditions
|
|
595
|
-
* @param {Class} ModelClass - The model class for schema traversal
|
|
596
|
-
* @returns {Object} Sift-compatible criteria
|
|
597
|
-
*/
|
|
598
|
-
function convertToSiftCriteria(conditions, ModelClass) {
|
|
599
|
-
const result = {};
|
|
600
|
-
const datePartFilters = new Map(); // Map to collect date part filters by field
|
|
601
|
-
const m2mConditions = new Map(); // Map to collect M2M $elemMatch conditions by field
|
|
602
|
-
for (const [key, value] of Object.entries(conditions)) {
|
|
603
|
-
try {
|
|
604
|
-
const processedResult = processFieldPath(key, value, ModelClass);
|
|
605
|
-
const { field, operator, isDatePart, isM2M } = processedResult;
|
|
606
|
-
if (isDatePart) {
|
|
607
|
-
// Handle date part operators separately
|
|
608
|
-
if (!datePartFilters.has(field)) {
|
|
609
|
-
datePartFilters.set(field, []);
|
|
610
|
-
}
|
|
611
|
-
datePartFilters.get(field).push({ [field]: operator });
|
|
612
|
-
}
|
|
613
|
-
else if (isM2M && operator.$elemMatch) {
|
|
614
|
-
// Collect M2M conditions to merge into single $elemMatch
|
|
615
|
-
if (!m2mConditions.has(field)) {
|
|
616
|
-
m2mConditions.set(field, []);
|
|
617
|
-
}
|
|
618
|
-
m2mConditions.get(field).push(operator.$elemMatch);
|
|
619
|
-
}
|
|
620
|
-
else {
|
|
621
|
-
// For regular operators, merge if we already have criteria for this field
|
|
622
|
-
if (result[field]) {
|
|
623
|
-
result[field] = { ...result[field], ...operator };
|
|
624
|
-
}
|
|
625
|
-
else {
|
|
626
|
-
result[field] = operator;
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
catch (error) {
|
|
631
|
-
throw new Error(`Failed to process field '${key}': ${error.message}`);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
// Merge M2M conditions: all conditions on same M2M field go into single $elemMatch
|
|
635
|
-
// so the SAME related object must match ALL conditions
|
|
636
|
-
for (const [field, elemMatchConditions] of m2mConditions.entries()) {
|
|
637
|
-
if (elemMatchConditions.length === 1) {
|
|
638
|
-
result[field] = { $elemMatch: elemMatchConditions[0] };
|
|
639
|
-
}
|
|
640
|
-
else {
|
|
641
|
-
// Multiple conditions - wrap in $and so same element must match all
|
|
642
|
-
result[field] = { $elemMatch: { $and: elemMatchConditions } };
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
// If we have date part filters, combine them with the result
|
|
646
|
-
if (datePartFilters.size > 0) {
|
|
647
|
-
const andConditions = [];
|
|
648
|
-
let hasRegularFilters = Object.keys(result).length > 0;
|
|
649
|
-
// Add regular filters if any
|
|
650
|
-
if (hasRegularFilters) {
|
|
651
|
-
andConditions.push(result);
|
|
652
|
-
}
|
|
653
|
-
// Add each date part filter
|
|
654
|
-
for (const [field, operators] of datePartFilters.entries()) {
|
|
655
|
-
if (operators.length === 1) {
|
|
656
|
-
// If there's only one date filter for this field
|
|
657
|
-
if (hasRegularFilters || andConditions.length > 0) {
|
|
658
|
-
andConditions.push(operators[0]);
|
|
659
|
-
}
|
|
660
|
-
else {
|
|
661
|
-
// If this is the only filter, return it directly
|
|
662
|
-
return operators[0];
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
else {
|
|
666
|
-
// Multiple date filters for the same field
|
|
667
|
-
andConditions.push(...operators);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
// If we need to combine multiple conditions
|
|
671
|
-
if (andConditions.length > 1) {
|
|
672
|
-
return { $and: andConditions };
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
return result;
|
|
676
|
-
}
|
|
677
|
-
/**
|
|
678
|
-
* Processes a Q object array to match the backend AST structure
|
|
679
|
-
* @param {Array} qConditions - Array of Q objects or conditions
|
|
680
|
-
* @param {Class} ModelClass - The model class for schema traversal
|
|
681
|
-
* @returns {Object} Sift criteria for Q conditions
|
|
682
|
-
*/
|
|
683
|
-
function processQConditions(qConditions, ModelClass) {
|
|
684
|
-
if (!qConditions || !qConditions.length)
|
|
685
|
-
return null;
|
|
686
|
-
// Convert each Q condition to sift criteria and combine with $or
|
|
687
|
-
return {
|
|
688
|
-
$or: qConditions.map(q => {
|
|
689
|
-
if ('operator' in q && 'conditions' in q) {
|
|
690
|
-
const op = q.operator === 'AND' ? '$and' : '$or';
|
|
691
|
-
return { [op]: q.conditions.map(c => convertToSiftCriteria(c, ModelClass)) };
|
|
692
|
-
}
|
|
693
|
-
else {
|
|
694
|
-
return convertToSiftCriteria(q, ModelClass);
|
|
695
|
-
}
|
|
696
|
-
})
|
|
697
|
-
};
|
|
698
|
-
}
|
|
699
|
-
/**
|
|
700
|
-
* Convert a filter node to sift criteria with proper relationship traversal
|
|
701
|
-
* @param {Object} filterNode - The filter node to convert
|
|
702
|
-
* @param {Class} ModelClass - The model class for schema traversal
|
|
703
|
-
* @returns {Object} Sift criteria object
|
|
704
|
-
*/
|
|
705
|
-
function convertFilterNodeToSiftCriteria(filterNode, ModelClass) {
|
|
706
|
-
if (!filterNode)
|
|
707
|
-
return null;
|
|
708
|
-
// For simple filter nodes with conditions
|
|
709
|
-
if (filterNode.type === 'filter') {
|
|
710
|
-
const { conditions, Q: qConditions } = filterNode;
|
|
711
|
-
let criteria = {};
|
|
712
|
-
// Add regular conditions
|
|
713
|
-
if (conditions && Object.keys(conditions).length > 0) {
|
|
714
|
-
criteria = convertToSiftCriteria(conditions, ModelClass);
|
|
715
|
-
}
|
|
716
|
-
// Add Q conditions if present
|
|
717
|
-
if (qConditions && qConditions.length > 0) {
|
|
718
|
-
const qCriteria = processQConditions(qConditions, ModelClass);
|
|
719
|
-
if (qCriteria) {
|
|
720
|
-
// Combine with AND if both types of conditions exist
|
|
721
|
-
if (Object.keys(criteria).length > 0) {
|
|
722
|
-
return { $and: [criteria, qCriteria] };
|
|
723
|
-
}
|
|
724
|
-
return qCriteria;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
return criteria;
|
|
728
|
-
}
|
|
729
|
-
// For compound AND nodes
|
|
730
|
-
if (filterNode.type === 'and' && filterNode.children) {
|
|
731
|
-
const childCriteria = filterNode.children
|
|
732
|
-
.map(child => convertFilterNodeToSiftCriteria(child, ModelClass))
|
|
733
|
-
.filter(c => c != null);
|
|
734
|
-
if (childCriteria.length === 0)
|
|
735
|
-
return null;
|
|
736
|
-
if (childCriteria.length === 1)
|
|
737
|
-
return childCriteria[0];
|
|
738
|
-
// Chained filters use $and at top level - this gives ANY/ANY semantics for M2M
|
|
739
|
-
// (each $elemMatch can be satisfied by different elements)
|
|
740
|
-
// This matches Django's chained .filter() behavior
|
|
741
|
-
return { $and: childCriteria };
|
|
742
|
-
}
|
|
743
|
-
// For compound OR nodes
|
|
744
|
-
if (filterNode.type === 'or' && filterNode.children) {
|
|
745
|
-
const childCriteria = filterNode.children
|
|
746
|
-
.map(child => convertFilterNodeToSiftCriteria(child, ModelClass))
|
|
747
|
-
.filter(c => c != null);
|
|
748
|
-
if (childCriteria.length === 0)
|
|
749
|
-
return null;
|
|
750
|
-
if (childCriteria.length === 1)
|
|
751
|
-
return childCriteria[0];
|
|
752
|
-
return { $or: childCriteria };
|
|
753
|
-
}
|
|
754
|
-
// Handle exclude nodes using $not
|
|
755
|
-
if (filterNode.type === 'exclude' && filterNode.child) {
|
|
756
|
-
const excludeCriteria = convertFilterNodeToSiftCriteria(filterNode.child, ModelClass);
|
|
757
|
-
if (!excludeCriteria)
|
|
758
|
-
return null;
|
|
759
|
-
return { $not: excludeCriteria };
|
|
760
|
-
}
|
|
761
|
-
return null;
|
|
762
|
-
}
|
|
763
|
-
/**
|
|
764
|
-
* Apply search criteria to a dataset
|
|
765
|
-
* @param {Array} data - Collection of objects to search
|
|
766
|
-
* @param {Object} searchNode - Search node from query
|
|
767
|
-
* @param {Class} ModelClass - The model class for schema traversal
|
|
768
|
-
* @returns {Array} Filtered results
|
|
769
|
-
*/
|
|
770
|
-
function applySearch(data, searchNode, ModelClass) {
|
|
771
|
-
if (!searchNode || !searchNode.searchQuery)
|
|
772
|
-
return data;
|
|
773
|
-
// Default to all string fields if searchFields not specified
|
|
774
|
-
const searchFields = searchNode.searchFields ||
|
|
775
|
-
(data[0] ? Object.keys(data[0]).filter(key => typeof data[0][key] === 'string') : []);
|
|
776
|
-
if (!searchFields.length)
|
|
777
|
-
return data;
|
|
778
|
-
// Helper function to escape RegExp characters
|
|
779
|
-
function escapeRegExp(string) {
|
|
780
|
-
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
781
|
-
}
|
|
782
|
-
// Process each search field through the model schema to handle relationships
|
|
783
|
-
const processedSearchConditions = searchFields.map(field => {
|
|
784
|
-
try {
|
|
785
|
-
// Convert field path to a dot notation path for Sift
|
|
786
|
-
const { field: processedField } = processFieldPath(field, '', ModelClass);
|
|
787
|
-
return {
|
|
788
|
-
[processedField]: {
|
|
789
|
-
$regex: new RegExp(escapeRegExp(searchNode.searchQuery), 'i')
|
|
790
|
-
}
|
|
791
|
-
};
|
|
792
|
-
}
|
|
793
|
-
catch (error) {
|
|
794
|
-
console.error(`Error processing search field '${field}':`, error.message);
|
|
795
|
-
// Fall back to using field as is
|
|
796
|
-
return {
|
|
797
|
-
[field.replace(/__/g, '.')]: {
|
|
798
|
-
$regex: new RegExp(escapeRegExp(searchNode.searchQuery), 'i')
|
|
799
|
-
}
|
|
800
|
-
};
|
|
801
|
-
}
|
|
802
|
-
});
|
|
803
|
-
return data.filter(sift({ $or: processedSearchConditions }));
|
|
804
|
-
}
|
|
805
|
-
/**
|
|
806
|
-
* Gets a nested value from an object using dot notation
|
|
807
|
-
* @param {Object} obj - Object to get value from
|
|
808
|
-
* @param {string} path - Path to value (dot notation)
|
|
809
|
-
* @returns {any} Value at path
|
|
810
|
-
*/
|
|
811
|
-
function getNestedValue(obj, path) {
|
|
812
|
-
const parts = path.split('.');
|
|
813
|
-
let current = obj;
|
|
814
|
-
for (const part of parts) {
|
|
815
|
-
if (current === null || current === undefined) {
|
|
816
|
-
return undefined;
|
|
817
|
-
}
|
|
818
|
-
current = current[part];
|
|
819
|
-
}
|
|
820
|
-
return current;
|
|
821
|
-
}
|
|
822
|
-
/**
|
|
823
|
-
* Process Django-style ordering fields
|
|
824
|
-
* @param {Array<string>} orderBy - Fields to order by (prefix with - for descending)
|
|
825
|
-
* @param {Class} ModelClass - The model class for schema traversal
|
|
826
|
-
* @returns {Array<Object>} Processed ordering specifications
|
|
827
|
-
*/
|
|
828
|
-
function processOrderBy(orderBy, ModelClass) {
|
|
829
|
-
return orderBy.map(field => {
|
|
830
|
-
const isDescending = field.startsWith('-');
|
|
831
|
-
const fieldName = isDescending ? field.substring(1) : field;
|
|
832
|
-
try {
|
|
833
|
-
// Process the field path to handle relationships
|
|
834
|
-
const { field: processedField } = processFieldPath(fieldName, '', ModelClass);
|
|
835
|
-
return {
|
|
836
|
-
field: processedField,
|
|
837
|
-
desc: isDescending
|
|
838
|
-
};
|
|
839
|
-
}
|
|
840
|
-
catch (error) {
|
|
841
|
-
console.error(`Error processing order field '${fieldName}':`, error.message);
|
|
842
|
-
// Fall back to using the field as is with dot notation
|
|
843
|
-
return {
|
|
844
|
-
field: fieldName.replace(/__/g, '.'),
|
|
845
|
-
desc: isDescending
|
|
846
|
-
};
|
|
847
|
-
}
|
|
848
|
-
});
|
|
849
|
-
}
|
|
850
|
-
/**
|
|
851
|
-
* Applies ordering to a dataset based on a list of fields
|
|
852
|
-
* @param {Array} data - Collection of objects to order
|
|
853
|
-
* @param {Array<string>} orderBy - Fields to order by (prefix with - for descending)
|
|
854
|
-
* @param {Class} ModelClass - The model class for schema traversal
|
|
855
|
-
* @returns {Array} Ordered results
|
|
856
|
-
*/
|
|
857
|
-
function applyOrderBy(data, orderBy, ModelClass) {
|
|
858
|
-
if (!orderBy || !orderBy.length)
|
|
859
|
-
return [...data];
|
|
860
|
-
const processedOrdering = processOrderBy(orderBy, ModelClass);
|
|
861
|
-
return [...data].sort((a, b) => {
|
|
862
|
-
for (const { field, desc } of processedOrdering) {
|
|
863
|
-
// Get values
|
|
864
|
-
const value1 = getNestedValue(a, field);
|
|
865
|
-
const value2 = getNestedValue(b, field);
|
|
866
|
-
if (value1 === value2)
|
|
867
|
-
continue;
|
|
868
|
-
// Handle nulls - null values should come last in ascending order
|
|
869
|
-
if (value1 === null && value2 !== null)
|
|
870
|
-
return desc ? -1 : 1;
|
|
871
|
-
if (value1 !== null && value2 === null)
|
|
872
|
-
return desc ? 1 : -1;
|
|
873
|
-
// Handle dates
|
|
874
|
-
if (value1 instanceof Date && value2 instanceof Date) {
|
|
875
|
-
return desc
|
|
876
|
-
? (value2.getTime() - value1.getTime())
|
|
877
|
-
: (value1.getTime() - value2.getTime());
|
|
878
|
-
}
|
|
879
|
-
// Default comparison
|
|
880
|
-
if (value1 > value2)
|
|
881
|
-
return desc ? -1 : 1;
|
|
882
|
-
if (value1 < value2)
|
|
883
|
-
return desc ? 1 : -1;
|
|
884
|
-
}
|
|
885
|
-
return 0;
|
|
886
|
-
});
|
|
887
|
-
}
|
|
888
|
-
/**
|
|
889
|
-
* Process an array of denormalized objects to filter & order them,
|
|
890
|
-
* then return just the matching primary-keys in order.
|
|
891
|
-
*
|
|
892
|
-
* @param {Array<Object>} data – denormalized rows, e.g. [{ id:1, name:"A", related:{ name:"B" } }, …]
|
|
893
|
-
* @param {Object} queryBuild – the result of QuerySet.build()
|
|
894
|
-
* @param {Class} ModelClass – your model class (for fieldPath resolution & date-ops)
|
|
895
|
-
* @returns {Array<*>} – the primary keys of matching rows, in order
|
|
896
|
-
*/
|
|
897
|
-
function processQuery(data, queryBuild, ModelClass) {
|
|
898
|
-
if (!Array.isArray(data) || data.length === 0) {
|
|
899
|
-
return [];
|
|
900
|
-
}
|
|
901
|
-
if (!ModelClass) {
|
|
902
|
-
throw new Error('ModelClass is required for proper relationship traversal');
|
|
903
|
-
}
|
|
904
|
-
const pkField = ModelClass.primaryKeyField;
|
|
905
|
-
let results = [...data]; // assume already denormalized objects
|
|
906
|
-
// 1) Apply filtering
|
|
907
|
-
if (queryBuild.filter) {
|
|
908
|
-
const criteria = convertFilterNodeToSiftCriteria(queryBuild.filter, ModelClass);
|
|
909
|
-
if (criteria && Object.keys(criteria).length) {
|
|
910
|
-
results = results.filter(createFilterWithDateOperations(criteria, ModelClass));
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
// 2) Apply search
|
|
914
|
-
if (queryBuild.search && queryBuild.search.searchQuery) {
|
|
915
|
-
results = applySearch(results, queryBuild.search, ModelClass);
|
|
916
|
-
}
|
|
917
|
-
// 3) Apply ordering
|
|
918
|
-
if (Array.isArray(queryBuild.orderBy) && queryBuild.orderBy.length) {
|
|
919
|
-
results = applyOrderBy(results, queryBuild.orderBy, ModelClass);
|
|
920
|
-
}
|
|
921
|
-
// 4) Return only the primary-keys, in the filtered & ordered sequence
|
|
922
|
-
return results.map(item => item[pkField]);
|
|
923
|
-
}
|
|
924
|
-
/**
|
|
925
|
-
* Inspect a QuerySet.build() and collect every field path
|
|
926
|
-
* you’ll need to fetch before running sift.
|
|
927
|
-
*
|
|
928
|
-
* @param {Object} queryBuild – result of QuerySet.build()
|
|
929
|
-
* @param {Class} ModelClass – the root model class
|
|
930
|
-
* @returns {string[]} Array of dot-notation paths, e.g. ['author.id','createdAt.year']
|
|
931
|
-
*/
|
|
932
|
-
export function getRequiredFields(queryBuild, ModelClass) {
|
|
933
|
-
const paths = new Set();
|
|
934
|
-
const pkField = ModelClass.primaryKeyField;
|
|
935
|
-
// Always include the PK so we can re-map results
|
|
936
|
-
paths.add(pkField);
|
|
937
|
-
// Try to turn a Django-style key into the final dot path
|
|
938
|
-
function addPath(rawKey) {
|
|
939
|
-
try {
|
|
940
|
-
// We pass `null` as the value, since we only care about .field
|
|
941
|
-
const { field, isM2M, requiredPath } = processFieldPath(rawKey, null, ModelClass);
|
|
942
|
-
// Use requiredPath if available (for M2M traversal), otherwise use field
|
|
943
|
-
// For M2M fields at the end of a path (no requiredPath), we need the pk field
|
|
944
|
-
let finalPath;
|
|
945
|
-
if (requiredPath) {
|
|
946
|
-
finalPath = requiredPath;
|
|
947
|
-
}
|
|
948
|
-
else if (isM2M) {
|
|
949
|
-
finalPath = `${field}.pk`;
|
|
950
|
-
}
|
|
951
|
-
else {
|
|
952
|
-
finalPath = field;
|
|
953
|
-
}
|
|
954
|
-
paths.add(finalPath);
|
|
955
|
-
}
|
|
956
|
-
catch (err) {
|
|
957
|
-
// if a key doesn't map, warn and skip it
|
|
958
|
-
console.warn(`getRequiredFields: couldn't process "${rawKey}": ${err.message}`);
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
// Recursively walk your filter AST
|
|
962
|
-
function walkFilter(node) {
|
|
963
|
-
if (!node)
|
|
964
|
-
return;
|
|
965
|
-
switch (node.type) {
|
|
966
|
-
case 'filter':
|
|
967
|
-
// simple conditions
|
|
968
|
-
Object.keys(node.conditions || {}).forEach(addPath);
|
|
969
|
-
// any Q-objects
|
|
970
|
-
(node.Q || []).forEach(q => {
|
|
971
|
-
Object.keys(q.conditions || {}).forEach(addPath);
|
|
972
|
-
});
|
|
973
|
-
break;
|
|
974
|
-
case 'and':
|
|
975
|
-
case 'or':
|
|
976
|
-
(node.children || []).forEach(walkFilter);
|
|
977
|
-
break;
|
|
978
|
-
case 'exclude':
|
|
979
|
-
walkFilter(node.child);
|
|
980
|
-
break;
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
// collect from filter
|
|
984
|
-
if (queryBuild.filter) {
|
|
985
|
-
walkFilter(queryBuild.filter);
|
|
986
|
-
}
|
|
987
|
-
// collect from search
|
|
988
|
-
if (queryBuild.search && Array.isArray(queryBuild.search.searchFields)) {
|
|
989
|
-
queryBuild.search.searchFields.forEach(addPath);
|
|
990
|
-
}
|
|
991
|
-
// collect from orderBy
|
|
992
|
-
if (Array.isArray(queryBuild.orderBy)) {
|
|
993
|
-
queryBuild.orderBy.forEach(field => {
|
|
994
|
-
// strip leading '-' for desc sorts
|
|
995
|
-
addPath(field.replace(/^-/, ''));
|
|
996
|
-
});
|
|
997
|
-
}
|
|
998
|
-
return Array.from(paths);
|
|
999
|
-
}
|
|
1000
|
-
/**
|
|
1001
|
-
* Pick out only the required fields from a (possibly nested) model object.
|
|
1002
|
-
*
|
|
1003
|
-
* @param {string[]} requiredPaths – e.g. ['id','related.name','related.age']
|
|
1004
|
-
* @param {Object} instance – e.g. { id: 3, related: { name: 'bob', age: 12, foo: 'bar' } }
|
|
1005
|
-
* @returns {Object} – e.g. { id: 3, related: { name: 'bob', age: 12 } }
|
|
1006
|
-
*/
|
|
1007
|
-
/**
|
|
1008
|
-
* Recursively sets a value in a result object following a path.
|
|
1009
|
-
* Handles arrays (M2M fields) by mapping over each element.
|
|
1010
|
-
*/
|
|
1011
|
-
function setNestedValueRecursive(result, source, pathParts) {
|
|
1012
|
-
if (source == null || pathParts.length === 0) {
|
|
1013
|
-
return;
|
|
1014
|
-
}
|
|
1015
|
-
const [current, ...rest] = pathParts;
|
|
1016
|
-
const sourceValue = source[current];
|
|
1017
|
-
if (sourceValue === undefined) {
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
if (rest.length === 0) {
|
|
1021
|
-
// Last part - set the value directly, keeping M2M as full objects
|
|
1022
|
-
result[current] = sourceValue;
|
|
1023
|
-
}
|
|
1024
|
-
else if (Array.isArray(sourceValue)) {
|
|
1025
|
-
// M2M array with nested path - recursively extract from each element
|
|
1026
|
-
if (!(current in result)) {
|
|
1027
|
-
result[current] = [];
|
|
1028
|
-
}
|
|
1029
|
-
sourceValue.forEach((item, idx) => {
|
|
1030
|
-
if (result[current][idx] === undefined) {
|
|
1031
|
-
result[current][idx] = {};
|
|
1032
|
-
}
|
|
1033
|
-
setNestedValueRecursive(result[current][idx], item, rest);
|
|
1034
|
-
});
|
|
1035
|
-
}
|
|
1036
|
-
else if (typeof sourceValue === 'object') {
|
|
1037
|
-
// Regular nested object (FK)
|
|
1038
|
-
if (!(current in result)) {
|
|
1039
|
-
result[current] = {};
|
|
1040
|
-
}
|
|
1041
|
-
setNestedValueRecursive(result[current], sourceValue, rest);
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
export function pickRequiredFields(requiredPaths, instance) {
|
|
1045
|
-
const result = {};
|
|
1046
|
-
requiredPaths.forEach(path => {
|
|
1047
|
-
const parts = path.split('.');
|
|
1048
|
-
setNestedValueRecursive(result, instance, parts);
|
|
1049
|
-
});
|
|
1050
|
-
return result;
|
|
1051
|
-
}
|
|
1052
|
-
/**
|
|
1053
|
-
* Filter and order a collection of data objects according to a QuerySet's AST.
|
|
1054
|
-
* This combines getRequiredFields, pickRequiredFields, and processQuery in one function.
|
|
1055
|
-
*
|
|
1056
|
-
* @param {Array<Object>} data - Collection of objects to filter and order
|
|
1057
|
-
* @param {Object} ast - Abstract Syntax Tree from QuerySet.build()
|
|
1058
|
-
* @param {Class} ModelClass - The model class for schema traversal
|
|
1059
|
-
* @param {boolean} [returnFullObjects=false] - If true, returns full objects instead of just primary keys
|
|
1060
|
-
* @returns {Array} Filtered and ordered results (primary keys or full objects based on returnFullObjects)
|
|
1061
|
-
*/
|
|
1062
|
-
export function filter(data, ast, ModelClass, returnFullObjects = false) {
|
|
1063
|
-
if (!Array.isArray(data) || data.length === 0) {
|
|
1064
|
-
return [];
|
|
1065
|
-
}
|
|
1066
|
-
if (!ModelClass) {
|
|
1067
|
-
throw new Error('ModelClass is required for proper relationship traversal');
|
|
1068
|
-
}
|
|
1069
|
-
const pkField = ModelClass.primaryKeyField || 'id';
|
|
1070
|
-
let requiredFields = getRequiredFields(ast, ModelClass);
|
|
1071
|
-
let denormalizedData = data.map(item => pickRequiredFields(requiredFields, item));
|
|
1072
|
-
const resultKeys = processQuery(denormalizedData, ast, ModelClass);
|
|
1073
|
-
if (returnFullObjects) {
|
|
1074
|
-
const dataMap = new Map(data.map(item => [item[pkField], item]));
|
|
1075
|
-
return resultKeys.map(key => dataMap.get(key));
|
|
1076
|
-
}
|
|
1077
|
-
return resultKeys;
|
|
1078
|
-
}
|
|
1079
|
-
// Export the utility functions for testing and usage
|
|
1080
|
-
export { processFieldPath, convertToSiftCriteria, processQConditions, convertFilterNodeToSiftCriteria, applySearch, applyOrderBy, processQuery, createDateOperations, createFilterWithDateOperations, createDatePartComparisonOperator, getBackendTimezone };
|