@statezero/core 0.1.1 → 0.1.3-9.1

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.
@@ -94,6 +94,59 @@ import { loadConfigFromFile } from "../configFileLoader.js";
94
94
  * @property {string} model
95
95
  */
96
96
  // --------------------
97
+ // Fallback Selection for Inquirer Errors
98
+ // --------------------
99
+ /**
100
+ * Simple fallback that generates all models when inquirer fails
101
+ * @param {Array} choices - Array of choice objects with {name, value, checked} properties
102
+ * @param {string} message - Selection message
103
+ * @returns {Promise<Array>} - All model values
104
+ */
105
+ async function fallbackSelectAll(choices, message) {
106
+ console.log(`\n${message}`);
107
+ console.log("Interactive selection not available - generating ALL models:");
108
+ const allModels = [];
109
+ for (const choice of choices) {
110
+ // Skip separators (they don't have a 'value' property)
111
+ if (!choice.value) {
112
+ console.log(choice.name); // Print separator text
113
+ continue;
114
+ }
115
+ // Add ALL models, regardless of checked status
116
+ allModels.push(choice.value);
117
+ console.log(` ✓ ${choice.name}`);
118
+ }
119
+ console.log(`\nGenerating ALL ${allModels.length} models.`);
120
+ return allModels;
121
+ }
122
+ /**
123
+ * Model selection with inquirer fallback
124
+ * @param {Array} choices - Array of choice objects
125
+ * @param {string} message - Selection message
126
+ * @returns {Promise<Array>} - Selected model objects
127
+ */
128
+ async function selectModels(choices, message) {
129
+ try {
130
+ // Try to use inquirer first
131
+ const inquirer = (await import("inquirer")).default;
132
+ const { selectedModels } = await inquirer.prompt([
133
+ {
134
+ type: "checkbox",
135
+ name: "selectedModels",
136
+ message,
137
+ choices,
138
+ pageSize: 20,
139
+ },
140
+ ]);
141
+ return selectedModels;
142
+ }
143
+ catch (error) {
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);
147
+ }
148
+ }
149
+ // --------------------
97
150
  // Handlebars Templates & Helpers
98
151
  // --------------------
99
152
  // Updated JS_MODEL_TEMPLATE with getters and setters
@@ -106,7 +159,7 @@ const JS_MODEL_TEMPLATE = `/**
106
159
 
107
160
  import { Model, Manager, QuerySet, getModelClass } from '{{modulePath}}';
108
161
  import { wrapReactiveModel } from '{{modulePath}}';
109
- import schemaData from './{{className}}.schema.json';
162
+ import schemaData from './{{toLowerCase className}}.schema.json';
110
163
 
111
164
  /**
112
165
  * Model-specific QuerySet implementation
@@ -174,22 +227,6 @@ export class {{className}} extends Model {
174
227
  });
175
228
  });
176
229
  }
177
-
178
- /**
179
- * Serialize the model's data.
180
- * Returns a plain object containing the current values of all fields,
181
- * including raw IDs for relationship fields.
182
- * @returns {Object}
183
- */
184
- serialize() {
185
- const data = {};
186
- // Simply assign the current value of each field.
187
- // No special handling or PK extraction for relationships here.
188
- {{#each properties}}
189
- data.{{name}} = this.{{name}};
190
- {{/each}}
191
- return data;
192
- }
193
230
  }
194
231
  `;
195
232
  // Updated TS_DECLARATION_TEMPLATE with improved relationship handling
@@ -446,6 +483,9 @@ Handlebars.registerHelper("ifDefaultProvided", function (defaultValue, options)
446
483
  Handlebars.registerHelper("isRequired", function (required) {
447
484
  return required ? "" : "?";
448
485
  });
486
+ Handlebars.registerHelper("toLowerCase", function (str) {
487
+ return str.toLowerCase();
488
+ });
449
489
  const jsTemplate = Handlebars.compile(JS_MODEL_TEMPLATE);
450
490
  const dtsTemplate = Handlebars.compile(TS_DECLARATION_TEMPLATE);
451
491
  // --------------------
@@ -944,14 +984,12 @@ export declare const FileObject: typeof ${backend.NAME}FileObject;
944
984
  // --------------------
945
985
  // Main Runner: Fetch models and prompt selection
946
986
  // --------------------
947
- // Update main function to use this new approach
948
987
  async function main() {
949
988
  // Load configuration from file (CLI-only or tests) before any other operations.
950
989
  loadConfigFromFile();
951
990
  // Retrieve the validated configuration from the global config singleton.
952
991
  const configData = configInstance.getConfig();
953
992
  const backendConfigs = configData.backendConfigs;
954
- const inquirer = (await import("inquirer")).default;
955
993
  const fetchPromises = Object.keys(backendConfigs).map(async (key) => {
956
994
  const backend = backendConfigs[key];
957
995
  backend.NAME = key;
@@ -966,8 +1004,10 @@ async function main() {
966
1004
  });
967
1005
  const backendModels = await Promise.all(fetchPromises);
968
1006
  const choices = [];
1007
+ // Create a simple separator object for environments where inquirer.Separator isn't available
1008
+ const createSeparator = (text) => ({ name: text, value: null });
969
1009
  for (const { backend, models } of backendModels) {
970
- choices.push(new inquirer.Separator(`\n=== ${backend.NAME} ===\n`));
1010
+ choices.push(createSeparator(`\n=== ${backend.NAME} ===\n`));
971
1011
  for (const model of models) {
972
1012
  choices.push({
973
1013
  name: model,
@@ -980,15 +1020,7 @@ async function main() {
980
1020
  console.log("No models to synchronise");
981
1021
  process.exit(0);
982
1022
  }
983
- const { selectedModels } = await inquirer.prompt([
984
- {
985
- type: "checkbox",
986
- name: "selectedModels",
987
- message: "Select models to synchronise:",
988
- choices,
989
- pageSize: 20,
990
- },
991
- ]);
1023
+ const selectedModels = await selectModels(choices, "Select models to synchronise:");
992
1024
  if (!selectedModels || selectedModels.length === 0) {
993
1025
  console.log("No models selected. Exiting.");
994
1026
  process.exit(0);
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.