@statezero/core 0.1.28 ā 0.1.29
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/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/index.js +18 -10
- package/dist/config.js +18 -10
- package/dist/syncEngine/stores/querysetStore.js +12 -10
- package/dist/syncEngine/sync.d.ts +5 -0
- package/dist/syncEngine/sync.js +56 -0
- package/package.json +5 -3
- package/readme.md +1 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { generateSchema } from "./syncModels.js";
|
|
2
|
+
import { generateActions } from "./syncActions.js";
|
|
3
|
+
/**
|
|
4
|
+
* Executes a full synchronization, running sync-models first,
|
|
5
|
+
* followed by sync-actions.
|
|
6
|
+
* @param {object} args - Command-line arguments, passed from yargs.
|
|
7
|
+
*/
|
|
8
|
+
export async function sync(args = {}) {
|
|
9
|
+
try {
|
|
10
|
+
console.log("š Starting full synchronization...");
|
|
11
|
+
// --- Step 1: Synchronize Models ---
|
|
12
|
+
console.log("\n----- Running Model Synchronization -----");
|
|
13
|
+
// Pass args down, as generateSchema expects an object
|
|
14
|
+
await generateSchema(args);
|
|
15
|
+
console.log("----- Model Synchronization Complete -----\n");
|
|
16
|
+
// --- Step 2: Synchronize Actions ---
|
|
17
|
+
console.log("----- Running Action Synchronization -----");
|
|
18
|
+
// generateActions does not require args based on its definition
|
|
19
|
+
await generateActions();
|
|
20
|
+
console.log("----- Action Synchronization Complete -----\n");
|
|
21
|
+
console.log("ā
Full synchronization finished successfully!");
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
console.error("\nā A critical error occurred during full synchronization:", error.message);
|
|
25
|
+
if (process.env.DEBUG) {
|
|
26
|
+
console.error("Stack trace:", error.stack);
|
|
27
|
+
}
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI entry point.
|
|
3
|
+
*/
|
|
4
|
+
export function generateActions(): Promise<void>;
|
|
5
|
+
export type ActionProperty = {
|
|
6
|
+
type: string;
|
|
7
|
+
format?: string | undefined;
|
|
8
|
+
required?: boolean | undefined;
|
|
9
|
+
nullable?: boolean | undefined;
|
|
10
|
+
default?: any;
|
|
11
|
+
choices?: string[] | undefined;
|
|
12
|
+
min_length?: number | undefined;
|
|
13
|
+
max_length?: number | undefined;
|
|
14
|
+
min_value?: number | undefined;
|
|
15
|
+
max_value?: number | undefined;
|
|
16
|
+
items?: ActionProperty | undefined;
|
|
17
|
+
properties?: {
|
|
18
|
+
[x: string]: ActionProperty;
|
|
19
|
+
} | undefined;
|
|
20
|
+
description?: string | undefined;
|
|
21
|
+
};
|
|
22
|
+
export type ActionDefinition = {
|
|
23
|
+
action_name: string;
|
|
24
|
+
title: string;
|
|
25
|
+
class_name: string;
|
|
26
|
+
/**
|
|
27
|
+
* - The application group for the action.
|
|
28
|
+
*/
|
|
29
|
+
app: string | null;
|
|
30
|
+
/**
|
|
31
|
+
* - The action's documentation string.
|
|
32
|
+
*/
|
|
33
|
+
docstring: string | null;
|
|
34
|
+
input_properties: {
|
|
35
|
+
[x: string]: ActionProperty;
|
|
36
|
+
};
|
|
37
|
+
response_properties: {
|
|
38
|
+
[x: string]: ActionProperty;
|
|
39
|
+
};
|
|
40
|
+
permissions: string[];
|
|
41
|
+
};
|
|
42
|
+
export type BackendConfig = {
|
|
43
|
+
NAME: string;
|
|
44
|
+
API_URL: string;
|
|
45
|
+
GENERATED_ACTIONS_DIR: string;
|
|
46
|
+
};
|
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import cliProgress from "cli-progress";
|
|
5
|
+
import Handlebars from "handlebars";
|
|
6
|
+
import _ from "lodash-es";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { configInstance } from "../../config.js";
|
|
9
|
+
import { loadConfigFromFile } from "../configFileLoader.js";
|
|
10
|
+
// ================================================================================================
|
|
11
|
+
// JSDOC TYPE DEFINITIONS
|
|
12
|
+
// ================================================================================================
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} ActionProperty
|
|
15
|
+
* @property {string} type
|
|
16
|
+
* @property {string} [format]
|
|
17
|
+
* @property {boolean} [required]
|
|
18
|
+
* @property {boolean} [nullable]
|
|
19
|
+
* @property {any} [default]
|
|
20
|
+
* @property {string[]} [choices]
|
|
21
|
+
* @property {number} [min_length]
|
|
22
|
+
* @property {number} [max_length]
|
|
23
|
+
* @property {number} [min_value]
|
|
24
|
+
* @property {number} [max_value]
|
|
25
|
+
* @property {ActionProperty} [items]
|
|
26
|
+
* @property {Object.<string, ActionProperty>} [properties]
|
|
27
|
+
* @property {string} [description]
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Object} ActionDefinition
|
|
31
|
+
* @property {string} action_name
|
|
32
|
+
* @property {string} title
|
|
33
|
+
* @property {string} class_name
|
|
34
|
+
* @property {string | null} app - The application group for the action.
|
|
35
|
+
* @property {string | null} docstring - The action's documentation string.
|
|
36
|
+
* @property {Object.<string, ActionProperty>} input_properties
|
|
37
|
+
* @property {Object.<string, ActionProperty>} response_properties
|
|
38
|
+
* @property {string[]} permissions
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {Object} BackendConfig
|
|
42
|
+
* @property {string} NAME
|
|
43
|
+
* @property {string} API_URL
|
|
44
|
+
* @property {string} GENERATED_ACTIONS_DIR
|
|
45
|
+
*/
|
|
46
|
+
// ================================================================================================
|
|
47
|
+
// CLI INTERACTIVITY & FALLBACKS
|
|
48
|
+
// ================================================================================================
|
|
49
|
+
async function fallbackSelectAll(choices, message) {
|
|
50
|
+
console.log(`\n${message}`);
|
|
51
|
+
console.log("Interactive selection not available - generating ALL actions:");
|
|
52
|
+
const allActions = [];
|
|
53
|
+
for (const choice of choices) {
|
|
54
|
+
if (!choice.value) {
|
|
55
|
+
console.log(choice.name);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
allActions.push(choice.value);
|
|
59
|
+
console.log(` ā ${choice.name}`);
|
|
60
|
+
}
|
|
61
|
+
console.log(`\nGenerating ALL ${allActions.length} actions.`);
|
|
62
|
+
return allActions;
|
|
63
|
+
}
|
|
64
|
+
async function selectActions(choices, message) {
|
|
65
|
+
try {
|
|
66
|
+
const inquirer = (await import("inquirer")).default;
|
|
67
|
+
const { selectedActions } = await inquirer.prompt([
|
|
68
|
+
{
|
|
69
|
+
type: "checkbox",
|
|
70
|
+
name: "selectedActions",
|
|
71
|
+
message,
|
|
72
|
+
choices,
|
|
73
|
+
pageSize: 20,
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
76
|
+
return selectedActions;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.warn("Interactive selection failed, generating all available actions:", error.message);
|
|
80
|
+
return await fallbackSelectAll(choices, message);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// ================================================================================================
|
|
84
|
+
// HANDLEBARS TEMPLATES
|
|
85
|
+
// ================================================================================================
|
|
86
|
+
// Register a helper to format multi-line docstrings for JSDoc.
|
|
87
|
+
Handlebars.registerHelper("formatJsDoc", function (text) {
|
|
88
|
+
if (!text)
|
|
89
|
+
return "";
|
|
90
|
+
return text
|
|
91
|
+
.split("\n")
|
|
92
|
+
.map((line) => ` * ${line}`)
|
|
93
|
+
.join("\n");
|
|
94
|
+
});
|
|
95
|
+
const JS_ACTION_TEMPLATE = `/**
|
|
96
|
+
* This file was auto-generated. Do not make direct changes to the file.
|
|
97
|
+
* Action: {{title}}
|
|
98
|
+
* App: {{app}}
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
import axios from 'axios';
|
|
102
|
+
import { z } from 'zod';
|
|
103
|
+
import { configInstance } from '{{modulePath}}';
|
|
104
|
+
import { parseStateZeroError } from '{{modulePath}}/flavours/django/errors.js';
|
|
105
|
+
|
|
106
|
+
{{#if inputSchemaString}}
|
|
107
|
+
/**
|
|
108
|
+
* Zod schema for the input of {{functionName}}.
|
|
109
|
+
* NOTE: This is an object schema for validating the data payload.
|
|
110
|
+
*/
|
|
111
|
+
export const {{functionName}}InputSchema = z.object({ {{{inputSchemaString}}} });
|
|
112
|
+
{{/if}}
|
|
113
|
+
|
|
114
|
+
{{#if responseSchemaString}}
|
|
115
|
+
/**
|
|
116
|
+
* Zod schema for the response of {{functionName}}.
|
|
117
|
+
*/
|
|
118
|
+
export const {{functionName}}ResponseSchema = z.object({ {{{responseSchemaString}}} });
|
|
119
|
+
{{/if}}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
{{#if docstring}}
|
|
123
|
+
{{{formatJsDoc docstring}}}
|
|
124
|
+
*
|
|
125
|
+
{{else}}
|
|
126
|
+
* {{title}}
|
|
127
|
+
{{/if}}
|
|
128
|
+
{{#each tsDocParams}}
|
|
129
|
+
* @param {{{this.type}}} {{this.name}} - {{this.description}}
|
|
130
|
+
{{/each}}
|
|
131
|
+
* @param {Object} [axiosOverrides] - Allows overriding Axios request parameters.
|
|
132
|
+
* @returns {Promise<Object>} A promise that resolves with the action's result.
|
|
133
|
+
*/
|
|
134
|
+
export async function {{functionName}}({{{jsFunctionParams}}}) {
|
|
135
|
+
// Construct the data payload from the function arguments
|
|
136
|
+
{{#if payloadProperties}}
|
|
137
|
+
const payload = {
|
|
138
|
+
{{{payloadProperties}}}
|
|
139
|
+
};
|
|
140
|
+
{{else}}
|
|
141
|
+
const payload = {};
|
|
142
|
+
{{/if}}
|
|
143
|
+
|
|
144
|
+
const config = configInstance.getConfig();
|
|
145
|
+
const backend = config.backendConfigs['{{configKey}}'];
|
|
146
|
+
|
|
147
|
+
if (!backend) {
|
|
148
|
+
throw new Error(\`No backend configuration found for key: {{configKey}}\`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const baseUrl = backend.API_URL.replace(/\\/+$/, '');
|
|
152
|
+
const actionUrl = \`\${baseUrl}/actions/{{actionName}}/\`;
|
|
153
|
+
const headers = backend.getAuthHeaders ? backend.getAuthHeaders() : {};
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const response = await axios.post(actionUrl, payload, {
|
|
157
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
158
|
+
...axiosOverrides,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
{{#if responseSchemaString}}
|
|
162
|
+
return {{functionName}}ResponseSchema.parse(response.data);
|
|
163
|
+
{{else}}
|
|
164
|
+
return response.data;
|
|
165
|
+
{{/if}}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (error instanceof z.ZodError) {
|
|
168
|
+
throw new Error(\`{{title}} failed: Invalid response from server. Details: \${error.message}\`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (error.response && error.response.data) {
|
|
172
|
+
const parsedError = parseStateZeroError(error.response.data);
|
|
173
|
+
|
|
174
|
+
if (Error.captureStackTrace) {
|
|
175
|
+
Error.captureStackTrace(parsedError, {{functionName}});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
throw parsedError;
|
|
179
|
+
} else if (error.request) {
|
|
180
|
+
throw new Error(\`{{title}} failed: No response received from server.\`);
|
|
181
|
+
} else {
|
|
182
|
+
throw new Error(\`{{title}} failed: \${error.message}\`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export default {{functionName}};
|
|
188
|
+
|
|
189
|
+
{{functionName}}.actionName = '{{actionName}}';
|
|
190
|
+
{{functionName}}.title = '{{title}}';
|
|
191
|
+
{{functionName}}.app = {{#if app}}'{{app}}'{{else}}null{{/if}};
|
|
192
|
+
{{functionName}}.permissions = [{{#each permissions}}'{{this}}'{{#unless @last}}, {{/unless}}{{/each}}];
|
|
193
|
+
{{functionName}}.configKey = '{{configKey}}';
|
|
194
|
+
`;
|
|
195
|
+
const TS_ACTION_DECLARATION_TEMPLATE = `/**
|
|
196
|
+
* This file was auto-generated. Do not make direct changes to the file.
|
|
197
|
+
* Action: {{title}}
|
|
198
|
+
* App: {{app}}
|
|
199
|
+
*/
|
|
200
|
+
import { z } from 'zod';
|
|
201
|
+
import { AxiosRequestConfig } from 'axios';
|
|
202
|
+
|
|
203
|
+
{{#if inputTsSchemaString}}
|
|
204
|
+
export type {{functionName}}Input = { {{inputTsSchemaString}} };
|
|
205
|
+
{{/if}}
|
|
206
|
+
|
|
207
|
+
{{#if responseTsSchemaString}}
|
|
208
|
+
export type {{functionName}}Response = { {{responseTsSchemaString}} };
|
|
209
|
+
{{else}}
|
|
210
|
+
export type {{functionName}}Response = any;
|
|
211
|
+
{{/if}}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
{{#if docstring}}
|
|
215
|
+
{{{formatJsDoc docstring}}}
|
|
216
|
+
*
|
|
217
|
+
{{else}}
|
|
218
|
+
* {{title}}
|
|
219
|
+
{{/if}}
|
|
220
|
+
{{#each tsDocParams}}
|
|
221
|
+
* @param {{{this.type}}} {{this.name}} - {{this.description}}
|
|
222
|
+
{{/each}}
|
|
223
|
+
* @param {AxiosRequestConfig} [axiosOverrides] - Allows overriding Axios request parameters.
|
|
224
|
+
* @returns {Promise<{{functionName}}Response>} A promise that resolves with the action's result.
|
|
225
|
+
*/
|
|
226
|
+
export declare function {{functionName}}(
|
|
227
|
+
{{{tsFunctionParams}}}
|
|
228
|
+
): Promise<{{functionName}}Response>;
|
|
229
|
+
|
|
230
|
+
export default {{functionName}};
|
|
231
|
+
|
|
232
|
+
export declare namespace {{functionName}} {
|
|
233
|
+
export const actionName: string;
|
|
234
|
+
export const title: string;
|
|
235
|
+
export const app: string | null;
|
|
236
|
+
export const permissions: string[];
|
|
237
|
+
export const configKey: string;
|
|
238
|
+
}
|
|
239
|
+
`;
|
|
240
|
+
const jsActionTemplate = Handlebars.compile(JS_ACTION_TEMPLATE);
|
|
241
|
+
const dtsActionTemplate = Handlebars.compile(TS_ACTION_DECLARATION_TEMPLATE);
|
|
242
|
+
// ================================================================================================
|
|
243
|
+
// SCHEMA TRANSLATION HELPERS
|
|
244
|
+
// ================================================================================================
|
|
245
|
+
function generateZodSchemaForProperty(prop) {
|
|
246
|
+
let zodString;
|
|
247
|
+
if (prop.choices && prop.choices.length > 0) {
|
|
248
|
+
zodString = `z.enum(${JSON.stringify(prop.choices)})`;
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
switch (prop.type) {
|
|
252
|
+
case "string":
|
|
253
|
+
zodString = prop.max_digits
|
|
254
|
+
? 'z.string().regex(/^-?\\d+(\\.\\d+)?$/, "Must be a numeric string")'
|
|
255
|
+
: "z.string()";
|
|
256
|
+
break;
|
|
257
|
+
case "integer":
|
|
258
|
+
zodString = "z.number().int()";
|
|
259
|
+
break;
|
|
260
|
+
case "number":
|
|
261
|
+
zodString = "z.number()";
|
|
262
|
+
break;
|
|
263
|
+
case "boolean":
|
|
264
|
+
zodString = "z.boolean()";
|
|
265
|
+
break;
|
|
266
|
+
case "array":
|
|
267
|
+
const itemSchema = prop.items
|
|
268
|
+
? generateZodSchemaForProperty(prop.items)
|
|
269
|
+
: "z.any()";
|
|
270
|
+
zodString = `z.array(${itemSchema})`;
|
|
271
|
+
break;
|
|
272
|
+
case "object":
|
|
273
|
+
if (prop.properties) {
|
|
274
|
+
const nestedProps = Object.entries(prop.properties)
|
|
275
|
+
.map(([key, value]) => `${key}: ${generateZodSchemaForProperty(value)}`)
|
|
276
|
+
.join(", ");
|
|
277
|
+
zodString = `z.object({ ${nestedProps} })`;
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
zodString = "z.record(z.any())";
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
283
|
+
default:
|
|
284
|
+
zodString = "z.any()";
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (prop.format) {
|
|
289
|
+
switch (prop.format) {
|
|
290
|
+
case "email":
|
|
291
|
+
zodString += ".email()";
|
|
292
|
+
break;
|
|
293
|
+
case "uri":
|
|
294
|
+
zodString += ".url()";
|
|
295
|
+
break;
|
|
296
|
+
case "uuid":
|
|
297
|
+
zodString += ".uuid()";
|
|
298
|
+
break;
|
|
299
|
+
case "date-time":
|
|
300
|
+
zodString +=
|
|
301
|
+
'.datetime({ message: "Invalid ISO 8601 datetime string" })';
|
|
302
|
+
break;
|
|
303
|
+
case "date":
|
|
304
|
+
zodString +=
|
|
305
|
+
'.regex(/^\\d{4}-\\d{2}-\\d{2}$/, "Must be in YYYY-MM-DD format")';
|
|
306
|
+
break;
|
|
307
|
+
case "time":
|
|
308
|
+
zodString +=
|
|
309
|
+
'.regex(/^\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?$/, "Must be in HH:MM:SS format")';
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (prop.min_length != null)
|
|
314
|
+
zodString += `.min(${prop.min_length})`;
|
|
315
|
+
if (prop.max_length != null)
|
|
316
|
+
zodString += `.max(${prop.max_length})`;
|
|
317
|
+
if (prop.min_value != null)
|
|
318
|
+
zodString += `.min(${prop.min_value})`;
|
|
319
|
+
if (prop.max_value != null)
|
|
320
|
+
zodString += `.max(${prop.max_value})`;
|
|
321
|
+
if (prop.nullable)
|
|
322
|
+
zodString += ".nullable()";
|
|
323
|
+
if (!prop.required)
|
|
324
|
+
zodString += ".optional()";
|
|
325
|
+
if (prop.default !== undefined && prop.default !== null) {
|
|
326
|
+
zodString += `.default(${JSON.stringify(prop.default)})`;
|
|
327
|
+
}
|
|
328
|
+
return zodString;
|
|
329
|
+
}
|
|
330
|
+
function generateTsTypeForProperty(prop) {
|
|
331
|
+
if (prop.choices && prop.choices.length > 0) {
|
|
332
|
+
return prop.choices
|
|
333
|
+
.map((c) => `'${String(c).replace(/'/g, "\\'")}'`)
|
|
334
|
+
.join(" | ");
|
|
335
|
+
}
|
|
336
|
+
let tsType;
|
|
337
|
+
switch (prop.type) {
|
|
338
|
+
case "string":
|
|
339
|
+
tsType = "string";
|
|
340
|
+
break;
|
|
341
|
+
case "integer":
|
|
342
|
+
case "number":
|
|
343
|
+
tsType = "number";
|
|
344
|
+
break;
|
|
345
|
+
case "boolean":
|
|
346
|
+
tsType = "boolean";
|
|
347
|
+
break;
|
|
348
|
+
case "array":
|
|
349
|
+
tsType = prop.items
|
|
350
|
+
? `Array<${generateTsTypeForProperty(prop.items)}>`
|
|
351
|
+
: "any[]";
|
|
352
|
+
break;
|
|
353
|
+
case "object":
|
|
354
|
+
tsType = "Record<string, any>";
|
|
355
|
+
break;
|
|
356
|
+
default:
|
|
357
|
+
tsType = "any";
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
if (prop.nullable) {
|
|
361
|
+
tsType = `${tsType} | null`;
|
|
362
|
+
}
|
|
363
|
+
return tsType;
|
|
364
|
+
}
|
|
365
|
+
// ================================================================================================
|
|
366
|
+
// DATA PREPARATION & FILE GENERATION
|
|
367
|
+
// ================================================================================================
|
|
368
|
+
function prepareActionTemplateData(modulePath, functionName, actionName, actionDefinition, configKey) {
|
|
369
|
+
const inputProps = actionDefinition.input_properties || {};
|
|
370
|
+
const responseProps = actionDefinition.response_properties || {};
|
|
371
|
+
const processProperties = (properties) => {
|
|
372
|
+
const propertyEntries = Object.entries(properties);
|
|
373
|
+
if (propertyEntries.length === 0)
|
|
374
|
+
return "";
|
|
375
|
+
const propertyStrings = propertyEntries
|
|
376
|
+
.map(([name, prop]) => ` ${name}: ${generateZodSchemaForProperty(prop)}`)
|
|
377
|
+
.join(",\n");
|
|
378
|
+
return `\n${propertyStrings}\n`;
|
|
379
|
+
};
|
|
380
|
+
// For TypeScript declarations, we need a different format
|
|
381
|
+
const processPropertiesForTS = (properties) => {
|
|
382
|
+
const propertyEntries = Object.entries(properties);
|
|
383
|
+
if (propertyEntries.length === 0)
|
|
384
|
+
return "";
|
|
385
|
+
const propertyStrings = propertyEntries
|
|
386
|
+
.map(([name, prop]) => `${name}: ${generateTsTypeForProperty(prop)}`)
|
|
387
|
+
.join(", ");
|
|
388
|
+
return propertyStrings;
|
|
389
|
+
};
|
|
390
|
+
const inputSchemaString = processProperties(inputProps);
|
|
391
|
+
const responseSchemaString = processProperties(responseProps);
|
|
392
|
+
// Generate TypeScript type strings for the .d.ts file
|
|
393
|
+
const inputTsSchemaString = processPropertiesForTS(inputProps);
|
|
394
|
+
const responseTsSchemaString = processPropertiesForTS(responseProps);
|
|
395
|
+
const requiredParams = [], optionalParams = [];
|
|
396
|
+
Object.entries(inputProps).forEach(([name, prop]) => {
|
|
397
|
+
(prop.required ? requiredParams : optionalParams).push({ name, prop });
|
|
398
|
+
});
|
|
399
|
+
const allParams = [...requiredParams, ...optionalParams];
|
|
400
|
+
const jsParams = allParams.map(({ name, prop }) => !prop.required && prop.default !== undefined && prop.default !== null
|
|
401
|
+
? `${name} = ${JSON.stringify(prop.default)}`
|
|
402
|
+
: name);
|
|
403
|
+
jsParams.push("axiosOverrides = {}");
|
|
404
|
+
const tsParams = allParams.map(({ name, prop }) => {
|
|
405
|
+
const type = generateTsTypeForProperty(prop);
|
|
406
|
+
const optionalMarker = prop.required ? "" : "?";
|
|
407
|
+
return `${name}${optionalMarker}: ${type}`;
|
|
408
|
+
});
|
|
409
|
+
tsParams.push(`axiosOverrides?: AxiosRequestConfig`);
|
|
410
|
+
const tsDocParams = allParams.map(({ name, prop }) => ({
|
|
411
|
+
name,
|
|
412
|
+
type: generateTsTypeForProperty(prop),
|
|
413
|
+
description: prop.description || `The ${name} parameter.`,
|
|
414
|
+
}));
|
|
415
|
+
return {
|
|
416
|
+
modulePath,
|
|
417
|
+
functionName,
|
|
418
|
+
actionName,
|
|
419
|
+
title: actionDefinition.title || _.startCase(functionName),
|
|
420
|
+
app: actionDefinition.app,
|
|
421
|
+
docstring: actionDefinition.docstring,
|
|
422
|
+
permissions: actionDefinition.permissions || [],
|
|
423
|
+
configKey,
|
|
424
|
+
inputSchemaString: inputSchemaString ? inputSchemaString.trim() : null,
|
|
425
|
+
responseSchemaString: responseSchemaString
|
|
426
|
+
? responseSchemaString.trim()
|
|
427
|
+
: null,
|
|
428
|
+
inputTsSchemaString: inputTsSchemaString || null,
|
|
429
|
+
responseTsSchemaString: responseTsSchemaString || null,
|
|
430
|
+
jsFunctionParams: jsParams.join(", "),
|
|
431
|
+
tsFunctionParams: tsParams.join(",\n "),
|
|
432
|
+
payloadProperties: Object.keys(inputProps).length > 0
|
|
433
|
+
? Object.keys(inputProps).join(",\n ")
|
|
434
|
+
: null,
|
|
435
|
+
tsDocParams: tsDocParams,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
async function generateActionFile(backend, actionName, actionDefinition) {
|
|
439
|
+
const functionName = _.camelCase(actionName);
|
|
440
|
+
const modulePath = process.env.NODE_ENV === "test" ? "../../../src" : "@statezero/core";
|
|
441
|
+
const appName = (actionDefinition.app || "general").toLowerCase();
|
|
442
|
+
const outDir = path.join(backend.GENERATED_ACTIONS_DIR, appName);
|
|
443
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
444
|
+
const templateData = prepareActionTemplateData(modulePath, functionName, actionName, actionDefinition, backend.NAME);
|
|
445
|
+
const fileName = _.kebabCase(actionName);
|
|
446
|
+
const jsFilePath = path.join(outDir, `${fileName}.js`);
|
|
447
|
+
await fs.writeFile(jsFilePath, jsActionTemplate(templateData));
|
|
448
|
+
const dtsFilePath = path.join(outDir, `${fileName}.d.ts`);
|
|
449
|
+
await fs.writeFile(dtsFilePath, dtsActionTemplate(templateData));
|
|
450
|
+
const relativePath = path
|
|
451
|
+
.relative(backend.GENERATED_ACTIONS_DIR, jsFilePath)
|
|
452
|
+
.replace(/\\/g, "/")
|
|
453
|
+
.replace(/\.js$/, "");
|
|
454
|
+
return {
|
|
455
|
+
action: actionName,
|
|
456
|
+
relativePath,
|
|
457
|
+
functionName,
|
|
458
|
+
backend: backend.NAME,
|
|
459
|
+
appName,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
async function generateActionRegistry(generatedFiles, backendConfigs) {
|
|
463
|
+
const registryByBackend = {};
|
|
464
|
+
const allImports = new Set();
|
|
465
|
+
for (const file of generatedFiles) {
|
|
466
|
+
const backendKey = file.backend;
|
|
467
|
+
if (!backendKey)
|
|
468
|
+
continue;
|
|
469
|
+
registryByBackend[backendKey] = registryByBackend[backendKey] || {
|
|
470
|
+
actions: {},
|
|
471
|
+
};
|
|
472
|
+
const functionName = file.functionName;
|
|
473
|
+
const actionsDir = backendConfigs[backendKey].GENERATED_ACTIONS_DIR;
|
|
474
|
+
const importPath = path
|
|
475
|
+
.relative(process.cwd(), path.join(actionsDir, `${file.relativePath}.js`))
|
|
476
|
+
.replace(/\\/g, "/");
|
|
477
|
+
const importStatement = `import { ${functionName} } from './${importPath}';`;
|
|
478
|
+
allImports.add(importStatement);
|
|
479
|
+
registryByBackend[backendKey].actions[file.action] = functionName;
|
|
480
|
+
}
|
|
481
|
+
let registryContent = `/**
|
|
482
|
+
* This file was auto-generated. Do not make direct changes to the file.
|
|
483
|
+
* It provides a registry of all generated actions.
|
|
484
|
+
*/\n\n`;
|
|
485
|
+
registryContent += Array.from(allImports).sort().join("\n") + "\n\n";
|
|
486
|
+
registryContent += `export const ACTION_REGISTRY = {\n`;
|
|
487
|
+
Object.entries(registryByBackend).forEach(([backendKey, data], index, arr) => {
|
|
488
|
+
registryContent += ` '${backendKey}': {\n`;
|
|
489
|
+
const actionEntries = Object.entries(data.actions);
|
|
490
|
+
actionEntries.forEach(([actionName, funcName], idx, actionsArr) => {
|
|
491
|
+
registryContent += ` '${actionName}': ${funcName}${idx < actionsArr.length - 1 ? "," : ""}\n`;
|
|
492
|
+
});
|
|
493
|
+
registryContent += ` }${index < arr.length - 1 ? "," : ""}\n`;
|
|
494
|
+
});
|
|
495
|
+
registryContent += `};\n\n`;
|
|
496
|
+
registryContent += `export function getAction(actionName, configKey) {
|
|
497
|
+
const action = ACTION_REGISTRY[configKey]?.[actionName];
|
|
498
|
+
if (!action) {
|
|
499
|
+
console.warn(\`Action '\${actionName}' not found for config key '\${configKey}'.\`);
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
return action;
|
|
503
|
+
}\n`;
|
|
504
|
+
const registryFilePath = path.join(process.cwd(), "action-registry.js");
|
|
505
|
+
await fs.writeFile(registryFilePath, registryContent);
|
|
506
|
+
console.log(`\n⨠Generated action registry at ${registryFilePath}`);
|
|
507
|
+
}
|
|
508
|
+
async function generateAppLevelIndexFiles(generatedFiles, backendConfigs) {
|
|
509
|
+
const filesByBackend = _.groupBy(generatedFiles, "backend");
|
|
510
|
+
const indexTemplate = Handlebars.compile(`{{#each files}}
|
|
511
|
+
export * from '{{this.relativePath}}';
|
|
512
|
+
{{/each}}`);
|
|
513
|
+
for (const backendName in filesByBackend) {
|
|
514
|
+
const backendFiles = filesByBackend[backendName];
|
|
515
|
+
const backendConfig = backendConfigs[backendName];
|
|
516
|
+
const rootActionsDir = backendConfig.GENERATED_ACTIONS_DIR;
|
|
517
|
+
const rootExports = [];
|
|
518
|
+
const filesByApp = _.groupBy(backendFiles, "appName");
|
|
519
|
+
for (const appName in filesByApp) {
|
|
520
|
+
const appFiles = filesByApp[appName];
|
|
521
|
+
const appDir = path.join(rootActionsDir, appName);
|
|
522
|
+
const appIndexExports = appFiles.map((file) => {
|
|
523
|
+
const relativePathToAppDir = `./${path.basename(file.relativePath)}`;
|
|
524
|
+
return { ...file, relativePath: relativePathToAppDir };
|
|
525
|
+
});
|
|
526
|
+
const indexContent = indexTemplate({ files: appIndexExports }).trim();
|
|
527
|
+
await fs.writeFile(path.join(appDir, "index.js"), indexContent);
|
|
528
|
+
await fs.writeFile(path.join(appDir, "index.d.ts"), indexContent);
|
|
529
|
+
rootExports.push(`export * from './${appName}';`);
|
|
530
|
+
}
|
|
531
|
+
const rootIndexContent = rootExports.sort().join("\n");
|
|
532
|
+
await fs.writeFile(path.join(rootActionsDir, "index.js"), rootIndexContent);
|
|
533
|
+
await fs.writeFile(path.join(rootActionsDir, "index.d.ts"), rootIndexContent);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// ================================================================================================
|
|
537
|
+
// MAIN SCRIPT RUNNER
|
|
538
|
+
// ================================================================================================
|
|
539
|
+
async function main() {
|
|
540
|
+
loadConfigFromFile();
|
|
541
|
+
const configData = configInstance.getConfig();
|
|
542
|
+
const backendConfigs = configData.backendConfigs;
|
|
543
|
+
for (const [key, backend] of Object.entries(backendConfigs)) {
|
|
544
|
+
if (!backend.GENERATED_ACTIONS_DIR) {
|
|
545
|
+
console.error(`ā Backend '${key}' is missing the GENERATED_ACTIONS_DIR configuration.`);
|
|
546
|
+
process.exit(1);
|
|
547
|
+
}
|
|
548
|
+
backend.NAME = key;
|
|
549
|
+
}
|
|
550
|
+
console.log("Fetching action schemas from backends...");
|
|
551
|
+
const fetchPromises = Object.values(backendConfigs).map(async (backend) => {
|
|
552
|
+
try {
|
|
553
|
+
const response = await axios.get(`${backend.API_URL}/actions-schema/`);
|
|
554
|
+
return { backend, actions: response.data.actions || {} };
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
console.error(`ā Error fetching actions from ${backend.NAME}: ${error.message}`);
|
|
558
|
+
return { backend, actions: {} };
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
const backendActions = await Promise.all(fetchPromises);
|
|
562
|
+
const choices = [];
|
|
563
|
+
// Reverted to group choices by backend for the CLI prompt
|
|
564
|
+
for (const { backend, actions } of backendActions) {
|
|
565
|
+
const actionNames = Object.keys(actions);
|
|
566
|
+
if (actionNames.length > 0) {
|
|
567
|
+
choices.push({ name: `\n=== ${backend.NAME} ===\n`, disabled: true });
|
|
568
|
+
for (const actionName of actionNames.sort()) {
|
|
569
|
+
const definition = actions[actionName];
|
|
570
|
+
choices.push({
|
|
571
|
+
name: ` ${definition.title || _.startCase(actionName)}`,
|
|
572
|
+
value: { backend, action: actionName, definition },
|
|
573
|
+
checked: true,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (choices.length === 0) {
|
|
579
|
+
console.log("No actions found to synchronize.");
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const selectedActions = await selectActions(choices, "Select actions to generate:");
|
|
583
|
+
if (!selectedActions || selectedActions.length === 0) {
|
|
584
|
+
console.log("No actions selected. Exiting.");
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
console.log("\nāļø Generating actions...");
|
|
588
|
+
const progressBar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
|
|
589
|
+
progressBar.start(selectedActions.length, 0);
|
|
590
|
+
const allGeneratedFiles = [];
|
|
591
|
+
for (const item of selectedActions) {
|
|
592
|
+
try {
|
|
593
|
+
const result = await generateActionFile(item.backend, item.action, item.definition);
|
|
594
|
+
allGeneratedFiles.push(result);
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
progressBar.stop();
|
|
598
|
+
console.error(`\nā Error generating action ${item.action}: ${error.message}`);
|
|
599
|
+
}
|
|
600
|
+
progressBar.increment();
|
|
601
|
+
}
|
|
602
|
+
progressBar.stop();
|
|
603
|
+
await generateAppLevelIndexFiles(allGeneratedFiles, backendConfigs);
|
|
604
|
+
await generateActionRegistry(allGeneratedFiles, backendConfigs);
|
|
605
|
+
console.log(`\n⨠Generated ${allGeneratedFiles.length} actions successfully.`);
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* CLI entry point.
|
|
609
|
+
*/
|
|
610
|
+
export async function generateActions() {
|
|
611
|
+
try {
|
|
612
|
+
console.log("š Starting action synchronization...");
|
|
613
|
+
await main();
|
|
614
|
+
console.log("\nā
Action synchronization completed!");
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
console.error("\nā Action synchronization failed:", error.message);
|
|
618
|
+
if (process.env.DEBUG) {
|
|
619
|
+
console.error("Stack trace:", error.stack);
|
|
620
|
+
}
|
|
621
|
+
process.exit(1);
|
|
622
|
+
}
|
|
623
|
+
}
|
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.js
CHANGED
|
@@ -44,16 +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
|
-
fileRootURL: z.string().url(
|
|
51
|
-
fileUploadMode: z.enum([
|
|
52
|
-
getAuthHeaders: z
|
|
53
|
-
.
|
|
54
|
-
|
|
55
|
-
.refine((fn) => fn === undefined || typeof fn ===
|
|
56
|
-
|
|
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()),
|
|
57
64
|
});
|
|
58
65
|
const configSchema = z.object({
|
|
59
66
|
backendConfigs: z.record(z.string(), backendSchema)
|
|
@@ -77,7 +84,8 @@ const configSchema = z.object({
|
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
86
|
return { message: errors.join('; ') };
|
|
80
|
-
})
|
|
87
|
+
}),
|
|
88
|
+
periodicSyncIntervalSeconds: z.number().min(5).nullable().optional().default(null),
|
|
81
89
|
});
|
|
82
90
|
// Internal variable to hold the validated configuration.
|
|
83
91
|
let config = null;
|
|
@@ -75,19 +75,21 @@ export class QuerysetStore {
|
|
|
75
75
|
return new Set(this.groundTruthPks);
|
|
76
76
|
}
|
|
77
77
|
_emitRenderEvent() {
|
|
78
|
-
const newPks = this.render(true, false);
|
|
79
|
-
//
|
|
78
|
+
const newPks = this.render(true, false);
|
|
79
|
+
// 1. Always notify direct child stores to trigger their own re-evaluation.
|
|
80
|
+
// They will perform their own check to see if their own results have changed.
|
|
81
|
+
this.renderCallbacks.forEach((callback) => {
|
|
82
|
+
try {
|
|
83
|
+
callback();
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.warn("Error in render callback:", error);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
// 2. Only emit the global event for UI components if the final list of PKs has actually changed.
|
|
80
90
|
if (!isEqual(newPks, this._lastRenderedPks)) {
|
|
81
91
|
this._lastRenderedPks = newPks; // Update the cache with the new state
|
|
82
92
|
querysetEventEmitter.emit(`${this.modelClass.configKey}::${this.modelClass.modelName}::queryset::render`, { ast: this.queryset.build(), ModelClass: this.modelClass });
|
|
83
|
-
this.renderCallbacks.forEach((callback) => {
|
|
84
|
-
try {
|
|
85
|
-
callback();
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
88
|
-
console.warn("Error in render callback:", error);
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
93
|
}
|
|
92
94
|
}
|
|
93
95
|
async addOperation(operation) {
|
|
@@ -15,10 +15,15 @@ export class SyncManager {
|
|
|
15
15
|
followedModels: Map<any, any>;
|
|
16
16
|
followAllQuerysets: boolean;
|
|
17
17
|
followedQuerysets: Map<any, any>;
|
|
18
|
+
periodicSyncTimer: NodeJS.Timeout | null;
|
|
18
19
|
/**
|
|
19
20
|
* Initialize event handlers for all event receivers
|
|
20
21
|
*/
|
|
21
22
|
initialize(): void;
|
|
23
|
+
startPeriodicSync(): void;
|
|
24
|
+
syncStaleQuerysets(): void;
|
|
25
|
+
isStoreFollowed(registry: any, semanticKey: any): boolean;
|
|
26
|
+
cleanup(): void;
|
|
22
27
|
followModel(registry: any, modelClass: any): void;
|
|
23
28
|
unfollowModel(registry: any, modelClass: any): void;
|
|
24
29
|
manageRegistry(registry: any): void;
|
package/dist/syncEngine/sync.js
CHANGED
|
@@ -58,6 +58,7 @@ export class SyncManager {
|
|
|
58
58
|
// Map of querysets to keep synced
|
|
59
59
|
this.followAllQuerysets = true;
|
|
60
60
|
this.followedQuerysets = new Map();
|
|
61
|
+
this.periodicSyncTimer = null;
|
|
61
62
|
}
|
|
62
63
|
/**
|
|
63
64
|
* Initialize event handlers for all event receivers
|
|
@@ -74,6 +75,61 @@ export class SyncManager {
|
|
|
74
75
|
receiver.addModelEventHandler(this.handleEvent.bind(this));
|
|
75
76
|
}
|
|
76
77
|
});
|
|
78
|
+
this.startPeriodicSync();
|
|
79
|
+
}
|
|
80
|
+
startPeriodicSync() {
|
|
81
|
+
if (this.periodicSyncTimer)
|
|
82
|
+
return;
|
|
83
|
+
try {
|
|
84
|
+
const config = getConfig();
|
|
85
|
+
const intervalSeconds = config.periodicSyncIntervalSeconds;
|
|
86
|
+
// If null or undefined, don't start periodic sync
|
|
87
|
+
if (!intervalSeconds) {
|
|
88
|
+
console.log("[SyncManager] Periodic sync disabled (set to null)");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const intervalMs = intervalSeconds * 1000;
|
|
92
|
+
this.periodicSyncTimer = setInterval(() => {
|
|
93
|
+
this.syncStaleQuerysets();
|
|
94
|
+
}, intervalMs);
|
|
95
|
+
console.log(`[SyncManager] Periodic sync started: ${intervalSeconds}s intervals`);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
// If no config, don't start periodic sync by default
|
|
99
|
+
console.log("[SyncManager] No config found, periodic sync disabled by default");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
syncStaleQuerysets() {
|
|
103
|
+
let syncedCount = 0;
|
|
104
|
+
// Sync all followed querysets - keep it simple
|
|
105
|
+
const querysetRegistry = this.registries.get("QuerysetStoreRegistry");
|
|
106
|
+
if (querysetRegistry) {
|
|
107
|
+
for (const [semanticKey, store] of querysetRegistry._stores.entries()) {
|
|
108
|
+
// Only sync if this store is actually being followed
|
|
109
|
+
const isFollowed = this.isStoreFollowed(querysetRegistry, semanticKey);
|
|
110
|
+
if (this.followAllQuerysets || isFollowed) {
|
|
111
|
+
store.sync();
|
|
112
|
+
syncedCount++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (syncedCount > 0) {
|
|
117
|
+
console.log(`[SyncManager] Periodic sync: ${syncedCount} stores synced`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
isStoreFollowed(registry, semanticKey) {
|
|
121
|
+
const followingQuerysets = registry.followingQuerysets.get(semanticKey);
|
|
122
|
+
if (!followingQuerysets)
|
|
123
|
+
return false;
|
|
124
|
+
return [...followingQuerysets].some((queryset) => {
|
|
125
|
+
return this.isQuerysetFollowed(queryset);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
cleanup() {
|
|
129
|
+
if (this.periodicSyncTimer) {
|
|
130
|
+
clearInterval(this.periodicSyncTimer);
|
|
131
|
+
this.periodicSyncTimer = null;
|
|
132
|
+
}
|
|
77
133
|
}
|
|
78
134
|
followModel(registry, modelClass) {
|
|
79
135
|
const models = this.followedModels.get(registry) || new Set();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statezero/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
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",
|
|
@@ -35,8 +35,12 @@
|
|
|
35
35
|
"test:coverage": "vitest run --coverage",
|
|
36
36
|
"build": "tsc",
|
|
37
37
|
"parse-queries": "node scripts/perfect-query-parser.js",
|
|
38
|
+
"sync": "node src/cli/index.js sync",
|
|
39
|
+
"sync:dev": "npx cross-env NODE_ENV=test npm run sync",
|
|
38
40
|
"sync-models": "node src/cli/index.js sync-models",
|
|
39
41
|
"sync-models:dev": "npx cross-env NODE_ENV=test npm run sync-models",
|
|
42
|
+
"sync-actions": "node src/cli/index.js sync-actions",
|
|
43
|
+
"sync-actions:dev": "npx cross-env NODE_ENV=test npm run sync-actions",
|
|
40
44
|
"clean": "npx rimraf dist",
|
|
41
45
|
"prepare": "npm run clean && npm run build",
|
|
42
46
|
"prepublishOnly": "npm run clean && npm run build"
|
|
@@ -82,8 +86,6 @@
|
|
|
82
86
|
"luxon": "^3.6.1",
|
|
83
87
|
"mathjs": "^14.4.0",
|
|
84
88
|
"mitt": "^3.0.1",
|
|
85
|
-
"mobx": "^6.13.7",
|
|
86
|
-
"mobx-utils": "^6.1.0",
|
|
87
89
|
"object-hash": "^3.0.0",
|
|
88
90
|
"openapi-typescript": "^6.7.1",
|
|
89
91
|
"p-queue": "^8.1.0",
|