@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
|
|
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,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
|
-
|
|
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
|
+
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
|
|
408
|
+
const luxonParam = DateTime.fromJSDate(paramDate).setZone(timezone);
|
|
407
409
|
// Compare year, month, and day
|
|
408
|
-
return luxonDate.year ===
|
|
409
|
-
luxonDate.month ===
|
|
410
|
-
luxonDate.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
|
-
|
|
417
|
-
|
|
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
|
-
//
|
|
434
|
-
const paramDate =
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
468
|
-
|
|
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