@statezero/core 0.1.2 → 0.1.3-9.2
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/LICENSE +3 -2
- package/dist/cli/commands/sync.d.ts +6 -0
- package/dist/cli/commands/sync.js +30 -0
- package/dist/cli/commands/syncActions.d.ts +46 -0
- package/dist/cli/commands/syncActions.js +623 -0
- package/dist/cli/commands/syncModels.js +17 -35
- package/dist/cli/index.js +18 -10
- package/dist/config.d.ts +5 -0
- package/dist/config.js +40 -10
- package/dist/flavours/django/dates.js +3 -3
- package/dist/flavours/django/files.d.ts +8 -7
- package/dist/flavours/django/files.js +36 -2
- package/dist/flavours/django/model.d.ts +15 -0
- package/dist/flavours/django/model.js +143 -24
- package/dist/setup.js +11 -0
- package/dist/syncEngine/registries/metricRegistry.d.ts +5 -0
- package/dist/syncEngine/registries/metricRegistry.js +8 -0
- package/dist/syncEngine/registries/querysetStoreGraph.d.ts +21 -0
- package/dist/syncEngine/registries/querysetStoreGraph.js +95 -0
- package/dist/syncEngine/registries/querysetStoreRegistry.d.ts +14 -0
- package/dist/syncEngine/registries/querysetStoreRegistry.js +64 -16
- package/dist/syncEngine/stores/modelStore.d.ts +3 -0
- package/dist/syncEngine/stores/modelStore.js +76 -41
- package/dist/syncEngine/stores/querysetStore.d.ts +19 -0
- package/dist/syncEngine/stores/querysetStore.js +133 -18
- package/dist/syncEngine/sync.d.ts +5 -0
- package/dist/syncEngine/sync.js +61 -5
- package/package.json +126 -123
- package/readme.md +1 -1
|
@@ -97,32 +97,27 @@ import { loadConfigFromFile } from "../configFileLoader.js";
|
|
|
97
97
|
// Fallback Selection for Inquirer Errors
|
|
98
98
|
// --------------------
|
|
99
99
|
/**
|
|
100
|
-
* Simple fallback
|
|
100
|
+
* Simple fallback that generates all models when inquirer fails
|
|
101
101
|
* @param {Array} choices - Array of choice objects with {name, value, checked} properties
|
|
102
102
|
* @param {string} message - Selection message
|
|
103
|
-
* @returns {Promise<Array>} -
|
|
103
|
+
* @returns {Promise<Array>} - All model values
|
|
104
104
|
*/
|
|
105
|
-
async function
|
|
105
|
+
async function fallbackSelectAll(choices, message) {
|
|
106
106
|
console.log(`\n${message}`);
|
|
107
|
-
console.log("Interactive selection not available -
|
|
108
|
-
const
|
|
107
|
+
console.log("Interactive selection not available - generating ALL models:");
|
|
108
|
+
const allModels = [];
|
|
109
109
|
for (const choice of choices) {
|
|
110
110
|
// Skip separators (they don't have a 'value' property)
|
|
111
111
|
if (!choice.value) {
|
|
112
112
|
console.log(choice.name); // Print separator text
|
|
113
113
|
continue;
|
|
114
114
|
}
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
console.log(` ✓ ${choice.name}`);
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
console.log(` ${choice.name}`);
|
|
122
|
-
}
|
|
115
|
+
// Add ALL models, regardless of checked status
|
|
116
|
+
allModels.push(choice.value);
|
|
117
|
+
console.log(` ✓ ${choice.name}`);
|
|
123
118
|
}
|
|
124
|
-
console.log(`\
|
|
125
|
-
return
|
|
119
|
+
console.log(`\nGenerating ALL ${allModels.length} models.`);
|
|
120
|
+
return allModels;
|
|
126
121
|
}
|
|
127
122
|
/**
|
|
128
123
|
* Model selection with inquirer fallback
|
|
@@ -146,9 +141,9 @@ async function selectModels(choices, message) {
|
|
|
146
141
|
return selectedModels;
|
|
147
142
|
}
|
|
148
143
|
catch (error) {
|
|
149
|
-
// Fall back to
|
|
150
|
-
console.warn("Interactive selection failed,
|
|
151
|
-
return await
|
|
144
|
+
// Fall back to generating all models if inquirer fails for any reason
|
|
145
|
+
console.warn("Interactive selection failed, generating all models:", error.message);
|
|
146
|
+
return await fallbackSelectAll(choices, message);
|
|
152
147
|
}
|
|
153
148
|
}
|
|
154
149
|
// --------------------
|
|
@@ -164,7 +159,7 @@ const JS_MODEL_TEMPLATE = `/**
|
|
|
164
159
|
|
|
165
160
|
import { Model, Manager, QuerySet, getModelClass } from '{{modulePath}}';
|
|
166
161
|
import { wrapReactiveModel } from '{{modulePath}}';
|
|
167
|
-
import schemaData from './{{className}}.schema.json';
|
|
162
|
+
import schemaData from './{{toLowerCase className}}.schema.json';
|
|
168
163
|
|
|
169
164
|
/**
|
|
170
165
|
* Model-specific QuerySet implementation
|
|
@@ -232,22 +227,6 @@ export class {{className}} extends Model {
|
|
|
232
227
|
});
|
|
233
228
|
});
|
|
234
229
|
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Serialize the model's data.
|
|
238
|
-
* Returns a plain object containing the current values of all fields,
|
|
239
|
-
* including raw IDs for relationship fields.
|
|
240
|
-
* @returns {Object}
|
|
241
|
-
*/
|
|
242
|
-
serialize() {
|
|
243
|
-
const data = {};
|
|
244
|
-
// Simply assign the current value of each field.
|
|
245
|
-
// No special handling or PK extraction for relationships here.
|
|
246
|
-
{{#each properties}}
|
|
247
|
-
data.{{name}} = this.{{name}};
|
|
248
|
-
{{/each}}
|
|
249
|
-
return data;
|
|
250
|
-
}
|
|
251
230
|
}
|
|
252
231
|
`;
|
|
253
232
|
// Updated TS_DECLARATION_TEMPLATE with improved relationship handling
|
|
@@ -504,6 +483,9 @@ Handlebars.registerHelper("ifDefaultProvided", function (defaultValue, options)
|
|
|
504
483
|
Handlebars.registerHelper("isRequired", function (required) {
|
|
505
484
|
return required ? "" : "?";
|
|
506
485
|
});
|
|
486
|
+
Handlebars.registerHelper("toLowerCase", function (str) {
|
|
487
|
+
return str.toLowerCase();
|
|
488
|
+
});
|
|
507
489
|
const jsTemplate = Handlebars.compile(JS_MODEL_TEMPLATE);
|
|
508
490
|
const dtsTemplate = Handlebars.compile(TS_DECLARATION_TEMPLATE);
|
|
509
491
|
// --------------------
|
package/dist/cli/index.js
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import dotenv from
|
|
2
|
+
import dotenv from "dotenv";
|
|
3
3
|
dotenv.config();
|
|
4
|
-
import yargs from
|
|
5
|
-
import { hideBin } from
|
|
6
|
-
import { generateSchema } from
|
|
4
|
+
import yargs from "yargs";
|
|
5
|
+
import { hideBin } from "yargs/helpers";
|
|
6
|
+
import { generateSchema } from "./commands/syncModels.js";
|
|
7
|
+
import { generateActions } from "./commands/syncActions.js";
|
|
8
|
+
import { sync } from "./commands/sync.js"; // Import the new combined sync function
|
|
7
9
|
yargs(hideBin(process.argv))
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
await
|
|
10
|
+
// The new 'sync' command
|
|
11
|
+
.command("sync", "Synchronize both models and actions from the backend", {}, // Builder for command-specific options (if any)
|
|
12
|
+
async (argv) => {
|
|
13
|
+
await sync(argv);
|
|
12
14
|
})
|
|
13
|
-
.
|
|
14
|
-
|
|
15
|
+
.command("sync-models", "Generate model classes from the backend schema", {}, async (argv) => {
|
|
16
|
+
await generateSchema(argv);
|
|
17
|
+
})
|
|
18
|
+
.command("sync-actions", "Generate action functions from the backend schema", {}, async (argv) => {
|
|
19
|
+
await generateActions(); // This function does not take arguments
|
|
20
|
+
})
|
|
21
|
+
.demandCommand(1, "You must provide a command to run. Use --help to see available commands.")
|
|
22
|
+
.help().argv;
|
package/dist/config.d.ts
CHANGED
|
@@ -29,6 +29,10 @@ export function initializeAllEventReceivers(): Object;
|
|
|
29
29
|
* @param {Function} getterFn - The getModelClass function imported from model-registry.js
|
|
30
30
|
*/
|
|
31
31
|
export function registerModelGetter(getterFn: Function): void;
|
|
32
|
+
/**
|
|
33
|
+
* Helper function to build file URLs
|
|
34
|
+
*/
|
|
35
|
+
export function buildFileUrl(fileUrl: any, backendKey?: string): any;
|
|
32
36
|
/**
|
|
33
37
|
* Get a model class by name using the registered getter.
|
|
34
38
|
*
|
|
@@ -47,6 +51,7 @@ export namespace configInstance {
|
|
|
47
51
|
export { initializeEventReceiver };
|
|
48
52
|
export { registerModelGetter };
|
|
49
53
|
export { getModelClass };
|
|
54
|
+
export { buildFileUrl };
|
|
50
55
|
}
|
|
51
56
|
export default configInstance;
|
|
52
57
|
import { PusherEventReceiver } from './core/eventReceivers.js';
|
package/dist/config.js
CHANGED
|
@@ -44,15 +44,23 @@ const eventConfigSchema = z.object({
|
|
|
44
44
|
}
|
|
45
45
|
});
|
|
46
46
|
const backendSchema = z.object({
|
|
47
|
-
API_URL: z.string().url(
|
|
48
|
-
GENERATED_TYPES_DIR: z.string({
|
|
47
|
+
API_URL: z.string().url("API_URL must be a valid URL"),
|
|
48
|
+
GENERATED_TYPES_DIR: z.string({
|
|
49
|
+
required_error: "GENERATED_TYPES_DIR is required",
|
|
50
|
+
}),
|
|
51
|
+
GENERATED_ACTIONS_DIR: z.string().optional(),
|
|
49
52
|
BACKEND_TZ: z.string().optional(),
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
.
|
|
55
|
-
|
|
53
|
+
fileRootURL: z.string().url("fileRootURL must be a valid URL").optional(),
|
|
54
|
+
fileUploadMode: z.enum(["server", "s3"]).default("server"),
|
|
55
|
+
getAuthHeaders: z
|
|
56
|
+
.function()
|
|
57
|
+
.optional()
|
|
58
|
+
.refine((fn) => fn === undefined || typeof fn === "function", "getAuthHeaders must be a function if provided"),
|
|
59
|
+
eventInterceptor: z
|
|
60
|
+
.function()
|
|
61
|
+
.optional()
|
|
62
|
+
.refine((fn) => fn === undefined || typeof fn === "function", "eventInterceptor must be a function if provided"),
|
|
63
|
+
events: z.lazy(() => eventConfigSchema.optional()),
|
|
56
64
|
});
|
|
57
65
|
const configSchema = z.object({
|
|
58
66
|
backendConfigs: z.record(z.string(), backendSchema)
|
|
@@ -76,7 +84,8 @@ const configSchema = z.object({
|
|
|
76
84
|
}
|
|
77
85
|
}
|
|
78
86
|
return { message: errors.join('; ') };
|
|
79
|
-
})
|
|
87
|
+
}),
|
|
88
|
+
periodicSyncIntervalSeconds: z.number().min(5).nullable().optional().default(null),
|
|
80
89
|
});
|
|
81
90
|
// Internal variable to hold the validated configuration.
|
|
82
91
|
let config = null;
|
|
@@ -212,6 +221,26 @@ export function registerModelGetter(getterFn) {
|
|
|
212
221
|
}
|
|
213
222
|
modelGetter = getterFn;
|
|
214
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Helper function to build file URLs
|
|
226
|
+
*/
|
|
227
|
+
export function buildFileUrl(fileUrl, backendKey = 'default') {
|
|
228
|
+
// If URL is already absolute (cloud storage), use it as-is
|
|
229
|
+
if (fileUrl && fileUrl.startsWith('http')) {
|
|
230
|
+
return fileUrl;
|
|
231
|
+
}
|
|
232
|
+
const cfg = getConfig();
|
|
233
|
+
const backend = cfg.backendConfigs[backendKey];
|
|
234
|
+
if (!backend) {
|
|
235
|
+
throw new ConfigError(`Backend "${backendKey}" not found in configuration.`);
|
|
236
|
+
}
|
|
237
|
+
// If no fileRootURL provided, return relative URL as-is
|
|
238
|
+
if (!backend.fileRootURL) {
|
|
239
|
+
return fileUrl;
|
|
240
|
+
}
|
|
241
|
+
// Construct full URL
|
|
242
|
+
return backend.fileRootURL.replace(/\/$/, '') + fileUrl;
|
|
243
|
+
}
|
|
215
244
|
/**
|
|
216
245
|
* Get a model class by name using the registered getter.
|
|
217
246
|
*
|
|
@@ -237,6 +266,7 @@ export const configInstance = {
|
|
|
237
266
|
setBackendConfig,
|
|
238
267
|
initializeEventReceiver,
|
|
239
268
|
registerModelGetter,
|
|
240
|
-
getModelClass
|
|
269
|
+
getModelClass,
|
|
270
|
+
buildFileUrl
|
|
241
271
|
};
|
|
242
272
|
export default configInstance;
|
|
@@ -23,7 +23,7 @@ export class DateParsingHelpers {
|
|
|
23
23
|
// Get field-specific format from schema
|
|
24
24
|
const formatStrings = {
|
|
25
25
|
'date': schema.date_format,
|
|
26
|
-
'
|
|
26
|
+
'date-time': schema.datetime_format
|
|
27
27
|
};
|
|
28
28
|
const dateFormat = formatStrings[fieldFormat];
|
|
29
29
|
// Check if format is supported
|
|
@@ -56,13 +56,13 @@ export class DateParsingHelpers {
|
|
|
56
56
|
return date;
|
|
57
57
|
}
|
|
58
58
|
const fieldFormat = schema.properties[fieldName].format;
|
|
59
|
-
if (!["date", "
|
|
59
|
+
if (!["date", "date-time"].includes(fieldFormat)) {
|
|
60
60
|
throw new Error(`Only date and date-time fields can be processed to JS date objects. ${fieldName} has format ${fieldFormat}`);
|
|
61
61
|
}
|
|
62
62
|
// Get field-specific format from schema
|
|
63
63
|
const formatStrings = {
|
|
64
64
|
'date': schema.date_format,
|
|
65
|
-
'
|
|
65
|
+
'date-time': schema.datetime_format
|
|
66
66
|
};
|
|
67
67
|
const dateFormat = formatStrings[fieldFormat];
|
|
68
68
|
// Check if format is supported
|
|
@@ -5,10 +5,10 @@ export class FileObject {
|
|
|
5
5
|
static configKey: string;
|
|
6
6
|
static MIN_CHUNK_SIZE: number;
|
|
7
7
|
constructor(file: any, options?: {});
|
|
8
|
-
name:
|
|
9
|
-
size:
|
|
10
|
-
type:
|
|
11
|
-
lastModified: number;
|
|
8
|
+
name: any;
|
|
9
|
+
size: any;
|
|
10
|
+
type: any;
|
|
11
|
+
lastModified: number | null;
|
|
12
12
|
uploaded: boolean;
|
|
13
13
|
uploading: boolean;
|
|
14
14
|
uploadResult: any;
|
|
@@ -21,6 +21,7 @@ export class FileObject {
|
|
|
21
21
|
chunkSize: any;
|
|
22
22
|
maxConcurrency: any;
|
|
23
23
|
uploadPromise: any;
|
|
24
|
+
get isStoredFile(): any;
|
|
24
25
|
get status(): "failed" | "uploading" | "uploaded" | "pending";
|
|
25
26
|
get filePath(): any;
|
|
26
27
|
get fileUrl(): any;
|
|
@@ -59,9 +60,9 @@ export class FileObject {
|
|
|
59
60
|
getBlob(): Blob;
|
|
60
61
|
waitForUpload(): Promise<any>;
|
|
61
62
|
toJSON(): {
|
|
62
|
-
name:
|
|
63
|
-
size:
|
|
64
|
-
type:
|
|
63
|
+
name: any;
|
|
64
|
+
size: any;
|
|
65
|
+
type: any;
|
|
65
66
|
status: string;
|
|
66
67
|
uploaded: boolean;
|
|
67
68
|
filePath: any;
|
|
@@ -5,9 +5,37 @@ import PQueue from "p-queue";
|
|
|
5
5
|
* FileObject - A file wrapper that handles uploads to StateZero backend
|
|
6
6
|
*/
|
|
7
7
|
export class FileObject {
|
|
8
|
+
// Simple changes to FileObject constructor
|
|
8
9
|
constructor(file, options = {}) {
|
|
10
|
+
// Handle stored file data (from API)
|
|
11
|
+
if (file &&
|
|
12
|
+
typeof file === "object" &&
|
|
13
|
+
file.file_path &&
|
|
14
|
+
!(file instanceof File)) {
|
|
15
|
+
// This is stored file data from the backend
|
|
16
|
+
this.name = file.file_name;
|
|
17
|
+
this.size = file.size;
|
|
18
|
+
this.type = file.mime_type; // Now coming from backend
|
|
19
|
+
this.lastModified = null;
|
|
20
|
+
// Mark as already uploaded
|
|
21
|
+
this.uploaded = true;
|
|
22
|
+
this.uploading = false;
|
|
23
|
+
this.uploadResult = file; // Store the entire response
|
|
24
|
+
this.uploadError = null;
|
|
25
|
+
this.fileData = null;
|
|
26
|
+
// No upload properties needed
|
|
27
|
+
this.uploadType = null;
|
|
28
|
+
this.uploadId = null;
|
|
29
|
+
this.totalChunks = 0;
|
|
30
|
+
this.completedChunks = 0;
|
|
31
|
+
this.chunkSize = null;
|
|
32
|
+
this.maxConcurrency = null;
|
|
33
|
+
this.uploadPromise = Promise.resolve(this.uploadResult);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// Handle File objects (for upload) - existing code
|
|
9
37
|
if (!file || !(file instanceof File)) {
|
|
10
|
-
throw new Error("FileObject requires a File object");
|
|
38
|
+
throw new Error("FileObject requires a File object or stored file data");
|
|
11
39
|
}
|
|
12
40
|
// Store file metadata directly
|
|
13
41
|
this.name = file.name;
|
|
@@ -33,6 +61,9 @@ export class FileObject {
|
|
|
33
61
|
this.maxConcurrency = options.maxConcurrency || 3;
|
|
34
62
|
this.uploadPromise = this._initializeAndStartUpload(file, options);
|
|
35
63
|
}
|
|
64
|
+
get isStoredFile() {
|
|
65
|
+
return this.uploaded && !this.fileData && this.uploadResult?.file_path;
|
|
66
|
+
}
|
|
36
67
|
get status() {
|
|
37
68
|
if (this.uploadError)
|
|
38
69
|
return "failed";
|
|
@@ -46,7 +77,10 @@ export class FileObject {
|
|
|
46
77
|
return this.uploadResult?.file_path;
|
|
47
78
|
}
|
|
48
79
|
get fileUrl() {
|
|
49
|
-
|
|
80
|
+
if (!this.uploadResult?.file_url) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return configInstance.buildFileUrl(this.uploadResult.file_url, this.constructor.configKey);
|
|
50
84
|
}
|
|
51
85
|
async _initializeAndStartUpload(file, options) {
|
|
52
86
|
const config = configInstance.getConfig();
|
|
@@ -35,6 +35,14 @@ export class Model {
|
|
|
35
35
|
* @throws {ValidationError} If an unknown key is found.
|
|
36
36
|
*/
|
|
37
37
|
static validateFields(data: Object): void;
|
|
38
|
+
/**
|
|
39
|
+
* Static method to validate data without creating an instance
|
|
40
|
+
* @param {Object} data - Data to validate
|
|
41
|
+
* @param {string} validateType - 'create' or 'update'
|
|
42
|
+
* @param {boolean} partial - Whether to allow partial validation
|
|
43
|
+
* @returns {Promise<boolean>} Promise that resolves to true if valid, throws error if invalid
|
|
44
|
+
*/
|
|
45
|
+
static validate(data: Object, validateType?: string, partial?: boolean): Promise<boolean>;
|
|
38
46
|
constructor(data?: {});
|
|
39
47
|
_data: {};
|
|
40
48
|
_pk: any;
|
|
@@ -105,6 +113,13 @@ export class Model {
|
|
|
105
113
|
* @throws {Error} If the instance has not been saved (no primary key).
|
|
106
114
|
*/
|
|
107
115
|
refreshFromDb(): Promise<void>;
|
|
116
|
+
/**
|
|
117
|
+
* Validates the model instance using the same serialize behavior as save()
|
|
118
|
+
* @param {string} validateType - 'create' or 'update' (defaults to auto-detect)
|
|
119
|
+
* @param {boolean} partial - Whether to allow partial validation
|
|
120
|
+
* @returns {Promise<boolean>} Promise that resolves to true if valid, throws error if invalid
|
|
121
|
+
*/
|
|
122
|
+
validate(validateType?: string, partial?: boolean): Promise<boolean>;
|
|
108
123
|
}
|
|
109
124
|
/**
|
|
110
125
|
* A constructor for a Model.
|
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
1
|
+
var __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) {
|
|
2
|
+
if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : "";
|
|
3
|
+
return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name });
|
|
4
|
+
};
|
|
5
|
+
import { Manager } from "./manager.js";
|
|
6
|
+
import { ValidationError } from "./errors.js";
|
|
7
|
+
import { modelStoreRegistry } from "../../syncEngine/registries/modelStoreRegistry.js";
|
|
8
|
+
import { isNil } from "lodash-es";
|
|
9
|
+
import { QueryExecutor } from "./queryExecutor";
|
|
10
|
+
import { wrapReactiveModel } from "../../reactiveAdaptor.js";
|
|
11
|
+
import { DateParsingHelpers } from "./dates.js";
|
|
12
|
+
import { FileObject } from './files.js';
|
|
13
|
+
import { configInstance } from "../../config.js";
|
|
14
|
+
import { parseStateZeroError, MultipleObjectsReturned, DoesNotExist, } from "./errors.js";
|
|
15
|
+
import axios from "axios";
|
|
8
16
|
/**
|
|
9
17
|
* A constructor for a Model.
|
|
10
18
|
*
|
|
@@ -54,7 +62,7 @@ export class Model {
|
|
|
54
62
|
* Instantiate from pk using queryset scoped singletons
|
|
55
63
|
*/
|
|
56
64
|
static fromPk(pk, querySet) {
|
|
57
|
-
let qsId = querySet ? querySet.__uuid :
|
|
65
|
+
let qsId = querySet ? querySet.__uuid : "";
|
|
58
66
|
let key = `${qsId}__${this.configKey}__${this.modelName}__${pk}`;
|
|
59
67
|
if (!this.instanceCache.has(key)) {
|
|
60
68
|
const instance = new this();
|
|
@@ -70,6 +78,7 @@ export class Model {
|
|
|
70
78
|
* @returns {any} The field value
|
|
71
79
|
*/
|
|
72
80
|
getField(field) {
|
|
81
|
+
var _a;
|
|
73
82
|
// Access the reactive __version property to establish dependency for vue integration
|
|
74
83
|
const trackVersion = this.__version;
|
|
75
84
|
const ModelClass = this.constructor;
|
|
@@ -78,11 +87,39 @@ export class Model {
|
|
|
78
87
|
// check local overrides
|
|
79
88
|
let value = this._data[field];
|
|
80
89
|
// if its not been overridden, get it from the store
|
|
81
|
-
if (
|
|
90
|
+
if (value === undefined && !isNil(this._pk)) {
|
|
82
91
|
let storedValue = modelStoreRegistry.getEntity(ModelClass, this._pk);
|
|
83
92
|
if (storedValue)
|
|
84
93
|
value = storedValue[field]; // if stops null -> undefined
|
|
85
94
|
}
|
|
95
|
+
// Date/DateTime fields need special handling - convert to Date objects
|
|
96
|
+
const dateFormats = ["date", "datetime", "date-time"];
|
|
97
|
+
if (ModelClass.schema &&
|
|
98
|
+
dateFormats.includes(ModelClass.schema.properties[field]?.format) &&
|
|
99
|
+
value) {
|
|
100
|
+
// Let DateParsingHelpers.parseDate throw if it fails
|
|
101
|
+
return DateParsingHelpers.parseDate(value, field, ModelClass.schema);
|
|
102
|
+
}
|
|
103
|
+
// File/Image fields need special handling - wrap as FileObject
|
|
104
|
+
const fileFormats = ["file-path", "image-path"];
|
|
105
|
+
if (ModelClass.schema &&
|
|
106
|
+
fileFormats.includes(ModelClass.schema.properties[field]?.format) &&
|
|
107
|
+
value) {
|
|
108
|
+
// Check if it's already a FileObject
|
|
109
|
+
if (value instanceof FileObject) {
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
// If it's stored file data from API, wrap it as FileObject
|
|
113
|
+
if (typeof value === "object" && value.file_path) {
|
|
114
|
+
// Create anonymous subclass with correct configKey
|
|
115
|
+
const BackendFileObject = (_a = class extends FileObject {
|
|
116
|
+
},
|
|
117
|
+
__setFunctionName(_a, "BackendFileObject"),
|
|
118
|
+
_a.configKey = ModelClass.configKey,
|
|
119
|
+
_a);
|
|
120
|
+
return new BackendFileObject(value);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
86
123
|
// relationship fields need special handling
|
|
87
124
|
if (ModelClass.relationshipFields.has(field) && value) {
|
|
88
125
|
// fetch the stored value
|
|
@@ -90,15 +127,15 @@ export class Model {
|
|
|
90
127
|
// footgun - fieldInfo.ModelClass() calls the arrow function that lazily gets the model class
|
|
91
128
|
let relPkField = fieldInfo.ModelClass().primaryKeyField;
|
|
92
129
|
switch (fieldInfo.relationshipType) {
|
|
93
|
-
case
|
|
130
|
+
case "many-to-many":
|
|
94
131
|
// value is an array
|
|
95
132
|
if (!Array.isArray(value) && value)
|
|
96
133
|
throw new Error(`Data corruption: m2m field for ${ModelClass.modelName} stored as ${value}`);
|
|
97
134
|
// set each pk to the full model object for that pk
|
|
98
|
-
value = value.map(pk => fieldInfo.ModelClass().fromPk(pk));
|
|
135
|
+
value = value.map((pk) => fieldInfo.ModelClass().fromPk(pk));
|
|
99
136
|
break;
|
|
100
|
-
case
|
|
101
|
-
case
|
|
137
|
+
case "one-to-one":
|
|
138
|
+
case "foreign-key":
|
|
102
139
|
// footgun - fieldInfo.ModelClass() calls the arrow function that lazily gets the model class
|
|
103
140
|
if (!isNil(value))
|
|
104
141
|
value = fieldInfo.ModelClass().fromPk(value);
|
|
@@ -135,13 +172,13 @@ export class Model {
|
|
|
135
172
|
return;
|
|
136
173
|
const allowedFields = this.fields;
|
|
137
174
|
for (const key of Object.keys(data)) {
|
|
138
|
-
if (key ===
|
|
175
|
+
if (key === "repr" || key === "type")
|
|
139
176
|
continue;
|
|
140
177
|
// Handle nested fields by splitting on double underscore
|
|
141
178
|
// and taking just the base field name
|
|
142
|
-
const baseField = key.split(
|
|
179
|
+
const baseField = key.split("__")[0];
|
|
143
180
|
if (!allowedFields.includes(baseField)) {
|
|
144
|
-
let errorMsg = `Invalid field: ${baseField}. Allowed fields are: ${allowedFields.join(
|
|
181
|
+
let errorMsg = `Invalid field: ${baseField}. Allowed fields are: ${allowedFields.join(", ")}`;
|
|
145
182
|
console.error(errorMsg);
|
|
146
183
|
throw new ValidationError(errorMsg);
|
|
147
184
|
}
|
|
@@ -160,11 +197,19 @@ export class Model {
|
|
|
160
197
|
// check local overrides
|
|
161
198
|
let value = this._data[field];
|
|
162
199
|
// if it's not been overridden, get it from the store
|
|
163
|
-
if (
|
|
200
|
+
if (value === undefined && !isNil(this._pk)) {
|
|
164
201
|
let storedValue = modelStoreRegistry.getEntity(ModelClass, this._pk);
|
|
165
202
|
if (storedValue)
|
|
166
203
|
value = storedValue[field];
|
|
167
204
|
}
|
|
205
|
+
// Date/DateTime fields need special handling - convert Date objects to strings for API
|
|
206
|
+
const dateFormats = ["date", "date-time"];
|
|
207
|
+
if (ModelClass.schema &&
|
|
208
|
+
dateFormats.includes(ModelClass.schema.properties[field]?.format) &&
|
|
209
|
+
value instanceof Date) {
|
|
210
|
+
// Let DateParsingHelpers.serializeDate throw if it fails
|
|
211
|
+
return DateParsingHelpers.serializeDate(value, field, ModelClass.schema);
|
|
212
|
+
}
|
|
168
213
|
return value;
|
|
169
214
|
}
|
|
170
215
|
/**
|
|
@@ -192,16 +237,20 @@ export class Model {
|
|
|
192
237
|
async save() {
|
|
193
238
|
const ModelClass = this.constructor;
|
|
194
239
|
const pkField = ModelClass.primaryKeyField;
|
|
195
|
-
const querySet = !this.pk
|
|
240
|
+
const querySet = !this.pk
|
|
241
|
+
? ModelClass.objects.newQuerySet()
|
|
242
|
+
: ModelClass.objects.filter({ [pkField]: this.pk });
|
|
196
243
|
const data = this.serialize();
|
|
197
244
|
let instance;
|
|
198
245
|
if (!this.pk) {
|
|
199
246
|
// Create new instance
|
|
200
|
-
instance = await QueryExecutor.execute(querySet,
|
|
247
|
+
instance = await QueryExecutor.execute(querySet, "create", { data });
|
|
201
248
|
}
|
|
202
249
|
else {
|
|
203
250
|
// Update existing instance
|
|
204
|
-
instance = await QueryExecutor.execute(querySet,
|
|
251
|
+
instance = await QueryExecutor.execute(querySet, "update_instance", {
|
|
252
|
+
data,
|
|
253
|
+
});
|
|
205
254
|
}
|
|
206
255
|
this._pk = instance.pk;
|
|
207
256
|
this._data = {};
|
|
@@ -218,14 +267,14 @@ export class Model {
|
|
|
218
267
|
*/
|
|
219
268
|
async delete() {
|
|
220
269
|
if (!this.pk) {
|
|
221
|
-
throw new Error(
|
|
270
|
+
throw new Error("Cannot delete unsaved instance");
|
|
222
271
|
}
|
|
223
272
|
const ModelClass = this.constructor;
|
|
224
273
|
const pkField = ModelClass.primaryKeyField;
|
|
225
274
|
const querySet = ModelClass.objects.filter({ [pkField]: this.pk });
|
|
226
275
|
// Pass the instance data with primary key as the args
|
|
227
276
|
const args = { [pkField]: this.pk };
|
|
228
|
-
const result = await QueryExecutor.execute(querySet,
|
|
277
|
+
const result = await QueryExecutor.execute(querySet, "delete_instance", args);
|
|
229
278
|
// result -> [deletedCount, { [modelName]: deletedCount }];
|
|
230
279
|
return result;
|
|
231
280
|
}
|
|
@@ -237,13 +286,83 @@ export class Model {
|
|
|
237
286
|
*/
|
|
238
287
|
async refreshFromDb() {
|
|
239
288
|
if (!this.pk) {
|
|
240
|
-
throw new Error(
|
|
289
|
+
throw new Error("Cannot refresh unsaved instance");
|
|
241
290
|
}
|
|
242
291
|
const ModelClass = this.constructor;
|
|
243
|
-
const fresh = await ModelClass.objects.get({
|
|
292
|
+
const fresh = await ModelClass.objects.get({
|
|
293
|
+
[ModelClass.primaryKeyField]: this.pk,
|
|
294
|
+
});
|
|
244
295
|
// clear the current data and fresh data will flow
|
|
245
296
|
this._data = {};
|
|
246
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* Validates the model instance using the same serialize behavior as save()
|
|
300
|
+
* @param {string} validateType - 'create' or 'update' (defaults to auto-detect)
|
|
301
|
+
* @param {boolean} partial - Whether to allow partial validation
|
|
302
|
+
* @returns {Promise<boolean>} Promise that resolves to true if valid, throws error if invalid
|
|
303
|
+
*/
|
|
304
|
+
async validate(validateType = null, partial = false) {
|
|
305
|
+
const ModelClass = this.constructor;
|
|
306
|
+
if (!validateType) {
|
|
307
|
+
validateType = this.pk ? "update" : "create";
|
|
308
|
+
}
|
|
309
|
+
// Validate the validateType parameter
|
|
310
|
+
if (!["update", "create"].includes(validateType)) {
|
|
311
|
+
throw new Error(`Validation type must be 'update' or 'create', not '${validateType}'`);
|
|
312
|
+
}
|
|
313
|
+
// Use the same serialize logic as save()
|
|
314
|
+
const data = this.serialize();
|
|
315
|
+
// Delegate to static method
|
|
316
|
+
return ModelClass.validate(data, validateType, partial);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Static method to validate data without creating an instance
|
|
320
|
+
* @param {Object} data - Data to validate
|
|
321
|
+
* @param {string} validateType - 'create' or 'update'
|
|
322
|
+
* @param {boolean} partial - Whether to allow partial validation
|
|
323
|
+
* @returns {Promise<boolean>} Promise that resolves to true if valid, throws error if invalid
|
|
324
|
+
*/
|
|
325
|
+
static async validate(data, validateType = "create", partial = false) {
|
|
326
|
+
const ModelClass = this;
|
|
327
|
+
// Validate the validateType parameter
|
|
328
|
+
if (!["update", "create"].includes(validateType)) {
|
|
329
|
+
throw new Error(`Validation type must be 'update' or 'create', not '${validateType}'`);
|
|
330
|
+
}
|
|
331
|
+
// Get backend config and check if it exists
|
|
332
|
+
const config = configInstance.getConfig();
|
|
333
|
+
const backend = config.backendConfigs[ModelClass.configKey];
|
|
334
|
+
if (!backend) {
|
|
335
|
+
throw new Error(`No backend configuration found for key: ${ModelClass.configKey}`);
|
|
336
|
+
}
|
|
337
|
+
// Build URL for validate endpoint
|
|
338
|
+
const baseUrl = backend.API_URL.replace(/\/+$/, "");
|
|
339
|
+
const url = `${baseUrl}/${ModelClass.modelName}/validate/`;
|
|
340
|
+
// Prepare headers
|
|
341
|
+
const headers = {
|
|
342
|
+
"Content-Type": "application/json",
|
|
343
|
+
...(backend.getAuthHeaders ? backend.getAuthHeaders() : {}),
|
|
344
|
+
};
|
|
345
|
+
// Make direct API call to validate endpoint
|
|
346
|
+
try {
|
|
347
|
+
const response = await axios.post(url, {
|
|
348
|
+
data: data,
|
|
349
|
+
validate_type: validateType,
|
|
350
|
+
partial: partial,
|
|
351
|
+
}, { headers });
|
|
352
|
+
// Backend returns {"valid": true} on success
|
|
353
|
+
return response.data.valid === true;
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
if (error.response && error.response.data) {
|
|
357
|
+
const parsedError = parseStateZeroError(error.response.data);
|
|
358
|
+
if (Error.captureStackTrace) {
|
|
359
|
+
Error.captureStackTrace(parsedError, ModelClass.validate);
|
|
360
|
+
}
|
|
361
|
+
throw parsedError;
|
|
362
|
+
}
|
|
363
|
+
throw new Error(`Validation failed: ${error.message}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
247
366
|
}
|
|
248
367
|
/**
|
|
249
368
|
* Creates a new Model instance.
|