@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.
@@ -97,32 +97,27 @@ import { loadConfigFromFile } from "../configFileLoader.js";
97
97
  // Fallback Selection for Inquirer Errors
98
98
  // --------------------
99
99
  /**
100
- * Simple fallback selector when inquirer fails
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>} - Selected values
103
+ * @returns {Promise<Array>} - All model values
104
104
  */
105
- async function fallbackSelect(choices, message) {
105
+ async function fallbackSelectAll(choices, message) {
106
106
  console.log(`\n${message}`);
107
- console.log("Interactive selection not available - auto-selecting all available models:");
108
- const selected = [];
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
- // Auto-select items that were checked by default
116
- if (choice.checked) {
117
- selected.push(choice.value);
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(`\nAuto-selected ${selected.length} models for generation.`);
125
- return selected;
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 auto-selection if inquirer fails for any reason
150
- console.warn("Interactive selection failed, falling back to auto-selection:", error.message);
151
- return await fallbackSelect(choices, message);
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 'dotenv';
2
+ import dotenv from "dotenv";
3
3
  dotenv.config();
4
- import yargs from 'yargs';
5
- import { hideBin } from 'yargs/helpers';
6
- import { generateSchema } from './commands/syncModels.js';
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
- .command('sync-models', 'Generate model classes from the openapi schema',
9
- // No CLI options since API_URL and GENERATED_TYPES_DIR are read from .env.
10
- {}, async () => {
11
- await generateSchema({});
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
- .help()
14
- .argv;
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('API_URL must be a valid URL'),
48
- GENERATED_TYPES_DIR: z.string({ required_error: 'GENERATED_TYPES_DIR is required' }),
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
- fileUploadMode: z.enum(['server', 's3']).default('server'),
51
- getAuthHeaders: z.function().optional()
52
- .refine((fn) => fn === undefined || typeof fn === 'function', 'getAuthHeaders must be a function if provided'),
53
- eventInterceptor: z.function().optional()
54
- .refine((fn) => fn === undefined || typeof fn === 'function', 'eventInterceptor must be a function if provided'),
55
- events: z.lazy(() => eventConfigSchema.optional())
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
- 'datetime': schema.datetime_format
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", "datetime"].includes(fieldFormat)) {
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
- 'datetime': schema.datetime_format
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: string;
9
- size: number;
10
- type: string;
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: string;
63
- size: number;
64
- type: string;
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
- return this.uploadResult?.file_url;
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
- import { Manager } from './manager.js';
2
- import { ValidationError } from './errors.js';
3
- import { modelStoreRegistry } from '../../syncEngine/registries/modelStoreRegistry.js';
4
- import { isNil } from 'lodash-es';
5
- import { QueryExecutor } from './queryExecutor';
6
- import { wrapReactiveModel } from '../../reactiveAdaptor.js';
7
- import { DateParsingHelpers } from './dates.js';
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 (isNil(value) && !isNil(this._pk)) {
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 'many-to-many':
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 'one-to-one':
101
- case 'foreign-key':
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 === 'repr' || key === 'type')
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('__')[0];
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 (isNil(value) && !isNil(this._pk)) {
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 ? ModelClass.objects.newQuerySet() : ModelClass.objects.filter({ [pkField]: 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, 'create', { data });
247
+ instance = await QueryExecutor.execute(querySet, "create", { data });
201
248
  }
202
249
  else {
203
250
  // Update existing instance
204
- instance = await QueryExecutor.execute(querySet, 'update_instance', { data });
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('Cannot delete unsaved instance');
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, 'delete_instance', args);
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('Cannot refresh unsaved instance');
289
+ throw new Error("Cannot refresh unsaved instance");
241
290
  }
242
291
  const ModelClass = this.constructor;
243
- const fresh = await ModelClass.objects.get({ [ModelClass.primaryKeyField]: this.pk });
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.