@statezero/core 0.1.90 → 0.1.92

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,31 @@ 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
+ const paramDate = toDateObject(params, fieldName);
404
+ if (!paramDate || isNaN(paramDate.getTime()))
403
405
  return false;
404
406
  // Convert both to timezone and get date portions
405
407
  const luxonDate = DateTime.fromJSDate(dateValue).setZone(timezone);
406
- const paramDate = DateTime.fromJSDate(new Date(params)).setZone(timezone);
408
+ const luxonParam = DateTime.fromJSDate(paramDate).setZone(timezone);
407
409
  // Compare year, month, and day
408
- return luxonDate.year === paramDate.year &&
409
- luxonDate.month === paramDate.month &&
410
- luxonDate.day === paramDate.day;
410
+ return luxonDate.year === luxonParam.year &&
411
+ luxonDate.month === luxonParam.month &&
412
+ luxonDate.day === luxonParam.day;
411
413
  }, ownerQuery, options);
412
414
  },
413
415
  // Time - extract time portion (ignore date)
414
416
  $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()))
417
+ return createEqualsOperation((value, fieldName) => {
418
+ const dateValue = toDateObject(value, fieldName);
419
+ if (!dateValue || !(dateValue instanceof Date) || isNaN(dateValue.getTime())) {
420
420
  return false;
421
+ }
421
422
  // Convert to timezone
422
423
  const luxonDate = DateTime.fromJSDate(dateValue).setZone(timezone);
423
424
  // Parse the time string (format: "HH:MM:SS" or a Date object)
@@ -430,11 +431,14 @@ function createDateOperations(timezone = 'UTC') {
430
431
  paramSecond = parseInt(timeParts[2], 10);
431
432
  }
432
433
  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;
434
+ // Convert params to Date using serializer
435
+ const paramDate = toDateObject(params, fieldName);
436
+ if (!paramDate)
437
+ return false;
438
+ const luxonParam = DateTime.fromJSDate(paramDate).setZone(timezone);
439
+ paramHour = luxonParam.hour;
440
+ paramMinute = luxonParam.minute;
441
+ paramSecond = luxonParam.second;
438
442
  }
439
443
  // Compare hour, minute, and second
440
444
  return luxonDate.hour === paramHour &&
@@ -463,12 +467,11 @@ function createDateOperations(timezone = 'UTC') {
463
467
  datePartComparisons.forEach(op => {
464
468
  // Create a custom operation for each combination (e.g., $year_gt, $month_lte)
465
469
  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()))
470
+ return createEqualsOperation((value, fieldName) => {
471
+ const dateValue = toDateObject(value, fieldName);
472
+ if (!dateValue || !(dateValue instanceof Date) || isNaN(dateValue.getTime())) {
471
473
  return false;
474
+ }
472
475
  // Convert to timezone and extract part
473
476
  const luxonDate = DateTime.fromJSDate(dateValue).setZone(timezone);
474
477
  const partValue = extractor(luxonDate);
@@ -499,7 +502,7 @@ function createDateOperations(timezone = 'UTC') {
499
502
  function createFilterWithDateOperations(criteria, ModelClass) {
500
503
  const timezone = getBackendTimezone(ModelClass);
501
504
  return sift(criteria, {
502
- operations: createDateOperations(timezone)
505
+ operations: createDateOperations(timezone, ModelClass)
503
506
  });
504
507
  }
505
508
  /**
@@ -43,6 +43,16 @@ export class Model {
43
43
  * @returns {Promise<boolean>} Promise that resolves to true if valid, throws error if invalid
44
44
  */
45
45
  static validate(data: Object, validateType?: string, partial?: boolean): Promise<boolean>;
46
+ /**
47
+ * Get field permissions for the current user (cached on the class)
48
+ * @param {boolean} refresh - Force refresh the cached permissions
49
+ * @returns {Promise<{visible_fields: string[], creatable_fields: string[], editable_fields: string[]}>}
50
+ */
51
+ static getFieldPermissions(refresh?: boolean): Promise<{
52
+ visible_fields: string[];
53
+ creatable_fields: string[];
54
+ editable_fields: string[];
55
+ }>;
46
56
  constructor(data?: {});
47
57
  serializer: ModelSerializer;
48
58
  _data: {};
@@ -299,6 +299,50 @@ export class Model {
299
299
  throw new Error(`Validation failed: ${error.message}`);
300
300
  }
301
301
  }
302
+ /**
303
+ * Get field permissions for the current user (cached on the class)
304
+ * @param {boolean} refresh - Force refresh the cached permissions
305
+ * @returns {Promise<{visible_fields: string[], creatable_fields: string[], editable_fields: string[]}>}
306
+ */
307
+ static async getFieldPermissions(refresh = false) {
308
+ const ModelClass = this;
309
+ // Return cached permissions if available and not forcing refresh
310
+ if (!refresh && ModelClass._fieldPermissionsCache) {
311
+ return ModelClass._fieldPermissionsCache;
312
+ }
313
+ // Get backend config and check if it exists
314
+ const config = configInstance.getConfig();
315
+ const backend = config.backendConfigs[ModelClass.configKey];
316
+ if (!backend) {
317
+ throw new Error(`No backend configuration found for key: ${ModelClass.configKey}`);
318
+ }
319
+ // Build URL for field permissions endpoint
320
+ const baseUrl = backend.API_URL.replace(/\/+$/, "");
321
+ const url = `${baseUrl}/${ModelClass.modelName}/field-permissions/`;
322
+ // Prepare headers
323
+ const headers = {
324
+ "Content-Type": "application/json",
325
+ ...(backend.getAuthHeaders ? backend.getAuthHeaders() : {}),
326
+ };
327
+ // Make direct API call to field permissions endpoint
328
+ try {
329
+ const response = await axios.get(url, { headers });
330
+ // Cache the permissions on the class
331
+ ModelClass._fieldPermissionsCache = response.data;
332
+ // Backend returns {visible_fields: [], creatable_fields: [], editable_fields: []}
333
+ return response.data;
334
+ }
335
+ catch (error) {
336
+ if (error.response && error.response.data) {
337
+ const parsedError = parseStateZeroError(error.response.data);
338
+ if (Error.captureStackTrace) {
339
+ Error.captureStackTrace(parsedError, ModelClass.getFieldPermissions);
340
+ }
341
+ throw parsedError;
342
+ }
343
+ throw new Error(`Failed to get field permissions: ${error.message}`);
344
+ }
345
+ }
302
346
  }
303
347
  /**
304
348
  * Creates a new Model instance.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statezero/core",
3
- "version": "0.1.90",
3
+ "version": "0.1.92",
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",