@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 based on the field's schema type.
19
- * This ensures Django-like behavior where date strings are automatically
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
- // Get the field schema
32
- const fieldSchema = ModelClass.schema.properties?.[fieldName];
33
- if (!fieldSchema)
34
- return value;
35
- const { type, format } = fieldSchema;
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
- // Helper function to extract date parts with Django-compatible behavior
326
- const getDatePart = (value, partExtractor) => {
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
- const dateValue = value instanceof Date ? value : new Date(value);
330
- if (isNaN(dateValue.getTime()))
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
- if (!value)
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
- const dateValue = value instanceof Date ? value : new Date(value);
402
- if (isNaN(dateValue.getTime()))
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 paramDate = DateTime.fromJSDate(new Date(params)).setZone(timezone);
409
+ const luxonParam = DateTime.fromJSDate(paramDate).setZone(timezone);
407
410
  // Compare year, month, and day
408
- return luxonDate.year === paramDate.year &&
409
- luxonDate.month === paramDate.month &&
410
- luxonDate.day === paramDate.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
- if (!value)
417
- return false;
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
- // If it's a Date object, extract time parts
434
- const paramDate = DateTime.fromJSDate(new Date(params)).setZone(timezone);
435
- paramHour = paramDate.hour;
436
- paramMinute = paramDate.minute;
437
- paramSecond = paramDate.second;
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
- if (!value)
468
- return false;
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
- return parseISO(value);
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
- // Use DateParsingHelpers like the model does
77
- return DateParsingHelpers.parseDate(value, field, model.schema);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statezero/core",
3
- "version": "0.1.91",
3
+ "version": "0.1.93",
4
4
  "type": "module",
5
5
  "module": "ESNext",
6
6
  "description": "The type-safe frontend client for StateZero - connect directly to your backend models with zero boilerplate",