@statezero/core 0.2.51 → 0.2.53

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.
Files changed (177) hide show
  1. package/dist/adaptors/vue/components/StateZeroDebugPanel.js +586 -559
  2. package/dist/adaptors/vue/composables.d.ts +1 -1
  3. package/dist/adaptors/vue/composables.js +8 -2
  4. package/dist/adaptors/vue/reactivity.d.ts +1 -0
  5. package/dist/adaptors/vue/reactivity.js +20 -8
  6. package/dist/cli/commands/syncActions.js +33 -1
  7. package/dist/cli/commands/syncModels.js +44 -3
  8. package/dist/filtering/localFiltering.js +3 -2
  9. package/dist/flavours/django/errors.d.ts +12 -0
  10. package/dist/flavours/django/errors.js +20 -0
  11. package/dist/flavours/django/f.js +16 -15
  12. package/dist/index.d.ts +2 -1
  13. package/dist/index.js +2 -2
  14. package/dist/models/backend1/django_app/author.d.ts +284 -0
  15. package/dist/models/backend1/django_app/author.js +69 -0
  16. package/dist/models/backend1/django_app/author.schema.json +281 -0
  17. package/dist/models/backend1/django_app/book.d.ts +262 -0
  18. package/dist/models/backend1/django_app/book.js +71 -0
  19. package/dist/models/backend1/django_app/book.schema.json +254 -0
  20. package/dist/models/backend1/django_app/composeditem.d.ts +140 -0
  21. package/dist/models/backend1/django_app/composeditem.js +69 -0
  22. package/dist/models/backend1/django_app/composeditem.schema.json +122 -0
  23. package/dist/models/backend1/django_app/comprehensivemodel.schema.json +3 -3
  24. package/dist/models/backend1/django_app/dailyrate.schema.json +7 -7
  25. package/dist/models/backend1/django_app/deepmodellevel1.schema.json +1 -1
  26. package/dist/models/backend1/django_app/dummymodel.schema.json +3 -3
  27. package/dist/models/backend1/django_app/errortestcompoundunique.d.ts +124 -0
  28. package/dist/models/backend1/django_app/errortestcompoundunique.js +69 -0
  29. package/dist/models/backend1/django_app/errortestcompoundunique.schema.json +106 -0
  30. package/dist/models/backend1/django_app/errortestonetoonemodel.d.ts +118 -0
  31. package/dist/models/backend1/django_app/errortestonetoonemodel.js +71 -0
  32. package/dist/models/backend1/django_app/errortestonetoonemodel.schema.json +90 -0
  33. package/dist/models/backend1/django_app/errortestparent.d.ts +92 -0
  34. package/dist/models/backend1/django_app/errortestparent.js +69 -0
  35. package/dist/models/backend1/django_app/errortestparent.schema.json +70 -0
  36. package/dist/models/backend1/django_app/errortestprotectedchild.d.ts +118 -0
  37. package/dist/models/backend1/django_app/errortestprotectedchild.js +71 -0
  38. package/dist/models/backend1/django_app/errortestprotectedchild.schema.json +94 -0
  39. package/dist/models/backend1/django_app/errortestuniquemodel.d.ts +108 -0
  40. package/dist/models/backend1/django_app/errortestuniquemodel.js +69 -0
  41. package/dist/models/backend1/django_app/errortestuniquemodel.schema.json +88 -0
  42. package/dist/models/backend1/django_app/excludeditem.d.ts +124 -0
  43. package/dist/models/backend1/django_app/excludeditem.js +69 -0
  44. package/dist/models/backend1/django_app/excludeditem.schema.json +105 -0
  45. package/dist/models/backend1/django_app/hfchild.d.ts +118 -0
  46. package/dist/models/backend1/django_app/hfchild.js +71 -0
  47. package/dist/models/backend1/django_app/hfchild.schema.json +94 -0
  48. package/dist/models/backend1/django_app/hfparent.d.ts +124 -0
  49. package/dist/models/backend1/django_app/hfparent.js +69 -0
  50. package/dist/models/backend1/django_app/hfparent.schema.json +105 -0
  51. package/dist/models/backend1/django_app/index.js +19 -0
  52. package/dist/models/backend1/django_app/m2mdepthtestlevel1.schema.json +3 -3
  53. package/dist/models/backend1/django_app/m2mdepthtestlevel2.schema.json +3 -3
  54. package/dist/models/backend1/django_app/m2mdepthtestlevel3.schema.json +5 -5
  55. package/dist/models/backend1/django_app/modelwithrestrictedfields.schema.json +2 -2
  56. package/dist/models/backend1/django_app/nodeleteitem.d.ts +124 -0
  57. package/dist/models/backend1/django_app/nodeleteitem.js +69 -0
  58. package/dist/models/backend1/django_app/nodeleteitem.schema.json +105 -0
  59. package/dist/models/backend1/django_app/objectlevelitem.d.ts +124 -0
  60. package/dist/models/backend1/django_app/objectlevelitem.js +69 -0
  61. package/dist/models/backend1/django_app/objectlevelitem.schema.json +105 -0
  62. package/dist/models/backend1/django_app/order.schema.json +7 -7
  63. package/dist/models/backend1/django_app/orderitem.schema.json +5 -5
  64. package/dist/models/backend1/django_app/product.schema.json +11 -11
  65. package/dist/models/backend1/django_app/productcategory.schema.json +2 -2
  66. package/dist/models/backend1/django_app/rateplan.schema.json +2 -2
  67. package/dist/models/backend1/django_app/readonlyitem.d.ts +124 -0
  68. package/dist/models/backend1/django_app/readonlyitem.js +69 -0
  69. package/dist/models/backend1/django_app/readonlyitem.schema.json +105 -0
  70. package/dist/models/backend1/django_app/restrictedcreateitem.d.ts +124 -0
  71. package/dist/models/backend1/django_app/restrictedcreateitem.js +69 -0
  72. package/dist/models/backend1/django_app/restrictedcreateitem.schema.json +105 -0
  73. package/dist/models/backend1/django_app/restrictededititem.d.ts +124 -0
  74. package/dist/models/backend1/django_app/restrictededititem.js +69 -0
  75. package/dist/models/backend1/django_app/restrictededititem.schema.json +105 -0
  76. package/dist/models/backend1/django_app/restrictedfieldrelatedmodel.schema.json +1 -1
  77. package/dist/models/backend1/django_app/rowfiltereditem.d.ts +124 -0
  78. package/dist/models/backend1/django_app/rowfiltereditem.js +69 -0
  79. package/dist/models/backend1/django_app/rowfiltereditem.schema.json +105 -0
  80. package/dist/models/backend1/django_app/tag.d.ts +554 -0
  81. package/dist/models/backend1/django_app/tag.js +71 -0
  82. package/dist/models/backend1/django_app/tag.schema.json +541 -0
  83. package/dist/models/backend1/django_app/updateonlyitem.d.ts +108 -0
  84. package/dist/models/backend1/django_app/updateonlyitem.js +69 -0
  85. package/dist/models/backend1/django_app/updateonlyitem.schema.json +88 -0
  86. package/dist/models/default/django_app/author.d.ts +284 -0
  87. package/dist/models/default/django_app/author.js +69 -0
  88. package/dist/models/default/django_app/author.schema.json +281 -0
  89. package/dist/models/default/django_app/book.d.ts +262 -0
  90. package/dist/models/default/django_app/book.js +71 -0
  91. package/dist/models/default/django_app/book.schema.json +254 -0
  92. package/dist/models/default/django_app/composeditem.d.ts +140 -0
  93. package/dist/models/default/django_app/composeditem.js +69 -0
  94. package/dist/models/default/django_app/composeditem.schema.json +122 -0
  95. package/dist/models/default/django_app/comprehensivemodel.schema.json +3 -3
  96. package/dist/models/default/django_app/dailyrate.schema.json +7 -7
  97. package/dist/models/default/django_app/deepmodellevel1.d.ts +1 -13
  98. package/dist/models/default/django_app/deepmodellevel1.schema.json +2 -14
  99. package/dist/models/default/django_app/deepmodellevel2.d.ts +1 -13
  100. package/dist/models/default/django_app/deepmodellevel2.schema.json +1 -13
  101. package/dist/models/default/django_app/deepmodellevel3.d.ts +1 -13
  102. package/dist/models/default/django_app/deepmodellevel3.schema.json +1 -13
  103. package/dist/models/default/django_app/dummymodel.d.ts +1 -13
  104. package/dist/models/default/django_app/dummymodel.schema.json +4 -16
  105. package/dist/models/default/django_app/dummyrelatedmodel.d.ts +1 -13
  106. package/dist/models/default/django_app/dummyrelatedmodel.schema.json +1 -13
  107. package/dist/models/default/django_app/errortestcompoundunique.d.ts +124 -0
  108. package/dist/models/default/django_app/errortestcompoundunique.js +69 -0
  109. package/dist/models/default/django_app/errortestcompoundunique.schema.json +106 -0
  110. package/dist/models/default/django_app/errortestonetoonemodel.d.ts +118 -0
  111. package/dist/models/default/django_app/errortestonetoonemodel.js +71 -0
  112. package/dist/models/default/django_app/errortestonetoonemodel.schema.json +90 -0
  113. package/dist/models/default/django_app/errortestparent.d.ts +92 -0
  114. package/dist/models/default/django_app/errortestparent.js +69 -0
  115. package/dist/models/default/django_app/errortestparent.schema.json +70 -0
  116. package/dist/models/default/django_app/errortestprotectedchild.d.ts +118 -0
  117. package/dist/models/default/django_app/errortestprotectedchild.js +71 -0
  118. package/dist/models/default/django_app/errortestprotectedchild.schema.json +94 -0
  119. package/dist/models/default/django_app/errortestuniquemodel.d.ts +108 -0
  120. package/dist/models/default/django_app/errortestuniquemodel.js +69 -0
  121. package/dist/models/default/django_app/errortestuniquemodel.schema.json +88 -0
  122. package/dist/models/default/django_app/excludeditem.d.ts +124 -0
  123. package/dist/models/default/django_app/excludeditem.js +69 -0
  124. package/dist/models/default/django_app/excludeditem.schema.json +105 -0
  125. package/dist/models/default/django_app/filetest.d.ts +1 -13
  126. package/dist/models/default/django_app/filetest.schema.json +1 -13
  127. package/dist/models/default/django_app/hfchild.d.ts +118 -0
  128. package/dist/models/default/django_app/hfchild.js +71 -0
  129. package/dist/models/default/django_app/hfchild.schema.json +94 -0
  130. package/dist/models/default/django_app/hfparent.d.ts +124 -0
  131. package/dist/models/default/django_app/hfparent.js +69 -0
  132. package/dist/models/default/django_app/hfparent.schema.json +105 -0
  133. package/dist/models/default/django_app/index.js +19 -0
  134. package/dist/models/default/django_app/m2mdepthtestlevel1.schema.json +3 -3
  135. package/dist/models/default/django_app/m2mdepthtestlevel2.schema.json +3 -3
  136. package/dist/models/default/django_app/m2mdepthtestlevel3.schema.json +5 -5
  137. package/dist/models/default/django_app/modelwithrestrictedfields.schema.json +2 -2
  138. package/dist/models/default/django_app/nodeleteitem.d.ts +124 -0
  139. package/dist/models/default/django_app/nodeleteitem.js +69 -0
  140. package/dist/models/default/django_app/nodeleteitem.schema.json +105 -0
  141. package/dist/models/default/django_app/objectlevelitem.d.ts +124 -0
  142. package/dist/models/default/django_app/objectlevelitem.js +69 -0
  143. package/dist/models/default/django_app/objectlevelitem.schema.json +105 -0
  144. package/dist/models/default/django_app/order.schema.json +7 -7
  145. package/dist/models/default/django_app/orderitem.schema.json +5 -5
  146. package/dist/models/default/django_app/product.schema.json +11 -11
  147. package/dist/models/default/django_app/productcategory.schema.json +2 -2
  148. package/dist/models/default/django_app/rateplan.schema.json +2 -2
  149. package/dist/models/default/django_app/readonlyitem.d.ts +124 -0
  150. package/dist/models/default/django_app/readonlyitem.js +69 -0
  151. package/dist/models/default/django_app/readonlyitem.schema.json +105 -0
  152. package/dist/models/default/django_app/restrictedcreateitem.d.ts +124 -0
  153. package/dist/models/default/django_app/restrictedcreateitem.js +69 -0
  154. package/dist/models/default/django_app/restrictedcreateitem.schema.json +105 -0
  155. package/dist/models/default/django_app/restrictededititem.d.ts +124 -0
  156. package/dist/models/default/django_app/restrictededititem.js +69 -0
  157. package/dist/models/default/django_app/restrictededititem.schema.json +105 -0
  158. package/dist/models/default/django_app/restrictedfieldrelatedmodel.schema.json +1 -1
  159. package/dist/models/default/django_app/rowfiltereditem.d.ts +124 -0
  160. package/dist/models/default/django_app/rowfiltereditem.js +69 -0
  161. package/dist/models/default/django_app/rowfiltereditem.schema.json +105 -0
  162. package/dist/models/default/django_app/tag.d.ts +554 -0
  163. package/dist/models/default/django_app/tag.js +71 -0
  164. package/dist/models/default/django_app/tag.schema.json +541 -0
  165. package/dist/models/default/django_app/updateonlyitem.d.ts +108 -0
  166. package/dist/models/default/django_app/updateonlyitem.js +69 -0
  167. package/dist/models/default/django_app/updateonlyitem.schema.json +88 -0
  168. package/dist/schemaIntegrity.d.ts +14 -0
  169. package/dist/schemaIntegrity.js +29 -0
  170. package/dist/setup.js +6 -0
  171. package/dist/syncEngine/cache/cache.d.ts +1 -1
  172. package/dist/syncEngine/cache/cache.js +8 -1
  173. package/dist/syncEngine/registries/metricRegistry.d.ts +14 -3
  174. package/dist/syncEngine/registries/metricRegistry.js +20 -6
  175. package/dist/syncEngine/stores/operationEventHandlers.js +10 -8
  176. package/dist/syncEngine/stores/querysetStore.js +27 -6
  177. package/package.json +1 -1
@@ -1,2 +1,2 @@
1
- export function useQueryset(querysetFactory: any): import("vue").ComputedRef<any>;
1
+ export function useQueryset(querysetFactory: any): import("vue").Ref<any, any> | import("vue").ComputedRef<any>;
2
2
  export const querysets: Map<any, any>;
@@ -1,6 +1,6 @@
1
- import { computed, getCurrentInstance, onBeforeUnmount } from 'vue';
1
+ import { computed, isRef, getCurrentInstance, onBeforeUnmount } from 'vue';
2
2
  import { querysetStoreRegistry } from '../../syncEngine/registries/querysetStoreRegistry.js';
3
- import { metricRegistry } from '../../syncEngine/registries/metricRegistry.js';
3
+ import { metricRegistry, LiveMetric } from '../../syncEngine/registries/metricRegistry.js';
4
4
  import { syncManager } from '../../syncEngine/sync.js';
5
5
  import { registerAdapterReset } from '../../reset.js';
6
6
  import { v7 } from 'uuid';
@@ -22,6 +22,12 @@ export function useQueryset(querysetFactory) {
22
22
  querysets.delete(composableId);
23
23
  updateSyncManager();
24
24
  });
25
+ const result = querysetFactory();
26
+ // Metrics: the adaptor already returns a ref(scalar) — return it directly.
27
+ // Wrapping in another computed would cause .value.value in templates.
28
+ if (isRef(result) && result.original instanceof LiveMetric) {
29
+ return result;
30
+ }
25
31
  return computed(() => {
26
32
  const result = querysetFactory();
27
33
  const original = result?.original || result;
@@ -16,3 +16,4 @@ export function ModelAdaptor(modelInstance: Object, reactivityFn?: Function): an
16
16
  */
17
17
  export function QuerySetAdaptor(liveQuerySet: Object, reactivityFn?: Function): any | import("vue").Ref;
18
18
  export function MetricAdaptor(metric: any): any;
19
+ export function resetVueAdaptors(): void;
@@ -123,15 +123,26 @@ export function QuerySetAdaptor(liveQuerySet, reactivityFn = reactive) {
123
123
  wrapper.__version = 0;
124
124
  const eventName = `${configKey}::${modelName}::queryset::render`;
125
125
  // Handler bumps version to trigger Vue reactivity when this queryset updates
126
+ const pkField = queryset.ModelClass.primaryKeyField;
126
127
  const renderHandler = (eventData) => {
127
128
  if (eventData && eventData.ast && isEqual(querysetAst, eventData.ast)) {
128
- console.log(`[sz] queryset update: ${modelName}`, { count: liveQuerySet.length, version: wrapper.__version + 1 });
129
+ const newData = [...liveQuerySet];
130
+ const newPks = newData.map(item => item[pkField]);
131
+ const currentArray = reactivityFn === ref ? wrapper.value : wrapper;
132
+ const oldPks = currentArray.map(item => item[pkField]);
133
+ // Skip if PKs haven't changed — field-level updates on existing items
134
+ // are already handled by Vue's deep reactivity via model touch().
135
+ // Bumping __version here would cause useQueryset's computed to
136
+ // re-evaluate and re-run the factory, risking circular reactive loops.
137
+ if (isEqual(oldPks, newPks))
138
+ return;
139
+ console.log(`[sz] queryset update: ${modelName}`, { count: newData.length, version: wrapper.__version + 1 });
129
140
  if (reactivityFn === ref) {
130
- wrapper.value = [...liveQuerySet];
141
+ wrapper.value = newData;
131
142
  }
132
143
  else {
133
144
  wrapper.splice(0, wrapper.length);
134
- wrapper.push(...liveQuerySet);
145
+ wrapper.push(...newData);
135
146
  }
136
147
  // Bump version so computed/watch can track changes
137
148
  wrapper.__version++;
@@ -162,7 +173,7 @@ export function MetricAdaptor(metric) {
162
173
  return wrappedMetricCache.get(cacheKey);
163
174
  }
164
175
  // Create a reactive reference with the initial value
165
- const wrapper = ref(metric.value);
176
+ const wrapper = ref(metric.valueOf());
166
177
  wrapper.original = metric;
167
178
  // Single handler for metric render events
168
179
  const metricRenderHandler = (eventData) => {
@@ -172,9 +183,9 @@ export function MetricAdaptor(metric) {
172
183
  eventData.field === metric.field &&
173
184
  eventData.ast === hash(querysetAst) &&
174
185
  eventData.valueChanged === true) {
175
- console.log(`[sz] metric update: ${modelName}.${metric.field} (${metric.metricType}) =`, metric.value);
186
+ console.log(`[sz] metric update: ${modelName}.${metric.field} (${metric.metricType}) =`, metric.valueOf());
176
187
  // Update the wrapper value with the latest metric value
177
- wrapper.value = metric.value;
188
+ wrapper.value = metric.valueOf();
178
189
  }
179
190
  };
180
191
  // Only listen for metric render events
@@ -185,7 +196,8 @@ export function MetricAdaptor(metric) {
185
196
  }
186
197
  return wrapper;
187
198
  }
188
- registerAdapterReset(() => {
199
+ export function resetVueAdaptors() {
189
200
  wrappedQuerysetCache.clear();
190
201
  wrappedMetricCache.clear();
191
- });
202
+ }
203
+ registerAdapterReset(resetVueAdaptors);
@@ -768,6 +768,38 @@ export declare namespace getCurrentUser {
768
768
  return generatedFiles;
769
769
  }
770
770
  // ================================================================================================
771
+ // SYNC TOKEN ERROR HANDLING
772
+ // ================================================================================================
773
+ function maskToken(token) {
774
+ if (!token || token.length <= 4)
775
+ return token || '';
776
+ return token.slice(0, 4) + '...';
777
+ }
778
+ function handleSyncFetchError(error, backend, endpoint) {
779
+ const status = error.response?.status;
780
+ const syncToken = backend.SYNC_TOKEN;
781
+ if (status === 409) {
782
+ console.error(`\n❌ Sync token mismatch for backend '${backend.NAME}' (${endpoint}).` +
783
+ `\n The token sent was: '${maskToken(syncToken)}'.` +
784
+ `\n Check that SYNC_TOKEN in your statezero config matches STATEZERO_SYNC_TOKEN on the server.`);
785
+ }
786
+ else if (status === 403) {
787
+ if (syncToken) {
788
+ console.error(`\n❌ Schema access denied for backend '${backend.NAME}' (${endpoint}).` +
789
+ `\n Sync token sent: '${maskToken(syncToken)}'.` +
790
+ `\n The server may not have STATEZERO_SYNC_TOKEN configured, or the token doesn't match.`);
791
+ }
792
+ else {
793
+ console.error(`\n❌ Schema access denied for backend '${backend.NAME}' (${endpoint}).` +
794
+ `\n No sync token was sent.` +
795
+ `\n Set SYNC_TOKEN in your statezero config to match STATEZERO_SYNC_TOKEN on the server, or enable DEBUG=True on the backend.`);
796
+ }
797
+ }
798
+ else {
799
+ console.error(`❌ Error fetching ${endpoint} from ${backend.NAME}: ${error.message}`);
800
+ }
801
+ }
802
+ // ================================================================================================
771
803
  // MAIN SCRIPT RUNNER
772
804
  // ================================================================================================
773
805
  async function main() {
@@ -789,7 +821,7 @@ async function main() {
789
821
  return { backend, actions: response.data.actions || {} };
790
822
  }
791
823
  catch (error) {
792
- console.error(`❌ Error fetching actions from ${backend.NAME}: ${error.message}`);
824
+ handleSyncFetchError(error, backend, '/actions-schema/');
793
825
  return { backend, actions: {} };
794
826
  }
795
827
  });
@@ -794,7 +794,7 @@ function generateImportAlias(backendKey, modelName, className) {
794
794
  * @param {Object.<string, BackendConfig>} backendConfigs
795
795
  * @returns {Promise<void>}
796
796
  */
797
- async function generateModelRegistry(generatedFiles, backendConfigs) {
797
+ async function generateModelRegistry(generatedFiles, backendConfigs, schemaChecksums = {}) {
798
798
  const registryByBackend = {};
799
799
  const allUniqueImports = new Set();
800
800
  for (const file of generatedFiles) {
@@ -876,6 +876,10 @@ export function getModelClass(modelName, configKey) {
876
876
  return null;
877
877
  }
878
878
  `;
879
+ // Add schema checksums if any were captured during sync
880
+ if (Object.keys(schemaChecksums).length > 0) {
881
+ registryContent += `\ngetModelClass.SYNC_CHECKSUMS = ${JSON.stringify(schemaChecksums)};\n`;
882
+ }
879
883
  // --- Write the file ---
880
884
  const rootDir = process.cwd();
881
885
  const registryFilePath = path.join(rootDir, "model-registry.js");
@@ -1027,6 +1031,38 @@ export * from './default/index.js';
1027
1031
  console.log(`✨ Generated top-level index for 'default' backend`);
1028
1032
  }
1029
1033
  // --------------------
1034
+ // Sync Token Error Handling
1035
+ // --------------------
1036
+ function maskToken(token) {
1037
+ if (!token || token.length <= 4)
1038
+ return token || '';
1039
+ return token.slice(0, 4) + '...';
1040
+ }
1041
+ function handleSyncFetchError(error, backend, endpoint) {
1042
+ const status = error.response?.status;
1043
+ const syncToken = backend.SYNC_TOKEN;
1044
+ if (status === 409) {
1045
+ console.error(`\n❌ Sync token mismatch for backend '${backend.NAME}' (${endpoint}).` +
1046
+ `\n The token sent was: '${maskToken(syncToken)}'.` +
1047
+ `\n Check that SYNC_TOKEN in your statezero config matches STATEZERO_SYNC_TOKEN on the server.`);
1048
+ }
1049
+ else if (status === 403) {
1050
+ if (syncToken) {
1051
+ console.error(`\n❌ Schema access denied for backend '${backend.NAME}' (${endpoint}).` +
1052
+ `\n Sync token sent: '${maskToken(syncToken)}'.` +
1053
+ `\n The server may not have STATEZERO_SYNC_TOKEN configured, or the token doesn't match.`);
1054
+ }
1055
+ else {
1056
+ console.error(`\n❌ Schema access denied for backend '${backend.NAME}' (${endpoint}).` +
1057
+ `\n No sync token was sent.` +
1058
+ `\n Set SYNC_TOKEN in your statezero config to match STATEZERO_SYNC_TOKEN on the server, or enable DEBUG=True on the backend.`);
1059
+ }
1060
+ }
1061
+ else {
1062
+ console.error(`Error fetching ${endpoint} from backend ${backend.NAME}:`, error.message);
1063
+ }
1064
+ }
1065
+ // --------------------
1030
1066
  // Main Runner: Fetch models and prompt selection
1031
1067
  // --------------------
1032
1068
  async function main() {
@@ -1035,16 +1071,21 @@ async function main() {
1035
1071
  // Retrieve the validated configuration from the global config singleton.
1036
1072
  const configData = configInstance.getConfig();
1037
1073
  const backendConfigs = configData.backendConfigs;
1074
+ const schemaChecksums = {};
1038
1075
  const fetchPromises = Object.keys(backendConfigs).map(async (key) => {
1039
1076
  const backend = backendConfigs[key];
1040
1077
  backend.NAME = key;
1041
1078
  try {
1042
1079
  const headers = backend.SYNC_TOKEN ? { "X-Sync-Token": backend.SYNC_TOKEN } : {};
1043
1080
  const response = await axios.get(`${backend.API_URL}/models/`, { headers });
1081
+ const checksum = response.headers['x-statezero-schema-checksum'];
1082
+ if (checksum) {
1083
+ schemaChecksums[key] = checksum;
1084
+ }
1044
1085
  return { backend, models: response.data };
1045
1086
  }
1046
1087
  catch (error) {
1047
- console.error(`Error fetching models from backend ${backend.NAME}:`, error.message);
1088
+ handleSyncFetchError(error, backend, '/models/');
1048
1089
  return { backend, models: [] };
1049
1090
  }
1050
1091
  });
@@ -1105,7 +1146,7 @@ async function main() {
1105
1146
  // Generate an index file per app
1106
1147
  await generateAppLevelIndexFiles(allGeneratedFiles, backendConfigs);
1107
1148
  // Generate model registry
1108
- await generateModelRegistry(allGeneratedFiles, backendConfigs);
1149
+ await generateModelRegistry(allGeneratedFiles, backendConfigs, schemaChecksums);
1109
1150
  // Generate top-level index for 'default' backend if it exists
1110
1151
  await generateDefaultBackendIndex(backendConfigs);
1111
1152
  console.log(`✨ Generated JavaScript files with TypeScript declarations for ${selectedModels.length} models across ${Object.keys(backendConfigs).length} backends.`);
@@ -2,6 +2,7 @@ import sift, { createEqualsOperation } from 'sift';
2
2
  import { configInstance } from '../config.js';
3
3
  import { DateTime } from 'luxon';
4
4
  import { ModelSerializer } from '../flavours/django/serializers.js';
5
+ import { ValidationError } from '../flavours/django/errors.js';
5
6
  /**
6
7
  * Gets the backend timezone for a model class
7
8
  * @param {Class} ModelClass - The model class
@@ -54,7 +55,7 @@ function processFieldPath(fieldPath, value, ModelClass, options = {}) {
54
55
  const knownLookups = [
55
56
  'exact', 'iexact', 'contains', 'icontains', 'startswith',
56
57
  'istartswith', 'endswith', 'iendswith', 'in', 'gt', 'gte',
57
- 'lt', 'lte', 'isnull', 'regex', 'iregex', 'year', 'month',
58
+ 'lt', 'lte', 'isnull', 'year', 'month',
58
59
  'day', 'week_day', 'hour', 'minute', 'second', 'date', 'time'
59
60
  ];
60
61
  // Date part lookups that can be followed by comparison lookups
@@ -627,7 +628,7 @@ function convertToSiftCriteria(conditions, ModelClass) {
627
628
  }
628
629
  }
629
630
  catch (error) {
630
- throw new Error(`Failed to process field '${key}': ${error.message}`);
631
+ throw new ValidationError(`Failed to process field '${key}': ${error.message}`);
631
632
  }
632
633
  }
633
634
  // Merge M2M conditions: all conditions on same M2M field go into single $elemMatch
@@ -88,6 +88,18 @@ export class MultipleObjectsReturned extends StateZeroError {
88
88
  */
89
89
  constructor(detail?: IErrorDetail | Object | string, status?: number);
90
90
  }
91
+ /**
92
+ * Error class for database conflict errors (integrity/constraint violations).
93
+ */
94
+ export class ConflictError extends StateZeroError {
95
+ /**
96
+ * Creates a new ConflictError.
97
+ *
98
+ * @param {IErrorDetail|Object|string} [detail="A database conflict occurred."] - The error details.
99
+ * @param {number} [status=409] - The HTTP status code.
100
+ */
101
+ constructor(detail?: IErrorDetail | Object | string, status?: number);
102
+ }
91
103
  /**
92
104
  * Error class for AST validation errors.
93
105
  */
@@ -116,6 +116,21 @@ export class MultipleObjectsReturned extends StateZeroError {
116
116
  this.name = "MultipleObjectsReturned";
117
117
  }
118
118
  }
119
+ /**
120
+ * Error class for database conflict errors (integrity/constraint violations).
121
+ */
122
+ export class ConflictError extends StateZeroError {
123
+ /**
124
+ * Creates a new ConflictError.
125
+ *
126
+ * @param {IErrorDetail|Object|string} [detail="A database conflict occurred."] - The error details.
127
+ * @param {number} [status=409] - The HTTP status code.
128
+ */
129
+ constructor(detail = "A database conflict occurred.", status = 409) {
130
+ super("Conflict", "conflict", detail, status);
131
+ this.name = "ConflictError";
132
+ }
133
+ }
119
134
  /**
120
135
  * Error class for AST validation errors.
121
136
  */
@@ -169,6 +184,8 @@ export function parseStateZeroError(errorResponse) {
169
184
  return new MultipleObjectsReturned(detail, status);
170
185
  case "PermissionDenied":
171
186
  return new PermissionDenied(detail, status);
187
+ case "ConflictError":
188
+ return new ConflictError(detail, status);
172
189
  case "ASTValidationError":
173
190
  return new ASTValidationError(detail, status);
174
191
  case "ConfigError":
@@ -189,6 +206,9 @@ export function parseStateZeroError(errorResponse) {
189
206
  else if (status === 404) {
190
207
  return new DoesNotExist(detail, status);
191
208
  }
209
+ else if (status === 409) {
210
+ return new ConflictError(detail, status);
211
+ }
192
212
  return new StateZeroError("Unknown error", "unknown", detail, status);
193
213
  }
194
214
  }
@@ -1,4 +1,3 @@
1
- import { ValidationError } from './errors.js';
2
1
  import * as math from 'mathjs';
3
2
  // Create a math instance
4
3
  const mathInstance = math.create();
@@ -8,7 +7,7 @@ const allowedFunctions = ['abs', 'round', 'floor', 'ceil', 'min', 'max'];
8
7
  const ALLOWED_OPERATORS = ['+', '-', '*', '/', '%', '^'];
9
8
  export function F(expression) {
10
9
  if (typeof expression !== 'string' || !expression) {
11
- throw new ValidationError('F expression requires a non-empty string');
10
+ throw new Error('F expression requires a non-empty string');
12
11
  }
13
12
  try {
14
13
  const node = math.parse(expression);
@@ -20,7 +19,9 @@ export function F(expression) {
20
19
  };
21
20
  }
22
21
  catch (err) {
23
- throw new ValidationError(`Invalid F expression: ${err.message}`);
22
+ if (err instanceof Error)
23
+ throw err;
24
+ throw new Error(`Invalid F expression: ${err}`);
24
25
  }
25
26
  }
26
27
  function validateExpression(node) {
@@ -28,19 +29,19 @@ function validateExpression(node) {
28
29
  return;
29
30
  if (node.type === 'OperatorNode') {
30
31
  if (!ALLOWED_OPERATORS.includes(node.op)) {
31
- throw new ValidationError(`Unsupported operator: ${node.op}`);
32
+ throw new Error(`Unsupported operator: ${node.op}`);
32
33
  }
33
34
  node.args.forEach(validateExpression);
34
35
  }
35
36
  else if (node.type === 'FunctionNode') {
36
37
  if (!allowedFunctions.includes(node.name)) {
37
- throw new ValidationError(`Function not allowed: ${node.name}`);
38
+ throw new Error(`Function not allowed: ${node.name}`);
38
39
  }
39
40
  node.args.forEach(validateExpression);
40
41
  }
41
42
  else if (node.type === 'SymbolNode') {
42
43
  if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(node.name)) {
43
- throw new ValidationError(`Invalid field name: ${node.name}`);
44
+ throw new Error(`Invalid field name: ${node.name}`);
44
45
  }
45
46
  }
46
47
  else if (node.type === 'ConstantNode') {
@@ -50,31 +51,31 @@ function validateExpression(node) {
50
51
  validateExpression(node.content);
51
52
  }
52
53
  else if (node.type === 'ConditionalNode') {
53
- throw new ValidationError('Conditional expressions are not supported in F expressions');
54
+ throw new Error('Conditional expressions are not supported in F expressions');
54
55
  }
55
56
  else if (node.type === 'AssignmentNode') {
56
- throw new ValidationError('Assignment expressions are not supported in F expressions');
57
+ throw new Error('Assignment expressions are not supported in F expressions');
57
58
  }
58
59
  else if (node.type === 'BlockNode') {
59
- throw new ValidationError('Block expressions are not supported in F expressions');
60
+ throw new Error('Block expressions are not supported in F expressions');
60
61
  }
61
62
  else if (node.type === 'AccessorNode') {
62
- throw new ValidationError('Accessor expressions are not supported in F expressions');
63
+ throw new Error('Accessor expressions are not supported in F expressions');
63
64
  }
64
65
  else if (node.type === 'IndexNode') {
65
- throw new ValidationError('Index expressions are not supported in F expressions');
66
+ throw new Error('Index expressions are not supported in F expressions');
66
67
  }
67
68
  else if (node.type === 'RangeNode') {
68
- throw new ValidationError('Range expressions are not supported in F expressions');
69
+ throw new Error('Range expressions are not supported in F expressions');
69
70
  }
70
71
  else if (node.type === 'ArrayNode') {
71
- throw new ValidationError('Array expressions are not supported in F expressions');
72
+ throw new Error('Array expressions are not supported in F expressions');
72
73
  }
73
74
  else if (node.type === 'ObjectNode') {
74
- throw new ValidationError('Object expressions are not supported in F expressions');
75
+ throw new Error('Object expressions are not supported in F expressions');
75
76
  }
76
77
  else {
77
- throw new ValidationError(`Unsupported node type: ${node.type}`);
78
+ throw new Error(`Unsupported node type: ${node.type}`);
78
79
  }
79
80
  }
80
81
  export function evaluateExpression(expr, data) {
package/dist/index.d.ts CHANGED
@@ -18,6 +18,7 @@ import { ValidationError } from "./flavours/django/errors.js";
18
18
  import { DoesNotExist } from "./flavours/django/errors.js";
19
19
  import { PermissionDenied } from "./flavours/django/errors.js";
20
20
  import { MultipleObjectsReturned } from "./flavours/django/errors.js";
21
+ import { ConflictError } from "./flavours/django/errors.js";
21
22
  import { ASTValidationError } from "./flavours/django/errors.js";
22
23
  import { ConfigError } from "./flavours/django/errors.js";
23
24
  import { parseStateZeroError } from "./flavours/django/errors.js";
@@ -43,4 +44,4 @@ import { setupTestStateZero } from "./testing.js";
43
44
  import { createActionMocker } from "./testing.js";
44
45
  import { seedRemote } from "./testing.js";
45
46
  import { resetRemote } from "./testing.js";
46
- export { EventType, PusherEventReceiver, setEventReceiver, getEventReceiver, setNamespaceResolver, setupStateZero, resetStateZero, FileObject, querysetStoreRegistry, modelStoreRegistry, metricRegistry, syncManager, Operation, operationRegistry, Q, StateZeroError, ValidationError, DoesNotExist, PermissionDenied, MultipleObjectsReturned, ASTValidationError, ConfigError, parseStateZeroError, QuerySet, Manager, ResultTuple, Model, setConfig, getConfig, setBackendConfig, initializeEventReceiver, configInstance, getModelClass, initEventHandler, cleanupEventHandler, setAdapters, wrapReactiveModel, wrapReactiveQuerySet, serializeActionPayload, onStateZeroError, createTestConfig, setupTestStateZero, createActionMocker, seedRemote, resetRemote };
47
+ export { EventType, PusherEventReceiver, setEventReceiver, getEventReceiver, setNamespaceResolver, setupStateZero, resetStateZero, FileObject, querysetStoreRegistry, modelStoreRegistry, metricRegistry, syncManager, Operation, operationRegistry, Q, StateZeroError, ValidationError, DoesNotExist, PermissionDenied, MultipleObjectsReturned, ConflictError, ASTValidationError, ConfigError, parseStateZeroError, QuerySet, Manager, ResultTuple, Model, setConfig, getConfig, setBackendConfig, initializeEventReceiver, configInstance, getModelClass, initEventHandler, cleanupEventHandler, setAdapters, wrapReactiveModel, wrapReactiveQuerySet, serializeActionPayload, onStateZeroError, createTestConfig, setupTestStateZero, createActionMocker, seedRemote, resetRemote };
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { EventType, PusherEventReceiver, setEventReceiver, getEventReceiver, set
3
3
  import { setupStateZero } from "./setup.js";
4
4
  // Django flavor modules
5
5
  import { Q } from "./flavours/django/q.js";
6
- import { StateZeroError, ValidationError, DoesNotExist, PermissionDenied, MultipleObjectsReturned, ASTValidationError, ConfigError, parseStateZeroError, } from "./flavours/django/errors.js";
6
+ import { StateZeroError, ValidationError, DoesNotExist, PermissionDenied, MultipleObjectsReturned, ConflictError, ASTValidationError, ConfigError, parseStateZeroError, } from "./flavours/django/errors.js";
7
7
  import { querysetStoreRegistry } from "./syncEngine/registries/querysetStoreRegistry.js";
8
8
  import { modelStoreRegistry } from "./syncEngine/registries/modelStoreRegistry.js";
9
9
  import { metricRegistry } from "./syncEngine/registries/metricRegistry.js";
@@ -35,7 +35,7 @@ querysetStoreRegistry, modelStoreRegistry, metricRegistry, syncManager,
35
35
  // Operations
36
36
  Operation, operationRegistry,
37
37
  // Django flavor modules
38
- Q, StateZeroError, ValidationError, DoesNotExist, PermissionDenied, MultipleObjectsReturned, ASTValidationError, ConfigError, parseStateZeroError, QuerySet, Manager, ResultTuple, Model,
38
+ Q, StateZeroError, ValidationError, DoesNotExist, PermissionDenied, MultipleObjectsReturned, ConflictError, ASTValidationError, ConfigError, parseStateZeroError, QuerySet, Manager, ResultTuple, Model,
39
39
  // Configuration
40
40
  setConfig, getConfig, setBackendConfig, initializeEventReceiver, configInstance, getModelClass,
41
41
  // Reactivity framework integration