@statezero/core 0.1.91 → 0.1.93
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.
|
@@ -88,9 +88,10 @@ export function processQuery(data: Array<Object>, queryBuild: Object, ModelClass
|
|
|
88
88
|
/**
|
|
89
89
|
* Creates custom operations for date parts to be used with Sift
|
|
90
90
|
* @param {string} timezone - The timezone to use for date operations
|
|
91
|
+
* @param {Class} ModelClass - The model class for serialization
|
|
91
92
|
* @returns {Object} Object containing custom operations
|
|
92
93
|
*/
|
|
93
|
-
export function createDateOperations(timezone?: string): Object;
|
|
94
|
+
export function createDateOperations(timezone?: string, ModelClass?: Class): Object;
|
|
94
95
|
/**
|
|
95
96
|
* Process a Django-style filter query to use with sift, including date part operations
|
|
96
97
|
* @param {Object} criteria - Sift criteria with possible date operations
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import sift, { createEqualsOperation } from 'sift';
|
|
2
2
|
import { configInstance } from '../config.js';
|
|
3
3
|
import { DateTime } from 'luxon';
|
|
4
|
+
import { ModelSerializer } from '../flavours/django/serializers.js';
|
|
4
5
|
/**
|
|
5
6
|
* Gets the backend timezone for a model class
|
|
6
7
|
* @param {Class} ModelClass - The model class
|
|
@@ -15,43 +16,25 @@ function getBackendTimezone(ModelClass) {
|
|
|
15
16
|
return backendConfig.BACKEND_TZ || 'UTC';
|
|
16
17
|
}
|
|
17
18
|
/**
|
|
18
|
-
* Normalizes a filter value
|
|
19
|
-
* This ensures
|
|
20
|
-
* converted to Date objects for comparison.
|
|
19
|
+
* Normalizes a filter value to match the live representation used in fetched data.
|
|
20
|
+
* This ensures filter values match the format of data returned by .fetch().
|
|
21
21
|
*
|
|
22
22
|
* @param {string} fieldName - The field name to check in the schema
|
|
23
23
|
* @param {any} value - The filter value to normalize
|
|
24
24
|
* @param {Class} ModelClass - The model class containing the field
|
|
25
|
-
* @returns {any} The normalized value
|
|
25
|
+
* @returns {any} The normalized value in live format
|
|
26
26
|
*/
|
|
27
27
|
function normalizeFilterValue(fieldName, value, ModelClass) {
|
|
28
28
|
// If no schema or field name, return value as-is
|
|
29
29
|
if (!ModelClass?.schema || !fieldName)
|
|
30
30
|
return value;
|
|
31
|
-
//
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// Handle date/datetime fields - convert strings to Date objects
|
|
37
|
-
if (type === 'string' && (format === 'date' || format === 'date-time')) {
|
|
38
|
-
if (typeof value === 'string') {
|
|
39
|
-
const parsed = new Date(value);
|
|
40
|
-
// Only return the parsed date if it's valid
|
|
41
|
-
return isNaN(parsed.getTime()) ? value : parsed;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
// Handle 'in' lookups - normalize array values
|
|
45
|
-
if (Array.isArray(value) && type === 'string' && (format === 'date' || format === 'date-time')) {
|
|
46
|
-
return value.map(v => {
|
|
47
|
-
if (typeof v === 'string') {
|
|
48
|
-
const parsed = new Date(v);
|
|
49
|
-
return isNaN(parsed.getTime()) ? v : parsed;
|
|
50
|
-
}
|
|
51
|
-
return v;
|
|
52
|
-
});
|
|
31
|
+
// Use the model's serializer to convert to live format (what .fetch() returns)
|
|
32
|
+
const serializer = new ModelSerializer(ModelClass);
|
|
33
|
+
// Handle array values (for 'in' lookups)
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
return value.map(v => serializer.toLiveField(fieldName, v));
|
|
53
36
|
}
|
|
54
|
-
return value;
|
|
37
|
+
return serializer.toLiveField(fieldName, value);
|
|
55
38
|
}
|
|
56
39
|
/**
|
|
57
40
|
* Process a Django-style field path with relationships to match Django ORM behavior.
|
|
@@ -319,16 +302,32 @@ function createOperatorFromLookup(field, lookup, value, isRelationship, ModelCla
|
|
|
319
302
|
/**
|
|
320
303
|
* Creates custom operations for date parts to be used with Sift
|
|
321
304
|
* @param {string} timezone - The timezone to use for date operations
|
|
305
|
+
* @param {Class} ModelClass - The model class for serialization
|
|
322
306
|
* @returns {Object} Object containing custom operations
|
|
323
307
|
*/
|
|
324
|
-
function createDateOperations(timezone = 'UTC') {
|
|
325
|
-
|
|
326
|
-
|
|
308
|
+
function createDateOperations(timezone = 'UTC', ModelClass = null) {
|
|
309
|
+
const serializer = ModelClass ? new ModelSerializer(ModelClass) : null;
|
|
310
|
+
// Helper function to convert value to Date object using serializer
|
|
311
|
+
const toDateObject = (value, fieldName = null) => {
|
|
327
312
|
if (!value)
|
|
328
313
|
return null;
|
|
329
|
-
|
|
330
|
-
if (
|
|
314
|
+
// If already a Date, return it
|
|
315
|
+
if (value instanceof Date)
|
|
316
|
+
return value;
|
|
317
|
+
// Use serializer to convert internal format to Date object
|
|
318
|
+
if (serializer && fieldName && typeof value === 'string') {
|
|
319
|
+
return serializer.toLiveField(fieldName, value);
|
|
320
|
+
}
|
|
321
|
+
// Fallback - should not happen in normal operation
|
|
322
|
+
console.warn('Date conversion without serializer context:', value);
|
|
323
|
+
return null;
|
|
324
|
+
};
|
|
325
|
+
// Helper function to extract date parts with Django-compatible behavior
|
|
326
|
+
const getDatePart = (value, partExtractor, fieldName = null) => {
|
|
327
|
+
const dateValue = toDateObject(value, fieldName);
|
|
328
|
+
if (!dateValue || !(dateValue instanceof Date) || isNaN(dateValue.getTime())) {
|
|
331
329
|
return null;
|
|
330
|
+
}
|
|
332
331
|
// Convert to timezone using Luxon
|
|
333
332
|
const luxonDate = DateTime.fromJSDate(dateValue).setZone(timezone);
|
|
334
333
|
// Extract the part using the provided function
|
|
@@ -395,29 +394,32 @@ function createDateOperations(timezone = 'UTC') {
|
|
|
395
394
|
},
|
|
396
395
|
// Date - extract date portion (ignore time)
|
|
397
396
|
$date(params, ownerQuery, options) {
|
|
398
|
-
return createEqualsOperation((value) => {
|
|
399
|
-
|
|
397
|
+
return createEqualsOperation((value, fieldName) => {
|
|
398
|
+
const dateValue = toDateObject(value, fieldName);
|
|
399
|
+
if (!dateValue || !(dateValue instanceof Date) || isNaN(dateValue.getTime())) {
|
|
400
400
|
return false;
|
|
401
|
-
|
|
402
|
-
|
|
401
|
+
}
|
|
402
|
+
// Convert params to Date using serializer
|
|
403
|
+
// The serializer now parses date strings in the backend timezone
|
|
404
|
+
const paramDate = toDateObject(params, fieldName);
|
|
405
|
+
if (!paramDate || isNaN(paramDate.getTime()))
|
|
403
406
|
return false;
|
|
404
407
|
// Convert both to timezone and get date portions
|
|
405
408
|
const luxonDate = DateTime.fromJSDate(dateValue).setZone(timezone);
|
|
406
|
-
const
|
|
409
|
+
const luxonParam = DateTime.fromJSDate(paramDate).setZone(timezone);
|
|
407
410
|
// Compare year, month, and day
|
|
408
|
-
return luxonDate.year ===
|
|
409
|
-
luxonDate.month ===
|
|
410
|
-
luxonDate.day ===
|
|
411
|
+
return luxonDate.year === luxonParam.year &&
|
|
412
|
+
luxonDate.month === luxonParam.month &&
|
|
413
|
+
luxonDate.day === luxonParam.day;
|
|
411
414
|
}, ownerQuery, options);
|
|
412
415
|
},
|
|
413
416
|
// Time - extract time portion (ignore date)
|
|
414
417
|
$time(params, ownerQuery, options) {
|
|
415
|
-
return createEqualsOperation((value) => {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const dateValue = value instanceof Date ? value : new Date(value);
|
|
419
|
-
if (isNaN(dateValue.getTime()))
|
|
418
|
+
return createEqualsOperation((value, fieldName) => {
|
|
419
|
+
const dateValue = toDateObject(value, fieldName);
|
|
420
|
+
if (!dateValue || !(dateValue instanceof Date) || isNaN(dateValue.getTime())) {
|
|
420
421
|
return false;
|
|
422
|
+
}
|
|
421
423
|
// Convert to timezone
|
|
422
424
|
const luxonDate = DateTime.fromJSDate(dateValue).setZone(timezone);
|
|
423
425
|
// Parse the time string (format: "HH:MM:SS" or a Date object)
|
|
@@ -430,11 +432,14 @@ function createDateOperations(timezone = 'UTC') {
|
|
|
430
432
|
paramSecond = parseInt(timeParts[2], 10);
|
|
431
433
|
}
|
|
432
434
|
else {
|
|
433
|
-
//
|
|
434
|
-
const paramDate =
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
435
|
+
// Convert params to Date using serializer
|
|
436
|
+
const paramDate = toDateObject(params, fieldName);
|
|
437
|
+
if (!paramDate)
|
|
438
|
+
return false;
|
|
439
|
+
const luxonParam = DateTime.fromJSDate(paramDate).setZone(timezone);
|
|
440
|
+
paramHour = luxonParam.hour;
|
|
441
|
+
paramMinute = luxonParam.minute;
|
|
442
|
+
paramSecond = luxonParam.second;
|
|
438
443
|
}
|
|
439
444
|
// Compare hour, minute, and second
|
|
440
445
|
return luxonDate.hour === paramHour &&
|
|
@@ -463,12 +468,11 @@ function createDateOperations(timezone = 'UTC') {
|
|
|
463
468
|
datePartComparisons.forEach(op => {
|
|
464
469
|
// Create a custom operation for each combination (e.g., $year_gt, $month_lte)
|
|
465
470
|
operations[`$${part}_${op}`] = (params, ownerQuery, options) => {
|
|
466
|
-
return createEqualsOperation((value) => {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const dateValue = value instanceof Date ? value : new Date(value);
|
|
470
|
-
if (isNaN(dateValue.getTime()))
|
|
471
|
+
return createEqualsOperation((value, fieldName) => {
|
|
472
|
+
const dateValue = toDateObject(value, fieldName);
|
|
473
|
+
if (!dateValue || !(dateValue instanceof Date) || isNaN(dateValue.getTime())) {
|
|
471
474
|
return false;
|
|
475
|
+
}
|
|
472
476
|
// Convert to timezone and extract part
|
|
473
477
|
const luxonDate = DateTime.fromJSDate(dateValue).setZone(timezone);
|
|
474
478
|
const partValue = extractor(luxonDate);
|
|
@@ -499,7 +503,7 @@ function createDateOperations(timezone = 'UTC') {
|
|
|
499
503
|
function createFilterWithDateOperations(criteria, ModelClass) {
|
|
500
504
|
const timezone = getBackendTimezone(ModelClass);
|
|
501
505
|
return sift(criteria, {
|
|
502
|
-
operations: createDateOperations(timezone)
|
|
506
|
+
operations: createDateOperations(timezone, ModelClass)
|
|
503
507
|
});
|
|
504
508
|
}
|
|
505
509
|
/**
|
|
@@ -19,9 +19,10 @@ export class DateParsingHelpers {
|
|
|
19
19
|
* @param {string|Date} value - The date value to parse
|
|
20
20
|
* @param {string} fieldName - Name of the field (for schema lookup)
|
|
21
21
|
* @param {Object} schema - The model schema containing format info
|
|
22
|
+
* @param {string} timezone - The backend timezone (defaults to 'UTC')
|
|
22
23
|
* @returns {Date|null} - Parsed Date object or null if invalid
|
|
23
24
|
*/
|
|
24
|
-
static parseDate(value: string | Date, fieldName: string, schema: Object): Date | null;
|
|
25
|
+
static parseDate(value: string | Date, fieldName: string, schema: Object, timezone?: string): Date | null;
|
|
25
26
|
/**
|
|
26
27
|
* Serialize a date using the field's configured format
|
|
27
28
|
* @param {Date} date - The date to serialize
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { format, parse, parseISO } from 'date-fns';
|
|
2
|
+
import { DateTime } from 'luxon';
|
|
2
3
|
/**
|
|
3
4
|
* Date parsing utilities for handling Django style date formats
|
|
4
5
|
*/
|
|
@@ -8,12 +9,13 @@ export class DateParsingHelpers {
|
|
|
8
9
|
* @param {string|Date} value - The date value to parse
|
|
9
10
|
* @param {string} fieldName - Name of the field (for schema lookup)
|
|
10
11
|
* @param {Object} schema - The model schema containing format info
|
|
12
|
+
* @param {string} timezone - The backend timezone (defaults to 'UTC')
|
|
11
13
|
* @returns {Date|null} - Parsed Date object or null if invalid
|
|
12
14
|
*/
|
|
13
|
-
static parseDate(value, fieldName, schema) {
|
|
15
|
+
static parseDate(value, fieldName, schema, timezone = 'UTC') {
|
|
14
16
|
if (!value)
|
|
15
17
|
return null;
|
|
16
|
-
// If already a Date object, return as-is
|
|
18
|
+
// If already a Date object, return as-is (it's already a specific instant in time)
|
|
17
19
|
if (value instanceof Date)
|
|
18
20
|
return value;
|
|
19
21
|
const fieldFormat = schema.properties[fieldName].format;
|
|
@@ -33,10 +35,19 @@ export class DateParsingHelpers {
|
|
|
33
35
|
try {
|
|
34
36
|
// Handle ISO format (Django's default)
|
|
35
37
|
if (!dateFormat || dateFormat === 'iso-8601') {
|
|
36
|
-
|
|
38
|
+
// Parse the string in the server timezone using Luxon
|
|
39
|
+
// This ensures '2022-03-15' is interpreted as midnight in the server TZ, not browser TZ
|
|
40
|
+
const luxonDate = DateTime.fromISO(value, { zone: timezone });
|
|
41
|
+
if (!luxonDate.isValid) {
|
|
42
|
+
console.error(`Failed to parse ISO date "${value}":`, luxonDate.invalidReason);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return luxonDate.toJSDate();
|
|
37
46
|
}
|
|
38
47
|
// Handle supported Django formats
|
|
39
48
|
const dateFnsFormat = this.SUPPORTED_FORMATS[dateFormat];
|
|
49
|
+
// For non-ISO formats, use date-fns but we should still consider timezone
|
|
50
|
+
// For now, parse with date-fns and assume it's in the server timezone
|
|
40
51
|
return parse(value, dateFnsFormat, new Date());
|
|
41
52
|
}
|
|
42
53
|
catch (error) {
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { configInstance } from "../../config.js";
|
|
2
2
|
import { isNil } from "lodash-es";
|
|
3
3
|
import { DateParsingHelpers } from "./dates.js";
|
|
4
|
+
/**
|
|
5
|
+
* Gets the backend timezone for a model class
|
|
6
|
+
* @param {Class} ModelClass - The model class
|
|
7
|
+
* @returns {string} The backend timezone or 'UTC' as fallback
|
|
8
|
+
*/
|
|
9
|
+
function getBackendTimezone(ModelClass) {
|
|
10
|
+
if (!ModelClass || !ModelClass.configKey) {
|
|
11
|
+
return 'UTC'; // Default fallback
|
|
12
|
+
}
|
|
13
|
+
const config = configInstance.getConfig();
|
|
14
|
+
const backendConfig = config.backendConfigs[ModelClass.configKey] || config.backendConfigs.default;
|
|
15
|
+
return backendConfig.BACKEND_TZ || 'UTC';
|
|
16
|
+
}
|
|
4
17
|
/**
|
|
5
18
|
* File field serializer - handles both camelCase (frontend) and snake_case (backend) formats
|
|
6
19
|
*/
|
|
@@ -73,8 +86,11 @@ export const dateFieldSerializer = {
|
|
|
73
86
|
return value;
|
|
74
87
|
const { model, field } = context;
|
|
75
88
|
if (model?.schema) {
|
|
76
|
-
//
|
|
77
|
-
|
|
89
|
+
// Get the backend timezone for this model
|
|
90
|
+
const timezone = getBackendTimezone(model);
|
|
91
|
+
// Use DateParsingHelpers with timezone awareness
|
|
92
|
+
// This ensures date strings are parsed in the server timezone, not browser timezone
|
|
93
|
+
return DateParsingHelpers.parseDate(value, field, model.schema, timezone);
|
|
78
94
|
}
|
|
79
95
|
// Fallback parsing if no schema context
|
|
80
96
|
try {
|
package/package.json
CHANGED