@seedprotocol/sdk 0.6.6 → 0.6.7

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 (128) hide show
  1. package/dist/{BasePathResolver-BGxb4Qod.js → BasePathResolver-qV0apdVY.js} +124 -2
  2. package/dist/BasePathResolver-qV0apdVY.js.map +1 -0
  3. package/dist/{index-CJNKYI37.js → ClientManager-Y5EWQuJj.js} +1525 -5495
  4. package/dist/ClientManager-Y5EWQuJj.js.map +1 -0
  5. package/dist/{ModelProperty-Bvg9cJcd.js → ModelProperty-DuB5nk6A.js} +33 -30
  6. package/dist/ModelProperty-DuB5nk6A.js.map +1 -0
  7. package/dist/{Schema-BNJBY80k.js → Schema-BYM-GNdE.js} +40 -39
  8. package/dist/{Schema-BNJBY80k.js.map → Schema-BYM-GNdE.js.map} +1 -1
  9. package/dist/{SchemaValidationService-aqXkwXTr.js → SchemaValidationService-Dr6nmwPm.js} +12 -9
  10. package/dist/{SchemaValidationService-aqXkwXTr.js.map → SchemaValidationService-Dr6nmwPm.js.map} +1 -1
  11. package/dist/{backfillMetadataPropertyIds-DYkvoOMz.js → backfillMetadataPropertyIds-BzbAB-C5.js} +4 -3
  12. package/dist/{backfillMetadataPropertyIds-DYkvoOMz.js.map → backfillMetadataPropertyIds-BzbAB-C5.js.map} +1 -1
  13. package/dist/cjs/{ModelProperty-alntkSnH.js → ModelProperty-CDTOx5zi.js} +21 -20
  14. package/dist/cjs/{ModelProperty-alntkSnH.js.map → ModelProperty-CDTOx5zi.js.map} +1 -1
  15. package/dist/cjs/{Schema-ByFv6fcb.js → Schema-XXcwj3hV.js} +24 -23
  16. package/dist/cjs/{Schema-ByFv6fcb.js.map → Schema-XXcwj3hV.js.map} +1 -1
  17. package/dist/cjs/{SchemaValidationService-D55MWsxv.js → SchemaValidationService-Z7OhtGn5.js} +6 -5
  18. package/dist/cjs/{SchemaValidationService-D55MWsxv.js.map → SchemaValidationService-Z7OhtGn5.js.map} +1 -1
  19. package/dist/cjs/{backfillMetadataPropertyIds-DrrL5RqD.js → backfillMetadataPropertyIds-Bt4fzfIV.js} +6 -5
  20. package/dist/cjs/{backfillMetadataPropertyIds-DrrL5RqD.js.map → backfillMetadataPropertyIds-Bt4fzfIV.js.map} +1 -1
  21. package/dist/cjs/{eas-D2E3_i_c.js → eas-DdI-4vwl.js} +31 -6
  22. package/dist/cjs/eas-DdI-4vwl.js.map +1 -0
  23. package/dist/cjs/{getItem-B2zLVB_l.js → getItem-B_2GeTQf.js} +5 -4
  24. package/dist/cjs/{getItem-B2zLVB_l.js.map → getItem-B_2GeTQf.js.map} +1 -1
  25. package/dist/cjs/{getPublishPayload-Bv94k4ak.js → getPublishPayload-Jmgi1RUj.js} +13 -12
  26. package/dist/cjs/getPublishPayload-Jmgi1RUj.js.map +1 -0
  27. package/dist/cjs/{getPublishUploads-CSbHUxYk.js → getPublishUploads-DNULcrN5.js} +8 -7
  28. package/dist/cjs/{getPublishUploads-CSbHUxYk.js.map → getPublishUploads-DNULcrN5.js.map} +1 -1
  29. package/dist/cjs/{getSegmentedItemProperties-CMhXKQHz.js → getSegmentedItemProperties-DK82kjri.js} +3 -3
  30. package/dist/cjs/{getSegmentedItemProperties-CMhXKQHz.js.map → getSegmentedItemProperties-DK82kjri.js.map} +1 -1
  31. package/dist/cjs/{index-D-9WglxG.js → index-DM1WR83P.js} +5152 -4967
  32. package/dist/cjs/index-DM1WR83P.js.map +1 -0
  33. package/dist/cjs/{index-pSLjO-ch.js → index-tpO1Nj4v.js} +7 -6
  34. package/dist/cjs/index-tpO1Nj4v.js.map +1 -0
  35. package/dist/cjs/{property-gUK2PijH.js → property-DmvgA8FD.js} +8 -7
  36. package/dist/cjs/{property-gUK2PijH.js.map → property-DmvgA8FD.js.map} +1 -1
  37. package/dist/cjs/{syncDbWithEas-CYtl7VTD.js → syncDbWithEas-3hOoPdu1.js} +9 -8
  38. package/dist/cjs/{syncDbWithEas-CYtl7VTD.js.map → syncDbWithEas-3hOoPdu1.js.map} +1 -1
  39. package/dist/{configurePlatform-bGvKBBQy.js → configurePlatform-CCYQruij.js} +6 -3
  40. package/dist/{configurePlatform-bGvKBBQy.js.map → configurePlatform-CCYQruij.js.map} +1 -1
  41. package/dist/constants-DSqHBPNe.js +1347 -0
  42. package/dist/constants-DSqHBPNe.js.map +1 -0
  43. package/dist/{db-YiH_kIpE.js → db-CceyWksv.js} +29 -35
  44. package/dist/{db-YiH_kIpE.js.map → db-CceyWksv.js.map} +1 -1
  45. package/dist/{eas-1nUCcmeh.js → eas-CH8iFIST.js} +9 -6
  46. package/dist/eas-CH8iFIST.js.map +1 -0
  47. package/dist/generateId-BopUwD8F.js +9 -0
  48. package/dist/generateId-BopUwD8F.js.map +1 -0
  49. package/dist/{getItem-Bb8bTYzY.js → getItem-C7Mtfz6O.js} +11 -9
  50. package/dist/{getItem-Bb8bTYzY.js.map → getItem-C7Mtfz6O.js.map} +1 -1
  51. package/dist/{getPublishPayload-BHw8mPSb.js → getPublishPayload-2gnnDdMh.js} +23 -20
  52. package/dist/getPublishPayload-2gnnDdMh.js.map +1 -0
  53. package/dist/{getPublishUploads-BYhckxFl.js → getPublishUploads-AKISt3JA.js} +14 -11
  54. package/dist/{getPublishUploads-BYhckxFl.js.map → getPublishUploads-AKISt3JA.js.map} +1 -1
  55. package/dist/{getSegmentedItemProperties-B73nMPBv.js → getSegmentedItemProperties-eIjvhCP_.js} +5 -5
  56. package/dist/{getSegmentedItemProperties-B73nMPBv.js.map → getSegmentedItemProperties-eIjvhCP_.js.map} +1 -1
  57. package/dist/index-BFpbtEsg.js +3492 -0
  58. package/dist/index-BFpbtEsg.js.map +1 -0
  59. package/dist/{index-DzgMt8ba.js → index-CHi212ST.js} +15 -12
  60. package/dist/index-CHi212ST.js.map +1 -0
  61. package/dist/{index-C_5xwOPQ.js → index-CsmiM0RI.js} +2 -2
  62. package/dist/index-CsmiM0RI.js.map +1 -0
  63. package/dist/{index.node-ZLiBj9Cu.js → index.node-CbWB4MDz.js} +4 -5
  64. package/dist/{index.node-ZLiBj9Cu.js.map → index.node-CbWB4MDz.js.map} +1 -1
  65. package/dist/main.cjs +5 -4
  66. package/dist/main.cjs.map +1 -1
  67. package/dist/main.js +33 -31
  68. package/dist/main.js.map +1 -1
  69. package/dist/model-DGMe-TTm.js +25 -0
  70. package/dist/model-DGMe-TTm.js.map +1 -0
  71. package/dist/node.js +19 -16
  72. package/dist/node.js.map +1 -1
  73. package/dist/platform.browser.js +60 -23
  74. package/dist/platform.browser.js.map +1 -1
  75. package/dist/platform.node.js +4 -5
  76. package/dist/platform.node.js.map +1 -1
  77. package/dist/{property-Dcb50seV.js → property-CWKcZ5YZ.js} +18 -16
  78. package/dist/property-CWKcZ5YZ.js.map +1 -0
  79. package/dist/src/Item/Item.d.ts.map +1 -1
  80. package/dist/src/ItemProperty/ItemProperty.d.ts.map +1 -1
  81. package/dist/src/ItemProperty/service/actors/hydrateFromDb.d.ts.map +1 -1
  82. package/dist/src/ItemProperty/service/actors/loadOrCreateProperty.d.ts.map +1 -1
  83. package/dist/src/Model/service/actors/loadOrCreateModel.d.ts.map +1 -1
  84. package/dist/src/browser/db/Db.d.ts.map +1 -1
  85. package/dist/src/browser/db/embeddedMigrations.d.ts +17 -0
  86. package/dist/src/browser/db/embeddedMigrations.d.ts.map +1 -0
  87. package/dist/src/client/ClientManager.d.ts +18 -3
  88. package/dist/src/client/ClientManager.d.ts.map +1 -1
  89. package/dist/src/client/clientManagerMachine.d.ts +6 -1
  90. package/dist/src/client/clientManagerMachine.d.ts.map +1 -1
  91. package/dist/src/eas.d.ts +2 -2
  92. package/dist/src/eas.d.ts.map +1 -1
  93. package/dist/src/helpers/ArweaveClient/selectReadGateway.d.ts +1 -1
  94. package/dist/src/helpers/ArweaveClient/selectReadGateway.d.ts.map +1 -1
  95. package/dist/src/helpers/constants.d.ts +1 -2
  96. package/dist/src/helpers/constants.d.ts.map +1 -1
  97. package/dist/src/helpers/crypto.d.ts +3 -2
  98. package/dist/src/helpers/crypto.d.ts.map +1 -1
  99. package/dist/src/helpers/ensureImageLocal.d.ts +24 -0
  100. package/dist/src/helpers/ensureImageLocal.d.ts.map +1 -0
  101. package/dist/src/helpers/ethereumUtils.d.ts.map +1 -1
  102. package/dist/src/helpers/index.d.ts +10 -2
  103. package/dist/src/helpers/index.d.ts.map +1 -1
  104. package/dist/src/imports/json.d.ts.map +1 -1
  105. package/dist/src/index.d.ts +3 -1
  106. package/dist/src/index.d.ts.map +1 -1
  107. package/dist/{syncDbWithEas-B0RUeDuj.js → syncDbWithEas-5kZneTEL.js} +19 -17
  108. package/dist/syncDbWithEas-5kZneTEL.js.map +1 -0
  109. package/dist/{updateSeedRevokedAt-D5ZbBehZ.js → updateSeedRevokedAt-CgfOBtqJ.js} +2 -2
  110. package/dist/{updateSeedRevokedAt-D5ZbBehZ.js.map → updateSeedRevokedAt-CgfOBtqJ.js.map} +1 -1
  111. package/package.json +6 -6
  112. package/dist/BasePathResolver-BGxb4Qod.js.map +0 -1
  113. package/dist/ModelProperty-Bvg9cJcd.js.map +0 -1
  114. package/dist/cjs/eas-D2E3_i_c.js.map +0 -1
  115. package/dist/cjs/getPublishPayload-Bv94k4ak.js.map +0 -1
  116. package/dist/cjs/index-D-9WglxG.js.map +0 -1
  117. package/dist/cjs/index-pSLjO-ch.js.map +0 -1
  118. package/dist/constants-Bre6dTJR.js +0 -122
  119. package/dist/constants-Bre6dTJR.js.map +0 -1
  120. package/dist/constants-CTs2RUrM.js +0 -657
  121. package/dist/constants-CTs2RUrM.js.map +0 -1
  122. package/dist/eas-1nUCcmeh.js.map +0 -1
  123. package/dist/getPublishPayload-BHw8mPSb.js.map +0 -1
  124. package/dist/index-CJNKYI37.js.map +0 -1
  125. package/dist/index-C_5xwOPQ.js.map +0 -1
  126. package/dist/index-DzgMt8ba.js.map +0 -1
  127. package/dist/property-Dcb50seV.js.map +0 -1
  128. package/dist/syncDbWithEas-B0RUeDuj.js.map +0 -1
@@ -0,0 +1,3492 @@
1
+ import { fromCallback, fromPromise, setup, assign, createActor } from 'xstate';
2
+ import { a as BaseDb, m as models, p as properties, G as isInternalSchema, i as modelSchemas, s as schemas } from './BasePathResolver-qV0apdVY.js';
3
+ import { eq, or } from 'drizzle-orm';
4
+ import { C as ClientManagerState, d as TProperty } from './constants-DSqHBPNe.js';
5
+ import debug from 'debug';
6
+ import { generateId } from './generateId-BopUwD8F.js';
7
+ import { w as writeModelToDb, k as writePropertyToDb, l as addSchemaToDb, r as renameModelInDb } from './db-CceyWksv.js';
8
+ import { w as waitForEntityIdle, a8 as getInvariantSafePropertyDescriptor, m as findEntity, B as getClient, I as Item, a9 as unloadEntity, aa as clearDestroySubscriptions, n as forceRemoveFromCaches, o as runDestroyLifecycle } from './ClientManager-Y5EWQuJj.js';
9
+ import { modelPropertiesToObject } from './model-DGMe-TTm.js';
10
+ import { toSnakeCase } from 'drizzle-orm/casing';
11
+ import { Type } from '@sinclair/typebox';
12
+
13
+ const logger$7 = debug('seedSdk:model:actors:loadOrCreateModel');
14
+ /**
15
+ * Create ModelProperty instances for all property IDs to ensure they're cached
16
+ * This ensures that ModelProperty.getById() in Model.properties getter will find the instances
17
+ * @param propertyFileIds - Array of property file IDs to create instances for
18
+ */
19
+ const createPropertyInstances = async (propertyFileIds) => {
20
+ if (propertyFileIds.length === 0) {
21
+ return;
22
+ }
23
+ try {
24
+ const mod = await import('./ModelProperty-DuB5nk6A.js');
25
+ const ModelProperty = mod?.ModelProperty ?? mod?.default;
26
+ if (!ModelProperty) {
27
+ logger$7('createPropertyInstances: ModelProperty not available from dynamic import');
28
+ return;
29
+ }
30
+ // Create instances for all property IDs in parallel
31
+ // ModelProperty.createById() will check cache first, then query DB and create if needed
32
+ const createPromises = propertyFileIds.map(async (propertyFileId) => {
33
+ try {
34
+ const property = await ModelProperty.createById(propertyFileId);
35
+ if (property) {
36
+ logger$7(`Created/cached ModelProperty instance for propertyFileId "${propertyFileId}"`);
37
+ }
38
+ else {
39
+ logger$7(`ModelProperty.createById returned undefined for propertyFileId "${propertyFileId}" (may not exist in DB yet)`);
40
+ }
41
+ }
42
+ catch (error) {
43
+ logger$7(`Error creating ModelProperty instance for propertyFileId "${propertyFileId}": ${error}`);
44
+ // Don't throw - continue with other properties
45
+ }
46
+ });
47
+ await Promise.all(createPromises);
48
+ logger$7(`Finished creating/caching ${propertyFileIds.length} ModelProperty instances`);
49
+ }
50
+ catch (error) {
51
+ logger$7(`Error in createPropertyInstances: ${error}`);
52
+ // Don't throw - this is best-effort to pre-populate cache
53
+ }
54
+ };
55
+ const loadOrCreateModel = fromCallback(({ sendBack, input: { context } }) => {
56
+ const _loadOrCreateModel = async () => {
57
+ const { modelName, schemaName, id, _idFromSchema } = context; // id is now the schemaFileId (string)
58
+ if (!modelName || !schemaName) {
59
+ throw new Error('Model name and schema name are required');
60
+ }
61
+ // CRITICAL: Model instances load their data from database, not from Schema context.
62
+ // Schema is read-only with respect to Model instances.
63
+ const db = BaseDb.getAppDb();
64
+ let schemaFileId = id; // id is now the schemaFileId (string)
65
+ let modelRecord = null;
66
+ let foundBySchemaFileId = false; // Track if model was found by schemaFileId
67
+ let existingNumbersFromSchemaContext = null; // When we fall through (same name, different id), schema model occupies base name
68
+ // Step 1: Load from database FIRST (primary source of truth)
69
+ if (db) {
70
+ try {
71
+ // Try to find model by schemaFileId if provided
72
+ if (schemaFileId) {
73
+ const dbModels = await db
74
+ .select()
75
+ .from(models)
76
+ .where(eq(models.schemaFileId, schemaFileId))
77
+ .limit(1);
78
+ if (dbModels.length > 0) {
79
+ modelRecord = dbModels[0];
80
+ foundBySchemaFileId = true;
81
+ logger$7(`Found model "${modelName}" in database by schemaFileId "${schemaFileId}"`);
82
+ }
83
+ }
84
+ // If not found by ID, try by name
85
+ // But if we have a schemaFileId and the model found by name has a different schemaFileId,
86
+ // don't use it - we're creating a new model from a schema file with a specific ID
87
+ if (!modelRecord) {
88
+ const dbModels = await db
89
+ .select()
90
+ .from(models)
91
+ .where(eq(models.name, modelName))
92
+ .limit(1);
93
+ if (dbModels.length > 0) {
94
+ const foundModel = dbModels[0];
95
+ const dbSchemaFileId = foundModel.schemaFileId;
96
+ // CRITICAL: If we found a model in the database by name, check if there's already a cached instance
97
+ // with that schemaFileId. If so, we should use that instance's schemaFileId for the current instance.
98
+ // This handles the case where Model.create was called with a generated ID, but the model
99
+ // already exists in the database with a different ID. By updating the current instance's
100
+ // schemaFileId to match the database, both will point to the same cached instance.
101
+ if (dbSchemaFileId) {
102
+ try {
103
+ const modelMod = await Promise.resolve().then(function () { return Model$1; });
104
+ const { Model } = modelMod;
105
+ // Access instanceCacheById via type assertion since it's protected
106
+ const cacheById = Model.instanceCacheById;
107
+ if (cacheById.has(dbSchemaFileId)) {
108
+ logger$7(`Model "${modelName}" found in database by name with schemaFileId "${dbSchemaFileId}", and a cached instance already exists. Updating current instance to use the same schemaFileId.`);
109
+ // Update the current instance's schemaFileId to match the database
110
+ // This ensures both instances point to the same cached instance
111
+ modelRecord = foundModel;
112
+ schemaFileId = dbSchemaFileId;
113
+ logger$7(`Using existing cached instance with schemaFileId "${schemaFileId}" for model "${modelName}"`);
114
+ }
115
+ }
116
+ catch (error) {
117
+ logger$7(`Error checking Model cache for schemaFileId "${dbSchemaFileId}": ${error}`);
118
+ // Fall through to normal processing
119
+ }
120
+ }
121
+ // If we have a schemaFileId and it doesn't match the found model's schemaFileId,
122
+ // this is a new model - don't use the existing model (will be renamed if duplicate)
123
+ // UNLESS we found a cached instance above (in which case modelRecord is already set)
124
+ if (!modelRecord && schemaFileId && dbSchemaFileId && schemaFileId !== dbSchemaFileId) {
125
+ logger$7(`Model "${modelName}" found by name but has different schemaFileId (provided: "${schemaFileId}", db: "${dbSchemaFileId}"). Creating new model (will check for duplicates).`);
126
+ // Don't set modelRecord - we'll create a new model with potentially renamed name
127
+ }
128
+ else if (!modelRecord && schemaFileId && !dbSchemaFileId) {
129
+ // We have a schemaFileId but the found model doesn't have one - this is a new model
130
+ // Don't reuse the existing model, create a new one (will be renamed if duplicate)
131
+ logger$7(`Model "${modelName}" found by name but found model has no schemaFileId. Creating new model with schemaFileId "${schemaFileId}" (will check for duplicates).`);
132
+ // Don't set modelRecord - we'll create a new model
133
+ }
134
+ else if (!modelRecord) {
135
+ // Only reuse existing model if:
136
+ // 1. No schemaFileId was provided (loading existing model), OR
137
+ // 2. schemaFileId matches (same model)
138
+ logger$7(`Model "${modelName}" found by name, reusing existing model (schemaFileId: provided="${schemaFileId}", db="${dbSchemaFileId}")`);
139
+ modelRecord = foundModel;
140
+ // Only use database schemaFileId if no id was explicitly provided
141
+ // If an id was provided, we should use it (it might be creating a new model with a specific ID)
142
+ if (!id && dbSchemaFileId) {
143
+ schemaFileId = dbSchemaFileId;
144
+ logger$7(`Using database schemaFileId "${schemaFileId}" for model "${modelName}" (no ID was provided)`);
145
+ }
146
+ else if (id) {
147
+ logger$7(`Preserving provided id (schemaFileId) "${schemaFileId}" for model "${modelName}" (ignoring database schemaFileId "${dbSchemaFileId}")`);
148
+ }
149
+ logger$7(`Found model "${modelName}" in database by name, schemaFileId: "${schemaFileId}"`);
150
+ }
151
+ }
152
+ }
153
+ // If we found the model record, load its properties from database
154
+ if (modelRecord && modelRecord.id) {
155
+ const propertyRecords = await db
156
+ .select()
157
+ .from(properties)
158
+ .where(eq(properties.modelId, modelRecord.id));
159
+ // Properties are now loaded via liveQuery, not passed through context
160
+ // The property records are used by liveQuery to populate ModelProperty instances
161
+ // Pre-create ModelProperty instances for all property IDs to ensure they're cached
162
+ // This ensures that ModelProperty.getById() in Model.properties getter will find the instances
163
+ const propertyFileIds = propertyRecords
164
+ .map((record) => record.schemaFileId)
165
+ .filter((id) => id !== null && id !== undefined);
166
+ if (propertyFileIds.length > 0) {
167
+ await createPropertyInstances(propertyFileIds);
168
+ }
169
+ // Generate schemaFileId if not set
170
+ if (!schemaFileId) {
171
+ schemaFileId = generateId();
172
+ logger$7(`Generated id (schemaFileId) "${schemaFileId}" for model "${modelName}"`);
173
+ }
174
+ // Include _dbId from database record so properties can be created if needed
175
+ // Even if we found an existing model, we may still need to create properties
176
+ // if _pendingPropertyDefinitions are provided
177
+ // Track conflict detection metadata
178
+ const loadedAt = Date.now();
179
+ // Note: models table doesn't have updatedAt or version, so we use loadedAt for _dbUpdatedAt
180
+ // In the future, if models table gets these fields, we should use them
181
+ const dbUpdatedAt = loadedAt;
182
+ // CRITICAL: Include _liveQueryPropertyIds in loadOrCreateModelSuccess event
183
+ // This ensures the properties getter works immediately, even before _setupLiveQuerySubscription runs
184
+ // We already have the property IDs from the database query, so include them now
185
+ sendBack({
186
+ type: 'loadOrCreateModelSuccess',
187
+ model: {
188
+ id: schemaFileId, // schemaFileId (string) - public ID
189
+ _dbId: modelRecord.id, // Database integer ID - internal only
190
+ _liveQueryPropertyIds: propertyFileIds, // Property IDs from database query
191
+ _propertiesUpdated: Date.now(), // Timestamp for tracking
192
+ _isEdited: modelRecord.isEdited ?? false, // Load isEdited from database
193
+ _loadedAt: loadedAt,
194
+ _dbUpdatedAt: dbUpdatedAt,
195
+ },
196
+ });
197
+ return;
198
+ }
199
+ }
200
+ catch (error) {
201
+ logger$7(`Error loading model from database: ${error}`);
202
+ // Fall through to create new model
203
+ }
204
+ }
205
+ // Step 2: Fallback to Schema context (only if database doesn't have the model)
206
+ // This handles the case where model exists in schema file but not yet in database
207
+ try {
208
+ const _mod_0 = await import('./Schema-BYM-GNdE.js');
209
+ const _ns_0 = _mod_0.b;
210
+ const schemaMod = _ns_0;
211
+ const { Schema } = schemaMod;
212
+ const schema = Schema.create(schemaName, { waitForReady: false });
213
+ const schemaSnapshot = schema.getService().getSnapshot();
214
+ // Wait for schema to load if it's still loading
215
+ // Check if state is a loading state object (XState v5 nested states)
216
+ const isLoading = typeof schemaSnapshot.value === 'object' && 'loading' in schemaSnapshot.value;
217
+ if (isLoading) {
218
+ await new Promise((resolve, reject) => {
219
+ const subscription = schema.getService().subscribe((snapshot) => {
220
+ if (snapshot.value === 'idle' || snapshot.value === 'error') {
221
+ subscription.unsubscribe();
222
+ if (snapshot.value === 'error') {
223
+ reject(new Error('Schema failed to load'));
224
+ }
225
+ else {
226
+ resolve();
227
+ }
228
+ }
229
+ });
230
+ });
231
+ }
232
+ const schemaContext = schema.getService().getSnapshot().context;
233
+ // Check by name first; also check by schemaFileId so we recognize the schema model even when context is keyed differently or not yet updated
234
+ if (schemaContext.models) {
235
+ const byName = schemaContext.models[modelName];
236
+ const schemaModelId = byName?.id ?? null;
237
+ if (schemaFileId === schemaModelId) {
238
+ logger$7(`Found model "${modelName}" in Schema context (database fallback)`);
239
+ if (!schemaFileId) {
240
+ schemaFileId = generateId();
241
+ logger$7(`Generated id (schemaFileId) "${schemaFileId}" for model "${modelName}"`);
242
+ }
243
+ sendBack({
244
+ type: 'loadOrCreateModelSuccess',
245
+ model: { id: schemaFileId },
246
+ });
247
+ return;
248
+ }
249
+ // Same name but different id: runtime create colliding with schema model name; fall through
250
+ if (byName) {
251
+ existingNumbersFromSchemaContext = new Set([0]);
252
+ }
253
+ else if (schemaFileId) {
254
+ // Schema context may not have model by name yet (e.g. still loading). If our id appears in any schema model, we're the schema model.
255
+ for (const def of Object.values(schemaContext.models)) {
256
+ if (def?.id === schemaFileId) {
257
+ logger$7(`Found model in Schema context by id "${schemaFileId}" (database fallback)`);
258
+ sendBack({
259
+ type: 'loadOrCreateModelSuccess',
260
+ model: { id: schemaFileId },
261
+ });
262
+ return;
263
+ }
264
+ }
265
+ }
266
+ }
267
+ }
268
+ catch (error) {
269
+ logger$7(`Error loading model from Schema context: ${error}`);
270
+ // Fall through to create new model
271
+ }
272
+ // Step 3: Check for duplicate names in database before creating new model
273
+ // Skip duplicate check only if we found a model record to use (modelRecord is set)
274
+ // OR if the model was found by schemaFileId (preserving original name from schema file)
275
+ // OR if this is an internal schema (Seed Protocol) - internal schemas should preserve original names
276
+ // Otherwise, check for duplicates when creating a new model
277
+ // NOTE: We should NOT skip duplicate check just because schemaFileId exists - that could be a newly generated ID
278
+ // for a new model that needs to be renamed if it's a duplicate
279
+ let finalModelName = modelName;
280
+ if (db) {
281
+ // Check if this is an internal schema (Seed Protocol)
282
+ const isInternal = isInternalSchema(schemaName);
283
+ // Only skip duplicate check if:
284
+ // 1. We found a model record to use (modelRecord is set), OR
285
+ // 2. The model was found by schemaFileId (preserving original name from schema file), OR
286
+ // 3. This is an internal schema (internal schemas should preserve original names from schema files), OR
287
+ // 4. This instance was created with modelFileId from schema (_idFromSchema) - schema models keep their name
288
+ const shouldSkipDuplicateCheck = modelRecord !== null || foundBySchemaFileId || isInternal || !!_idFromSchema;
289
+ logger$7(`Duplicate check: modelRecord=${modelRecord !== null}, foundBySchemaFileId=${foundBySchemaFileId}, isInternal=${isInternal}, _idFromSchema=${!!_idFromSchema}, schemaFileId=${schemaFileId}, shouldSkip=${shouldSkipDuplicateCheck}`);
290
+ if (!shouldSkipDuplicateCheck) {
291
+ try {
292
+ const lowerModelName = modelName.toLowerCase();
293
+ const existingNumbers = new Set(existingNumbersFromSchemaContext ?? []);
294
+ // First, check Model cache for models (includes models from imported schemas that may not be in DB yet)
295
+ try {
296
+ const modelMod = await Promise.resolve().then(function () { return Model$1; });
297
+ const { Model } = modelMod;
298
+ // Check name-based cache for this schema
299
+ // Skip the current model (context.id): Model.create() adds the new instance to the cache
300
+ // before this callback runs, so we must not count it as a duplicate.
301
+ logger$7(`Checking Model cache for duplicates in schema "${schemaName}"`);
302
+ const currentModelFileId = schemaFileId ?? null;
303
+ const cacheByName = Model.instanceCacheByName;
304
+ for (const [nameKey, cachedModelFileId] of cacheByName.entries()) {
305
+ if (currentModelFileId && cachedModelFileId === currentModelFileId)
306
+ continue; // skip self
307
+ const [cachedSchemaName, cachedModelName] = nameKey.split(':', 2);
308
+ if (cachedSchemaName === schemaName && cachedModelName) {
309
+ const lowerCachedName = cachedModelName.toLowerCase();
310
+ if (lowerCachedName === lowerModelName) {
311
+ existingNumbers.add(0); // Base name exists
312
+ logger$7(`Found duplicate in Model cache: "${cachedModelName}" matches "${modelName}"`);
313
+ }
314
+ else if (lowerCachedName.startsWith(lowerModelName + ' ')) {
315
+ // Check if it's the base name followed by a space and a number
316
+ const suffix = lowerCachedName.slice(lowerModelName.length + 1);
317
+ const number = parseInt(suffix, 10);
318
+ if (!isNaN(number) && suffix === number.toString()) {
319
+ existingNumbers.add(number);
320
+ logger$7(`Found numbered variant in Model cache: "${cachedModelName}" (number: ${number})`);
321
+ }
322
+ }
323
+ }
324
+ }
325
+ // Also check legacy cache (skip current model by id)
326
+ const legacyCache = Model.instanceCache;
327
+ for (const [nameKey, entry] of legacyCache?.entries() || []) {
328
+ const legacyId = entry?.instance?._getSnapshotContext?.()?.id;
329
+ if (currentModelFileId && legacyId === currentModelFileId)
330
+ continue; // skip self
331
+ const [cachedSchemaName, cachedModelName] = nameKey.split(':', 2);
332
+ if (cachedSchemaName === schemaName && cachedModelName) {
333
+ const lowerCachedName = cachedModelName.toLowerCase();
334
+ if (lowerCachedName === lowerModelName) {
335
+ existingNumbers.add(0); // Base name exists
336
+ logger$7(`Found duplicate in Model legacy cache: "${cachedModelName}" matches "${modelName}"`);
337
+ }
338
+ else if (lowerCachedName.startsWith(lowerModelName + ' ')) {
339
+ // Check if it's the base name followed by a space and a number
340
+ const suffix = lowerCachedName.slice(lowerModelName.length + 1);
341
+ const number = parseInt(suffix, 10);
342
+ if (!isNaN(number) && suffix === number.toString()) {
343
+ existingNumbers.add(number);
344
+ logger$7(`Found numbered variant in Model legacy cache: "${cachedModelName}" (number: ${number})`);
345
+ }
346
+ }
347
+ }
348
+ }
349
+ }
350
+ catch (error) {
351
+ logger$7(`Error checking Model cache for duplicates: ${error}`);
352
+ // Continue with database check
353
+ }
354
+ // Also check database for models (in case they're persisted but not in Schema context)
355
+ if (db) {
356
+ const _mod_1 = await import('./BasePathResolver-qV0apdVY.js');
357
+ const _ns_1 = _mod_1.N;
358
+ const modelSchemaSchemaMod = _ns_1;
359
+ const { modelSchemas } = modelSchemaSchemaMod;
360
+ const _mod_2 = await import('./BasePathResolver-qV0apdVY.js');
361
+ const _ns_2 = _mod_2.L;
362
+ const schemaSchemaMod = _ns_2;
363
+ const { schemas: schemasTable } = schemaSchemaMod;
364
+ // Query all models for this schema to check for duplicates (case-insensitive)
365
+ logger$7(`Checking database for duplicate model names in schema "${schemaName}"`);
366
+ const allModelsForSchema = await db
367
+ .select({
368
+ name: models.name,
369
+ })
370
+ .from(models)
371
+ .innerJoin(modelSchemas, eq(models.id, modelSchemas.modelId))
372
+ .innerJoin(schemasTable, eq(modelSchemas.schemaId, schemasTable.id))
373
+ .where(eq(schemasTable.name, schemaName));
374
+ logger$7(`Found ${allModelsForSchema.length} models in database for schema "${schemaName}": ${allModelsForSchema.map((m) => m.name).join(', ')}`);
375
+ if (allModelsForSchema.length === 0 && modelName.includes('Shared')) {
376
+ logger$7(`[DEBUG] No models found in database for schema "${schemaName}" when checking for duplicate "${modelName}"`);
377
+ }
378
+ // Check for exact match (case-insensitive) or matches with number suffix
379
+ for (const dbModel of allModelsForSchema) {
380
+ const lowerDbName = dbModel.name.toLowerCase();
381
+ if (lowerDbName === lowerModelName) {
382
+ existingNumbers.add(0); // Base name exists
383
+ logger$7(`Found duplicate in database: "${dbModel.name}" matches "${modelName}"`);
384
+ }
385
+ else if (lowerDbName.startsWith(lowerModelName + ' ')) {
386
+ // Check if it's the base name followed by a space and a number
387
+ const suffix = lowerDbName.slice(lowerModelName.length + 1);
388
+ const number = parseInt(suffix, 10);
389
+ if (!isNaN(number) && suffix === number.toString()) {
390
+ existingNumbers.add(number);
391
+ logger$7(`Found numbered variant in database: "${dbModel.name}" (number: ${number})`);
392
+ }
393
+ }
394
+ }
395
+ }
396
+ // If duplicates found, generate unique name
397
+ logger$7(`Duplicate check results: existingNumbers=${Array.from(existingNumbers).join(', ')}, hasBaseName=${existingNumbers.has(0)}`);
398
+ if (existingNumbers.has(0)) {
399
+ let nextNumber = 1;
400
+ while (existingNumbers.has(nextNumber)) {
401
+ nextNumber++;
402
+ }
403
+ finalModelName = `${modelName} ${nextNumber}`;
404
+ logger$7(`Found duplicate model name "${modelName}" in schema "${schemaName}", using unique name "${finalModelName}"`);
405
+ }
406
+ else {
407
+ logger$7(`No duplicate found for model name "${modelName}" in schema "${schemaName}"`);
408
+ }
409
+ }
410
+ catch (error) {
411
+ logger$7(`Error checking for duplicate model names: ${error}`);
412
+ // Continue with original name if check fails
413
+ }
414
+ }
415
+ else {
416
+ if (foundBySchemaFileId) {
417
+ logger$7(`Preserving original model name "${modelName}" for model found by schemaFileId (schemaFileId: ${schemaFileId})`);
418
+ }
419
+ else if (modelRecord) {
420
+ logger$7(`Using existing model name "${modelName}" from database (modelRecord found)`);
421
+ }
422
+ else if (isInternal) {
423
+ logger$7(`Preserving original model name "${modelName}" for internal schema "${schemaName}" (skipping duplicate check)`);
424
+ }
425
+ }
426
+ }
427
+ // Step 4: Create new empty model (not found in database or Schema context)
428
+ logger$7(`Creating new model "${finalModelName}" in schema "${schemaName}"`);
429
+ // Generate schemaFileId for new model
430
+ if (!schemaFileId) {
431
+ schemaFileId = generateId();
432
+ logger$7(`Generated id (schemaFileId) "${schemaFileId}" for new model "${finalModelName}"`);
433
+ }
434
+ // Runtime creates use a placeholder cache key in Model.create; move it to the final name. Skip when schema model (_idFromSchema).
435
+ if (!_idFromSchema) {
436
+ const modelMod = await Promise.resolve().then(function () { return Model$1; });
437
+ const { Model } = modelMod;
438
+ Model.updateNameIndex('__pending__' + schemaFileId, finalModelName, schemaName, schemaFileId);
439
+ }
440
+ // If model name was changed, store it in a temporary internal field so loadOrCreateModelSuccess can apply it
441
+ // This avoids the issue where updateContext with modelName triggers validation and state transition
442
+ // We use _pendingModelName (internal field) which won't trigger validation
443
+ if (finalModelName !== modelName) {
444
+ logger$7(`Model name changed from "${modelName}" to "${finalModelName}", storing in _pendingModelName for loadOrCreateModelSuccess`);
445
+ // Store the final name in a temporary internal field that will be picked up by loadOrCreateModelSuccess
446
+ sendBack({
447
+ type: 'updateContext',
448
+ _pendingModelName: finalModelName,
449
+ });
450
+ }
451
+ else {
452
+ logger$7(`Model name unchanged: "${modelName}"`);
453
+ }
454
+ // Mark schema as draft when a new model is created so saveNewVersion() can persist it
455
+ try {
456
+ const _mod_3 = await import('./Schema-BYM-GNdE.js');
457
+ const _ns_3 = _mod_3.b;
458
+ const schemaMod = _ns_3;
459
+ const { Schema } = schemaMod;
460
+ const schema = Schema.create(schemaName, { waitForReady: false });
461
+ schema.getService().send({ type: 'markAsDraft', propertyKey: 'schema:models' });
462
+ }
463
+ catch (err) {
464
+ logger$7(`Failed to mark schema as draft after creating model: ${err}`);
465
+ }
466
+ sendBack({
467
+ type: 'loadOrCreateModelSuccess',
468
+ model: {
469
+ id: schemaFileId, // schemaFileId (string) - public ID
470
+ },
471
+ });
472
+ };
473
+ _loadOrCreateModel().catch((error) => {
474
+ logger$7('Error loading or creating model:', error);
475
+ sendBack({ type: 'loadOrCreateModelError', error });
476
+ });
477
+ });
478
+
479
+ const logger$6 = debug('seedSdk:model:actors:validateModel');
480
+ // Validation timeout in milliseconds (10 seconds)
481
+ const VALIDATION_TIMEOUT_MS = 10000;
482
+ const validateModel = fromCallback(({ sendBack, input: { context } }) => {
483
+ let hasResponded = false;
484
+ // Timeout handler to ensure we always respond
485
+ const timeoutId = setTimeout(() => {
486
+ if (!hasResponded) {
487
+ hasResponded = true;
488
+ sendBack({
489
+ type: 'validationError',
490
+ errors: [{
491
+ field: 'model',
492
+ message: `Model validation timed out after ${VALIDATION_TIMEOUT_MS}ms`,
493
+ code: 'validation_timeout',
494
+ severity: 'error',
495
+ }],
496
+ });
497
+ }
498
+ }, VALIDATION_TIMEOUT_MS);
499
+ const _validateModel = async () => {
500
+ try {
501
+ // Use dynamic imports to break circular dependencies
502
+ const validationServiceMod = await import('./SchemaValidationService-Dr6nmwPm.js');
503
+ const { SchemaValidationService } = validationServiceMod;
504
+ const validationService = new SchemaValidationService();
505
+ const _mod_4 = await import('./Schema-BYM-GNdE.js');
506
+ const _ns_4 = _mod_4.b;
507
+ const schemaMod = _ns_4;
508
+ const { Schema } = schemaMod;
509
+ // Validate model structure
510
+ const structureResult = validationService.validateModelStructure(context);
511
+ if (!structureResult.isValid) {
512
+ if (!hasResponded) {
513
+ hasResponded = true;
514
+ clearTimeout(timeoutId);
515
+ sendBack({ type: 'validationError', errors: structureResult.errors });
516
+ }
517
+ return;
518
+ }
519
+ // If we have schema name, validate against schema
520
+ // CRITICAL: Use validateModelAgainstSchema which doesn't require the model to be in schema context
521
+ // This allows validation BEFORE registration, preventing update loops
522
+ if (context.schemaName) {
523
+ try {
524
+ const schema = Schema.create(context.schemaName, {
525
+ waitForReady: false,
526
+ });
527
+ const schemaSnapshot = schema.getService().getSnapshot();
528
+ const schemaStatus = schemaSnapshot.value;
529
+ // Only validate against schema if it's loaded (in idle state)
530
+ if (schemaStatus === 'idle') {
531
+ const schemaContext = schemaSnapshot.context;
532
+ // Use validateModelAgainstSchema which validates the model data directly
533
+ // without requiring it to be in schema.models. This prevents loops because:
534
+ // 1. Model validates against schema (model NOT in schema context yet)
535
+ // 2. If valid, model gets registered with schema
536
+ // 3. No need for manual updates because model is already correct (liveQuery handles updates)
537
+ const schemaResult = validationService.validateModelAgainstSchema(schemaContext, context.modelName, context);
538
+ if (!schemaResult.isValid) {
539
+ if (!hasResponded) {
540
+ hasResponded = true;
541
+ clearTimeout(timeoutId);
542
+ sendBack({ type: 'validationError', errors: schemaResult.errors });
543
+ }
544
+ return;
545
+ }
546
+ }
547
+ else {
548
+ logger$6(`Schema is in ${schemaStatus} state, skipping schema validation`);
549
+ // Continue with structure validation only
550
+ }
551
+ }
552
+ catch (error) {
553
+ logger$6('Error validating model against schema:', error);
554
+ // Continue with structure validation only
555
+ }
556
+ }
557
+ // All validations passed
558
+ if (!hasResponded) {
559
+ hasResponded = true;
560
+ clearTimeout(timeoutId);
561
+ sendBack({ type: 'validationSuccess', errors: [] });
562
+ }
563
+ }
564
+ catch (error) {
565
+ if (!hasResponded) {
566
+ hasResponded = true;
567
+ clearTimeout(timeoutId);
568
+ logger$6('Error in validateModel:', error);
569
+ sendBack({
570
+ type: 'validationError',
571
+ errors: [{
572
+ field: 'model',
573
+ message: error instanceof Error ? error.message : 'Unknown validation error',
574
+ code: 'validation_exception',
575
+ severity: 'error',
576
+ }],
577
+ });
578
+ }
579
+ }
580
+ };
581
+ _validateModel().catch((error) => {
582
+ if (!hasResponded) {
583
+ hasResponded = true;
584
+ clearTimeout(timeoutId);
585
+ logger$6('Error in validateModel:', error);
586
+ sendBack({
587
+ type: 'validationError',
588
+ errors: [{
589
+ field: 'model',
590
+ message: error instanceof Error ? error.message : 'Unknown validation error',
591
+ code: 'validation_exception',
592
+ severity: 'error',
593
+ }],
594
+ });
595
+ }
596
+ });
597
+ return () => {
598
+ // Cleanup function - clear timeout if actor is stopped
599
+ clearTimeout(timeoutId);
600
+ };
601
+ });
602
+
603
+ const logger$5 = debug('seedSdk:model:actors:createModelProperties');
604
+ const createModelProperties = fromCallback(({ sendBack, input }) => {
605
+ const _createProperties = async () => {
606
+ const { context, propertyDefinitions } = input;
607
+ const { id, _dbId, modelName } = context;
608
+ if (!id || !_dbId || !modelName) {
609
+ throw new Error('Model ID, file ID, and name are required to create properties');
610
+ }
611
+ if (!propertyDefinitions || Object.keys(propertyDefinitions).length === 0) {
612
+ logger$5('No property definitions provided, skipping property creation');
613
+ sendBack({ type: 'createModelPropertiesSuccess' });
614
+ return;
615
+ }
616
+ logger$5(`Creating ${Object.keys(propertyDefinitions).length} properties for model "${modelName}" (id: ${_dbId})`);
617
+ const mod = await import('./ModelProperty-DuB5nk6A.js');
618
+ const ModelProperty = mod?.ModelProperty ?? mod?.default;
619
+ if (!ModelProperty) {
620
+ logger$5('ModelProperty not available from dynamic import');
621
+ sendBack({ type: 'createModelPropertiesError', error: new Error('ModelProperty not available') });
622
+ return;
623
+ }
624
+ const drizzleMod = await import('drizzle-orm');
625
+ const { eq } = drizzleMod;
626
+ const db = BaseDb.getAppDb();
627
+ for (const [propName, propData] of Object.entries(propertyDefinitions)) {
628
+ // Use provided ID or generate a random one
629
+ // IDs should be generated in the import process before creating properties
630
+ const { generateId } = await import('./generateId-BopUwD8F.js');
631
+ const propertyFileId = propData.id || generateId();
632
+ logger$5(`Creating property "${propName}" with fileId "${propertyFileId}"`);
633
+ // Query database to get refModelId if the property has ref/refModelName
634
+ // The property should already be in DB from addModelsToDb
635
+ let refModelId = propData.refModelId;
636
+ if (!refModelId && (propData.ref || propData.refModelName) && db) {
637
+ try {
638
+ const refModelName = propData.refModelName || propData.ref;
639
+ // First try to get refModelId from the property record in the database
640
+ const propertyRecords = await db
641
+ .select()
642
+ .from(properties)
643
+ .where(eq(properties.schemaFileId, propertyFileId))
644
+ .limit(1);
645
+ if (propertyRecords.length > 0 && propertyRecords[0].refModelId) {
646
+ refModelId = propertyRecords[0].refModelId;
647
+ logger$5(`Found refModelId ${refModelId} from database for property "${propName}"`);
648
+ }
649
+ else if (refModelName) {
650
+ // Fallback: query models table directly by name
651
+ const refModelRecords = await db
652
+ .select()
653
+ .from(models)
654
+ .where(eq(models.name, refModelName))
655
+ .limit(1);
656
+ if (refModelRecords.length > 0 && refModelRecords[0].id) {
657
+ refModelId = refModelRecords[0].id;
658
+ logger$5(`Resolved refModelId ${refModelId} from model name "${refModelName}" for property "${propName}"`);
659
+ }
660
+ }
661
+ }
662
+ catch (error) {
663
+ logger$5(`Error fetching refModelId for property "${propName}":`, error);
664
+ // Continue without refModelId - it will be resolved later in _initializeOriginalValues
665
+ }
666
+ }
667
+ // Create ModelProperty instance
668
+ // This will load from DB if it exists, or create new instance
669
+ // The property should already be in DB from writeModelToDb
670
+ // Note: propertyFileId is the schemaFileId (string), not the database ID (number)
671
+ // We pass it as _propertyFileId so getById() can find the instance
672
+ await ModelProperty.create({
673
+ name: propName,
674
+ modelName,
675
+ modelId: _dbId,
676
+ dataType: propData.dataType,
677
+ ref: propData.ref,
678
+ refModelName: propData.refModelName,
679
+ refModelId,
680
+ refValueType: propData.refValueType,
681
+ storageType: propData.storageType,
682
+ localStorageDir: propData.localStorageDir,
683
+ filenameSuffix: propData.filenameSuffix,
684
+ required: propData.required,
685
+ _propertyFileId: propertyFileId, // Store schemaFileId for getById() lookups
686
+ }); // Use 'as any' because _propertyFileId is not in TProperty type
687
+ }
688
+ logger$5(`Successfully created all properties for model "${modelName}"`);
689
+ sendBack({ type: 'createModelPropertiesSuccess' });
690
+ };
691
+ _createProperties().catch((error) => {
692
+ logger$5(`Error creating model properties: ${error}`);
693
+ sendBack({
694
+ type: 'createModelPropertiesError',
695
+ error: error instanceof Error ? error : new Error(String(error))
696
+ });
697
+ });
698
+ return () => {
699
+ // Cleanup function (optional)
700
+ };
701
+ });
702
+
703
+ const logger$4 = debug('seedSdk:write:validateEntity');
704
+ // XState v5 type inference bug: fromPromise types are inverted
705
+ // We use type assertion to work around this
706
+ const validateEntity = fromPromise(async ({ input }) => {
707
+ // Type assertion to fix XState v5 type inference bug
708
+ // Convert through unknown to avoid type overlap error
709
+ const entityInput = input;
710
+ const _validate = async () => {
711
+ try {
712
+ const msg = `[validateEntity] Starting validation for ${entityInput.entityType}`;
713
+ logger$4(msg);
714
+ logger$4(`[validateEntity] Entity data:`, entityInput.entityData);
715
+ let result = { isValid: true, errors: [] };
716
+ if (entityInput.entityType === 'model') {
717
+ const structureMsg = `[validateEntity] Validating model structure`;
718
+ logger$4(structureMsg);
719
+ // Use existing Model validation
720
+ const validationServiceMod = await import('./SchemaValidationService-Dr6nmwPm.js');
721
+ const { SchemaValidationService } = validationServiceMod;
722
+ const validationService = new SchemaValidationService();
723
+ // Validate model structure
724
+ const structureResult = validationService.validateModelStructure(entityInput.entityData);
725
+ const structureResultMsg = `[validateEntity] Structure validation result: isValid=${structureResult.isValid}, errors=${structureResult.errors?.length || 0}`;
726
+ logger$4(structureResultMsg);
727
+ if (!structureResult.isValid) {
728
+ result = {
729
+ isValid: false,
730
+ errors: structureResult.errors,
731
+ };
732
+ }
733
+ else {
734
+ // If schema name provided, validate against schema
735
+ if (entityInput.entityData.schemaName) {
736
+ try {
737
+ logger$4(`[validateEntity] Validating model against schema "${entityInput.entityData.schemaName}"`);
738
+ const _mod_5 = await import('./Schema-BYM-GNdE.js');
739
+ const _ns_5 = _mod_5.b;
740
+ const schemaMod = _ns_5;
741
+ const { Schema } = schemaMod;
742
+ const schema = Schema.create(entityInput.entityData.schemaName, {
743
+ waitForReady: false,
744
+ });
745
+ const schemaSnapshot = schema.getService().getSnapshot();
746
+ const schemaStatus = schemaSnapshot.value;
747
+ logger$4(`[validateEntity] Schema status: ${schemaStatus}`);
748
+ if (schemaStatus === 'idle') {
749
+ const schemaContext = schemaSnapshot.context;
750
+ logger$4(`[validateEntity] Running validateModelAgainstSchema`);
751
+ const schemaResult = validationService.validateModelAgainstSchema(schemaContext, entityInput.entityData.modelName, entityInput.entityData);
752
+ logger$4(`[validateEntity] Schema validation result:`, schemaResult);
753
+ if (!schemaResult.isValid) {
754
+ result = {
755
+ isValid: false,
756
+ errors: schemaResult.errors,
757
+ };
758
+ }
759
+ }
760
+ else {
761
+ logger$4(`[validateEntity] Schema not in idle state (${schemaStatus}), skipping schema validation`);
762
+ }
763
+ }
764
+ catch (error) {
765
+ logger$4('Error validating model against schema:', error);
766
+ // Continue with structure validation only
767
+ }
768
+ }
769
+ else {
770
+ logger$4(`[validateEntity] No schemaName provided, skipping schema validation`);
771
+ }
772
+ }
773
+ }
774
+ else if (entityInput.entityType === 'modelProperty') {
775
+ // Use existing ModelProperty validation
776
+ const validationServiceMod = await import('./SchemaValidationService-Dr6nmwPm.js');
777
+ const { SchemaValidationService } = validationServiceMod;
778
+ const validationService = new SchemaValidationService();
779
+ // Validate property structure
780
+ const structureResult = validationService.validatePropertyStructure(entityInput.entityData);
781
+ if (!structureResult.isValid) {
782
+ result = {
783
+ isValid: false,
784
+ errors: structureResult.errors,
785
+ };
786
+ }
787
+ else {
788
+ // If schema name and model name provided, validate against schema
789
+ if (entityInput.entityData._schemaName && entityInput.entityData.modelName) {
790
+ try {
791
+ const _mod_6 = await import('./Schema-BYM-GNdE.js');
792
+ const _ns_6 = _mod_6.b;
793
+ const schemaMod = _ns_6;
794
+ const { Schema } = schemaMod;
795
+ const schema = Schema.create(entityInput.entityData._schemaName, {
796
+ waitForReady: false,
797
+ });
798
+ const schemaSnapshot = schema.getService().getSnapshot();
799
+ const schemaStatus = schemaSnapshot.value;
800
+ if (schemaStatus === 'idle') {
801
+ const schemaContext = schemaSnapshot.context;
802
+ if (schemaContext.models && Object.keys(schemaContext.models).length > 0) {
803
+ const schemaResult = validationService.validateProperty(schemaContext, entityInput.entityData.modelName, entityInput.entityData.name || '', entityInput.entityData);
804
+ if (!schemaResult.isValid) {
805
+ result = {
806
+ isValid: false,
807
+ errors: schemaResult.errors,
808
+ };
809
+ }
810
+ }
811
+ }
812
+ }
813
+ catch (error) {
814
+ logger$4('Error validating property against schema:', error);
815
+ // Continue with structure validation only
816
+ }
817
+ }
818
+ }
819
+ }
820
+ else if (entityInput.entityType === 'schema') {
821
+ // Schema validation - use existing validation
822
+ const validationServiceMod = await import('./SchemaValidationService-Dr6nmwPm.js');
823
+ const { SchemaValidationService } = validationServiceMod;
824
+ const validationService = new SchemaValidationService();
825
+ const schemaResult = validationService.validateSchema(entityInput.entityData);
826
+ if (!schemaResult.isValid) {
827
+ result = {
828
+ isValid: false,
829
+ errors: schemaResult.errors,
830
+ };
831
+ }
832
+ }
833
+ // Return the result - fromPromise will automatically put this in event.output
834
+ const completeMsg = `[validateEntity] Validation complete: isValid=${result.isValid}, errors=${result.errors?.length || 0}`;
835
+ logger$4(completeMsg);
836
+ logger$4(`[validateEntity] Validation complete:`, result);
837
+ return result;
838
+ }
839
+ catch (error) {
840
+ const errorMsg = `[validateEntity] Error in validateEntity: ${error instanceof Error ? error.message : String(error)}`;
841
+ logger$4(errorMsg);
842
+ logger$4('Error in validateEntity:', error);
843
+ // Throw error - fromPromise will automatically create error.platform event
844
+ throw error;
845
+ }
846
+ };
847
+ return await _validate();
848
+ // @ts-ignore - XState v5 type inference bug: fromPromise incorrectly expects output type to match input type
849
+ });
850
+
851
+ const logger$3 = debug('seedSdk:write:writeToDatabase');
852
+ const writeToDatabase = fromCallback(({ sendBack, input }) => {
853
+ const _write = async () => {
854
+ try {
855
+ const db = BaseDb.getAppDb();
856
+ if (!db) {
857
+ throw new Error('Database not available');
858
+ }
859
+ let output = input.entityData;
860
+ if (input.entityType === 'model') {
861
+ const writeMsg = `Writing model to database: ${input.entityData.modelName} (schemaId: ${input.entityData.schemaId})`;
862
+ logger$3(writeMsg);
863
+ logger$3(`Writing model to database:`, {
864
+ modelFileId: input.entityId,
865
+ modelName: input.entityData.modelName,
866
+ schemaId: input.entityData.schemaId,
867
+ });
868
+ await writeModelToDb(input.entityId, input.entityData);
869
+ // Get _dbId from database after write
870
+ const db = BaseDb.getAppDb();
871
+ if (db) {
872
+ const modelRecords = await db
873
+ .select({ id: models.id })
874
+ .from(models)
875
+ .where(eq(models.schemaFileId, input.entityId)) // entityId is now the schemaFileId (string)
876
+ .limit(1);
877
+ if (modelRecords.length > 0 && modelRecords[0].id) {
878
+ output = { ...input.entityData, _dbId: modelRecords[0].id }; // Store as _dbId (database integer ID)
879
+ }
880
+ else {
881
+ output = input.entityData;
882
+ }
883
+ }
884
+ else {
885
+ output = input.entityData;
886
+ }
887
+ const successMsg = `Successfully wrote model "${input.entityData.modelName}" to database`;
888
+ logger$3(successMsg);
889
+ }
890
+ else if (input.entityType === 'modelProperty') {
891
+ // Use current ModelProperty context when available so a user rename before the
892
+ // initial write completes is not overwritten by the stale requestWrite payload.
893
+ let dataToWrite = input.entityData;
894
+ try {
895
+ const mod = await import('./ModelProperty-DuB5nk6A.js');
896
+ const ModelProperty = mod?.ModelProperty ?? mod?.default;
897
+ if (ModelProperty && typeof ModelProperty.getById === 'function') {
898
+ const instance = ModelProperty.getById(input.entityId);
899
+ if (instance && typeof instance._getSnapshotContext === 'function') {
900
+ const ctx = instance._getSnapshotContext();
901
+ const ctxName = ctx.name ?? input.entityData.name;
902
+ dataToWrite = {
903
+ ...input.entityData,
904
+ name: ctxName,
905
+ dataType: ctx.dataType ?? input.entityData.dataType,
906
+ refModelId: ctx.refModelId ?? input.entityData.refModelId,
907
+ refValueType: ctx.refValueType ?? input.entityData.refValueType,
908
+ refModelName: ctx.refModelName ?? input.entityData.refModelName,
909
+ storageType: ctx.storageType ?? input.entityData.storageType,
910
+ localStorageDir: ctx.localStorageDir ?? input.entityData.localStorageDir,
911
+ filenameSuffix: ctx.filenameSuffix ?? input.entityData.filenameSuffix,
912
+ };
913
+ }
914
+ }
915
+ }
916
+ catch (_) {
917
+ // Fall back to input.entityData if instance not available
918
+ }
919
+ await writePropertyToDb(input.entityId, dataToWrite);
920
+ output = input.entityData;
921
+ }
922
+ else if (input.entityType === 'schema') {
923
+ const schemaRecord = await addSchemaToDb(input.entityData, input.entityId, // schemaFileId (string)
924
+ input.entityData.schemaData, input.entityData.isDraft);
925
+ output = { ...input.entityData, _dbId: schemaRecord.id }; // Store as _dbId (database integer ID)
926
+ }
927
+ else {
928
+ throw new Error(`Unknown entity type: ${input.entityType}`);
929
+ }
930
+ // Send success event explicitly - callback actors don't trigger onDone
931
+ sendBack({
932
+ type: 'writeSuccess',
933
+ output,
934
+ });
935
+ }
936
+ catch (error) {
937
+ logger$3('Error in writeToDatabase:', error);
938
+ sendBack({
939
+ type: 'writeError',
940
+ error: error instanceof Error ? error : new Error(String(error)),
941
+ });
942
+ }
943
+ };
944
+ _write().catch((error) => {
945
+ logger$3('Error in writeToDatabase promise:', error);
946
+ sendBack({
947
+ type: 'writeError',
948
+ error: error instanceof Error ? error : new Error(String(error)),
949
+ });
950
+ });
951
+ return () => {
952
+ // Cleanup function (optional)
953
+ };
954
+ });
955
+
956
+ const logger$2 = debug('seedSdk:write:writeProcessMachine');
957
+ const writeProcessMachine = setup({
958
+ types: {
959
+ context: {},
960
+ input: {},
961
+ events: {},
962
+ },
963
+ actors: {
964
+ validateEntity,
965
+ writeToDatabase,
966
+ },
967
+ }).createMachine({
968
+ id: 'writeProcess',
969
+ initial: 'idle',
970
+ context: ({ input }) => ({
971
+ entityType: input.entityType,
972
+ entityId: input.entityId,
973
+ entityData: input.entityData || {},
974
+ validationErrors: [],
975
+ writeStatus: 'idle',
976
+ error: null,
977
+ retryCount: 0,
978
+ pendingWrite: null,
979
+ }),
980
+ states: {
981
+ idle: {
982
+ on: {
983
+ startWrite: {
984
+ target: 'validating',
985
+ actions: [
986
+ assign({
987
+ pendingWrite: ({ event }) => ({
988
+ data: event.data,
989
+ timestamp: Date.now(),
990
+ }),
991
+ writeStatus: 'validating',
992
+ }),
993
+ ({ event, context }) => {
994
+ logger$2(`[startWrite] Received startWrite for ${context.entityType} "${context.entityId}"`);
995
+ logger$2(`[startWrite] Data:`, event.data);
996
+ },
997
+ ],
998
+ },
999
+ },
1000
+ },
1001
+ validating: {
1002
+ entry: ({ context }) => {
1003
+ const msg = `[validating] Entering validating state for ${context.entityType} "${context.entityId}"`;
1004
+ logger$2(msg);
1005
+ logger$2(`[validating] Entity data:`, context.pendingWrite?.data || context.entityData);
1006
+ },
1007
+ invoke: {
1008
+ src: 'validateEntity',
1009
+ input: ({ context }) => {
1010
+ const entityData = context.pendingWrite?.data || context.entityData;
1011
+ logger$2(`[validating] Invoking validateEntity with:`, { entityType: context.entityType, entityData });
1012
+ return {
1013
+ entityType: context.entityType,
1014
+ entityData,
1015
+ };
1016
+ },
1017
+ onDone: [
1018
+ {
1019
+ target: 'writing',
1020
+ guard: ({ event }) => {
1021
+ // The output might be the result directly, or it might be wrapped in an event
1022
+ let result;
1023
+ if (event.output && typeof event.output === 'object' && 'isValid' in event.output) {
1024
+ result = event.output;
1025
+ }
1026
+ else if (event.output && typeof event.output === 'object' && 'type' in event.output) {
1027
+ // If it's wrapped in an event, extract the result
1028
+ result = event.output;
1029
+ }
1030
+ else {
1031
+ console.error(`[validating] Unexpected event.output format! Event:`, event);
1032
+ return false;
1033
+ }
1034
+ if (!result || typeof result.isValid !== 'boolean') {
1035
+ console.error(`[validating] Invalid result format! Result:`, result);
1036
+ return false;
1037
+ }
1038
+ const resultMsg = `[validating] Validation result: isValid=${result.isValid}, errors=${result.errors?.length || 0}`;
1039
+ logger$2(resultMsg);
1040
+ logger$2(`[validating] Validation result:`, result);
1041
+ return result.isValid;
1042
+ },
1043
+ actions: [
1044
+ assign({
1045
+ validationErrors: [],
1046
+ writeStatus: 'writing',
1047
+ }),
1048
+ ({ context }) => {
1049
+ const msg = `[validating] Validation passed, transitioning to writing for ${context.entityType} "${context.entityId}"`;
1050
+ logger$2(msg);
1051
+ },
1052
+ ],
1053
+ },
1054
+ {
1055
+ target: 'error',
1056
+ actions: [
1057
+ assign({
1058
+ validationErrors: ({ event }) => {
1059
+ const result = event.output;
1060
+ return result.errors || [];
1061
+ },
1062
+ writeStatus: 'error',
1063
+ error: ({ event }) => {
1064
+ const result = event.output;
1065
+ return new Error(`Validation failed: ${result.errors.map(e => e.message).join(', ')}`);
1066
+ },
1067
+ }),
1068
+ ({ context, event }) => {
1069
+ const result = event.output;
1070
+ logger$2(`[validating] Validation failed for ${context.entityType} "${context.entityId}":`, result.errors);
1071
+ },
1072
+ ],
1073
+ },
1074
+ ],
1075
+ onError: {
1076
+ target: 'error',
1077
+ actions: [
1078
+ assign({
1079
+ error: ({ event }) => event.error instanceof Error ? event.error : new Error(String(event.error)),
1080
+ writeStatus: 'error',
1081
+ }),
1082
+ ({ context, event }) => {
1083
+ const errorMsg = `[validating] Validation error for ${context.entityType} "${context.entityId}": ${event.error}`;
1084
+ logger$2(errorMsg);
1085
+ console.error(errorMsg, event.error); // Always log to console
1086
+ },
1087
+ ],
1088
+ },
1089
+ },
1090
+ on: {
1091
+ reset: 'idle',
1092
+ },
1093
+ },
1094
+ writing: {
1095
+ invoke: {
1096
+ src: 'writeToDatabase',
1097
+ input: ({ context }) => {
1098
+ const entityData = context.pendingWrite?.data || context.entityData;
1099
+ logger$2(`[writing] Calling writeToDatabase for ${context.entityType} "${context.entityId}"`);
1100
+ logger$2(`[writing] Entity data:`, entityData);
1101
+ return {
1102
+ entityType: context.entityType,
1103
+ entityId: context.entityId,
1104
+ entityData,
1105
+ };
1106
+ },
1107
+ },
1108
+ on: {
1109
+ writeSuccess: {
1110
+ target: 'success',
1111
+ actions: [
1112
+ assign({
1113
+ writeStatus: 'success',
1114
+ pendingWrite: null,
1115
+ entityData: ({ context, event }) => {
1116
+ // Update entityData with any returned data from write
1117
+ return event.output || context.entityData;
1118
+ },
1119
+ }),
1120
+ ({ context }) => {
1121
+ logger$2(`[writing] Write successful for ${context.entityType} "${context.entityId}"`);
1122
+ },
1123
+ ],
1124
+ },
1125
+ writeError: {
1126
+ target: 'error',
1127
+ actions: [
1128
+ assign({
1129
+ error: ({ event }) => event.error instanceof Error ? event.error : new Error(String(event.error)),
1130
+ writeStatus: 'error',
1131
+ retryCount: ({ context }) => context.retryCount + 1,
1132
+ }),
1133
+ ({ context, event }) => {
1134
+ logger$2(`[writing] Write error for ${context.entityType} "${context.entityId}":`, event.error);
1135
+ },
1136
+ ],
1137
+ },
1138
+ reset: 'idle',
1139
+ },
1140
+ },
1141
+ success: {
1142
+ after: {
1143
+ 2000: { target: 'idle' }, // Auto-reset after 2s
1144
+ },
1145
+ on: {
1146
+ reset: 'idle',
1147
+ },
1148
+ entry: assign({
1149
+ writeStatus: 'success',
1150
+ }),
1151
+ },
1152
+ error: {
1153
+ on: {
1154
+ retry: {
1155
+ target: 'validating',
1156
+ guard: ({ context }) => context.retryCount < 3,
1157
+ actions: assign({
1158
+ error: null,
1159
+ }),
1160
+ },
1161
+ revert: {
1162
+ target: 'idle',
1163
+ actions: assign({
1164
+ pendingWrite: null,
1165
+ writeStatus: 'idle',
1166
+ error: null,
1167
+ }),
1168
+ },
1169
+ reset: 'idle',
1170
+ },
1171
+ },
1172
+ },
1173
+ });
1174
+
1175
+ const logger$1 = debug('seedSdk:model:modelMachine');
1176
+ const modelMachine = setup({
1177
+ types: {
1178
+ context: {},
1179
+ input: {},
1180
+ events: {},
1181
+ },
1182
+ actors: {
1183
+ loadOrCreateModel,
1184
+ validateModel,
1185
+ createModelProperties,
1186
+ writeProcessMachine,
1187
+ },
1188
+ guards: {
1189
+ isModelValid: ({ context }) => {
1190
+ return !context._validationErrors || context._validationErrors.length === 0;
1191
+ },
1192
+ hasValidationErrors: ({ context }) => {
1193
+ return !!context._validationErrors && context._validationErrors.length > 0;
1194
+ },
1195
+ },
1196
+ actions: {
1197
+ assignValidationErrors: assign(({ context, event }) => {
1198
+ if (event.type === 'validationError') {
1199
+ return {
1200
+ ...context,
1201
+ _validationErrors: event.errors,
1202
+ };
1203
+ }
1204
+ return context;
1205
+ }),
1206
+ },
1207
+ }).createMachine({
1208
+ id: 'model',
1209
+ initial: 'loading',
1210
+ context: ({ input }) => ({
1211
+ id: input.id,
1212
+ _dbId: undefined,
1213
+ modelName: input.modelName,
1214
+ schemaName: input.schemaName,
1215
+ _pendingPropertyDefinitions: input._pendingPropertyDefinitions,
1216
+ _idFromSchema: input._idFromSchema,
1217
+ _isDraft: false,
1218
+ _editedProperties: new Set(),
1219
+ _validationErrors: undefined,
1220
+ writeProcess: undefined,
1221
+ _liveQueryPropertyIds: [], // Initialize empty array
1222
+ }),
1223
+ on: {
1224
+ updateContext: [
1225
+ {
1226
+ // Always update context first (no guard, always runs)
1227
+ actions: [
1228
+ assign(({ context, event }) => {
1229
+ const newContext = { ...context };
1230
+ // Check if this is only updating internal fields
1231
+ const onlyInternalFields = Object.keys(event).every((key) => {
1232
+ return key === 'type' || key.startsWith('_');
1233
+ });
1234
+ // Update the context with new values
1235
+ for (let i = 0; i < Object.keys(event).length; i++) {
1236
+ const key = Object.keys(event)[i];
1237
+ if (key === 'type') {
1238
+ continue;
1239
+ }
1240
+ const eventValue = event[key];
1241
+ // Ensure arrays are properly copied (create new reference)
1242
+ newContext[key] = Array.isArray(eventValue) ? [...eventValue] : eventValue;
1243
+ }
1244
+ // Compare with original values and set _isEdited flag (only for non-internal updates)
1245
+ // Note: properties are not in context - they're computed from ModelProperty instances
1246
+ // Property changes are tracked via ModelProperty._isEdited flags
1247
+ if (!onlyInternalFields && context._originalValues) {
1248
+ const hasChanges = Object.keys(event).some((key) => {
1249
+ if (key === 'type' || key.startsWith('_') || key === 'properties')
1250
+ return false;
1251
+ const newValue = newContext[key];
1252
+ const oldValue = context._originalValues?.[key];
1253
+ // Deep comparison for objects/arrays
1254
+ if (typeof newValue === 'object' && typeof oldValue === 'object') {
1255
+ return JSON.stringify(newValue) !== JSON.stringify(oldValue);
1256
+ }
1257
+ return newValue !== oldValue;
1258
+ });
1259
+ newContext._isEdited = hasChanges;
1260
+ }
1261
+ // Clear validation errors on context update (will be re-validated if needed)
1262
+ newContext._validationErrors = undefined;
1263
+ return newContext;
1264
+ }),
1265
+ // Verify the context was updated and create property instances if needed
1266
+ ({ context, event, self }) => {
1267
+ if (event._liveQueryPropertyIds !== undefined) {
1268
+ const newPropertyIds = event._liveQueryPropertyIds;
1269
+ // Use setTimeout to check after assign has been applied
1270
+ setTimeout(() => {
1271
+ self.getSnapshot();
1272
+ // Create ModelProperty instances for any new property IDs
1273
+ if (Array.isArray(newPropertyIds) && newPropertyIds.length > 0) {
1274
+ // Import and create instances asynchronously (fire-and-forget)
1275
+ import('./ModelProperty-DuB5nk6A.js').then(({ ModelProperty }) => {
1276
+ const createPromises = newPropertyIds.map(async (propertyFileId) => {
1277
+ try {
1278
+ const property = await ModelProperty.createById(propertyFileId);
1279
+ if (property) {
1280
+ logger$1(`[modelMachine] Created/cached ModelProperty instance for propertyFileId "${propertyFileId}" after _liveQueryPropertyIds update`);
1281
+ }
1282
+ }
1283
+ catch (error) {
1284
+ logger$1(`[modelMachine] Error creating ModelProperty instance for propertyFileId "${propertyFileId}": ${error}`);
1285
+ }
1286
+ });
1287
+ Promise.all(createPromises).catch((error) => {
1288
+ logger$1(`[modelMachine] Error creating property instances: ${error}`);
1289
+ });
1290
+ }).catch((error) => {
1291
+ logger$1(`[modelMachine] Error importing ModelProperty: ${error}`);
1292
+ });
1293
+ }
1294
+ }, 0);
1295
+ }
1296
+ },
1297
+ ],
1298
+ // Don't transition - stay in current state, context will be updated
1299
+ },
1300
+ {
1301
+ // Conditionally trigger validation for non-internal updates
1302
+ guard: ({ event }) => {
1303
+ return !Object.keys(event).every((key) => {
1304
+ return key === 'type' || key.startsWith('_');
1305
+ });
1306
+ },
1307
+ target: '.validating',
1308
+ },
1309
+ ],
1310
+ validateModel: {
1311
+ target: '.validating',
1312
+ },
1313
+ validationSuccess: {
1314
+ actions: assign(({ context, event }) => ({
1315
+ ...context,
1316
+ _validationErrors: [],
1317
+ })),
1318
+ },
1319
+ validationError: {
1320
+ actions: 'assignValidationErrors',
1321
+ },
1322
+ initializeOriginalValues: {
1323
+ actions: assign(({ context, event }) => ({
1324
+ ...context,
1325
+ _originalValues: event.originalValues,
1326
+ _isEdited: event.isEdited ?? false,
1327
+ _validationErrors: undefined,
1328
+ })),
1329
+ },
1330
+ markAsDraft: {
1331
+ actions: assign(({ context, event }) => ({
1332
+ ...context,
1333
+ _isEdited: true,
1334
+ _editedProperties: new Set([
1335
+ ...(context._editedProperties || []),
1336
+ event.propertyKey,
1337
+ ]),
1338
+ })),
1339
+ },
1340
+ clearDraft: {
1341
+ actions: assign(({ context }) => ({
1342
+ ...context,
1343
+ _isEdited: false,
1344
+ _editedProperties: new Set(),
1345
+ _originalValues: {
1346
+ // Properties are not stored in context - they're computed from ModelProperty instances
1347
+ // Properties serialization happens separately when initializing original values
1348
+ },
1349
+ })),
1350
+ },
1351
+ destroyStarted: {
1352
+ actions: assign({ _destroyInProgress: true, _destroyError: null }),
1353
+ },
1354
+ destroyDone: {
1355
+ actions: assign({ _destroyInProgress: false }),
1356
+ },
1357
+ destroyError: {
1358
+ actions: assign(({ event }) => ({
1359
+ _destroyInProgress: false,
1360
+ _destroyError: event.error instanceof Error
1361
+ ? { message: event.error.message, name: event.error.name }
1362
+ : { message: String(event.error) },
1363
+ })),
1364
+ },
1365
+ clearDestroyError: {
1366
+ actions: assign({ _destroyError: null }),
1367
+ },
1368
+ },
1369
+ states: {
1370
+ loading: {
1371
+ on: {
1372
+ loadOrCreateModelSuccess: {
1373
+ target: 'idle',
1374
+ actions: assign(({ context, event }) => {
1375
+ const hasPendingProps = !!(context._pendingPropertyDefinitions && Object.keys(context._pendingPropertyDefinitions).length > 0);
1376
+ logger$1(`[loadOrCreateModelSuccess] Preserving _pendingPropertyDefinitions: ${hasPendingProps} (${Object.keys(context._pendingPropertyDefinitions || {}).length} properties)`);
1377
+ // Check if there's a pending model name update (set before this event to avoid validation trigger)
1378
+ const finalModelName = context._pendingModelName || context.modelName;
1379
+ if (context._pendingModelName) {
1380
+ logger$1(`[loadOrCreateModelSuccess] Applying pending model name: "${context.modelName}" -> "${finalModelName}"`);
1381
+ }
1382
+ return {
1383
+ ...context,
1384
+ id: event.model.id, // schemaFileId (string)
1385
+ _dbId: event.model._dbId, // Database integer ID
1386
+ _isEdited: event.model._isEdited ?? false, // Load isEdited from database
1387
+ _editedProperties: new Set(),
1388
+ _validationErrors: undefined,
1389
+ // Use final model name (may have been set via _pendingModelName to avoid validation trigger)
1390
+ modelName: finalModelName,
1391
+ // Clear the temporary field
1392
+ _pendingModelName: undefined,
1393
+ // Preserve _pendingPropertyDefinitions if it exists
1394
+ _pendingPropertyDefinitions: context._pendingPropertyDefinitions,
1395
+ // Preserve _liveQueryPropertyIds if provided (set by loadOrCreateModel when loading from DB)
1396
+ _liveQueryPropertyIds: event.model._liveQueryPropertyIds ?? context._liveQueryPropertyIds ?? [],
1397
+ // Preserve conflict detection metadata if provided
1398
+ _loadedAt: event.model._loadedAt,
1399
+ _dbVersion: event.model._dbVersion,
1400
+ _dbUpdatedAt: event.model._dbUpdatedAt,
1401
+ _originalValues: {
1402
+ // Properties are not stored in context - they're computed from ModelProperty instances
1403
+ // Properties will be loaded via liveQuery after model creation
1404
+ },
1405
+ };
1406
+ }),
1407
+ },
1408
+ loadOrCreateModelError: {
1409
+ target: 'error',
1410
+ },
1411
+ },
1412
+ invoke: {
1413
+ src: 'loadOrCreateModel',
1414
+ input: ({ context }) => ({ context }),
1415
+ },
1416
+ },
1417
+ idle: {
1418
+ entry: assign({
1419
+ writeProcess: ({ context, spawn }) => {
1420
+ if (!context.writeProcess && context.id) {
1421
+ logger$1(`[idle entry] Spawning writeProcess for model "${context.modelName}" (${context.id})`);
1422
+ return spawn(writeProcessMachine, {
1423
+ input: {
1424
+ entityType: 'model',
1425
+ entityId: context.id,
1426
+ entityData: {
1427
+ modelName: context.modelName,
1428
+ schemaName: context.schemaName,
1429
+ // Properties are not stored in context - they'll be provided via requestWrite event
1430
+ },
1431
+ },
1432
+ });
1433
+ }
1434
+ return context.writeProcess;
1435
+ },
1436
+ }),
1437
+ on: {
1438
+ refreshProperties: {
1439
+ actions: ({ self }) => {
1440
+ // This event will be handled by the Model instance's subscription
1441
+ // The Model will call _refreshPropertiesFromDb when it receives this event
1442
+ logger$1(`[idle] refreshProperties event received, will be handled by Model instance`);
1443
+ },
1444
+ },
1445
+ validateModel: {
1446
+ target: 'validating',
1447
+ },
1448
+ reloadFromDb: {
1449
+ target: 'loading',
1450
+ },
1451
+ requestWrite: {
1452
+ actions: ({ context, event }) => {
1453
+ if (context.writeProcess) {
1454
+ logger$1(`[requestWrite] Sending startWrite to writeProcess with data:`, event.data);
1455
+ context.writeProcess.send({
1456
+ type: 'startWrite',
1457
+ data: event.data,
1458
+ });
1459
+ }
1460
+ else {
1461
+ logger$1(`[requestWrite] ERROR: writeProcess not available for model "${context.modelName}" (${context.id})`);
1462
+ }
1463
+ },
1464
+ },
1465
+ writeSuccess: [
1466
+ {
1467
+ guard: ({ context }) => {
1468
+ // If we have pending property definitions, transition to creatingProperties
1469
+ const hasPending = !!(context._pendingPropertyDefinitions && Object.keys(context._pendingPropertyDefinitions).length > 0);
1470
+ logger$1(`[writeSuccess guard] hasPending: ${hasPending}, _pendingPropertyDefinitions keys: ${context._pendingPropertyDefinitions ? Object.keys(context._pendingPropertyDefinitions).length : 0}`);
1471
+ return hasPending;
1472
+ },
1473
+ target: 'creatingProperties',
1474
+ actions: assign(({ context, event }) => {
1475
+ logger$1(`[writeSuccess] Transitioning to creatingProperties for model "${context.modelName}"`);
1476
+ // Update _dbId from write output if available (writeToDatabase returns _dbId, not id)
1477
+ const newContext = { ...context };
1478
+ const dbId = event.output?._dbId ?? event.output?.id;
1479
+ if (dbId != null) {
1480
+ newContext._dbId = dbId;
1481
+ logger$1(`[writeSuccess] Updated _dbId to ${dbId}`);
1482
+ }
1483
+ return newContext;
1484
+ }),
1485
+ },
1486
+ {
1487
+ target: 'idle',
1488
+ actions: assign(({ context, event }) => {
1489
+ // Update _dbId from write output if available (writeToDatabase returns _dbId, not id)
1490
+ const newContext = { ...context };
1491
+ const dbId = event.output?._dbId ?? event.output?.id;
1492
+ if (dbId != null) {
1493
+ newContext._dbId = dbId;
1494
+ }
1495
+ return newContext;
1496
+ }),
1497
+ },
1498
+ ],
1499
+ },
1500
+ always: {
1501
+ guard: 'hasValidationErrors',
1502
+ target: 'validating',
1503
+ },
1504
+ },
1505
+ validating: {
1506
+ on: {
1507
+ validationSuccess: {
1508
+ target: 'idle',
1509
+ actions: assign(({ context, event }) => ({
1510
+ ...context,
1511
+ _validationErrors: [],
1512
+ })),
1513
+ },
1514
+ validationError: {
1515
+ target: 'idle',
1516
+ actions: 'assignValidationErrors',
1517
+ },
1518
+ },
1519
+ invoke: {
1520
+ src: 'validateModel',
1521
+ input: ({ context }) => ({ context }),
1522
+ },
1523
+ },
1524
+ creatingProperties: {
1525
+ entry: ({ context }) => {
1526
+ logger$1(`[creatingProperties entry] Starting property creation for model "${context.modelName}" with ${Object.keys(context._pendingPropertyDefinitions || {}).length} properties`);
1527
+ },
1528
+ invoke: {
1529
+ src: 'createModelProperties',
1530
+ input: ({ context }) => {
1531
+ logger$1(`[creatingProperties invoke] Invoking createModelProperties with ${Object.keys(context._pendingPropertyDefinitions || {}).length} property definitions`);
1532
+ return {
1533
+ context,
1534
+ propertyDefinitions: context._pendingPropertyDefinitions || {},
1535
+ };
1536
+ },
1537
+ },
1538
+ on: {
1539
+ createModelPropertiesSuccess: {
1540
+ target: 'idle',
1541
+ actions: [
1542
+ assign(({ context }) => {
1543
+ // Only clear _pendingPropertyDefinitions, preserve everything else including _liveQueryPropertyIds
1544
+ return {
1545
+ ...context,
1546
+ _pendingPropertyDefinitions: undefined,
1547
+ };
1548
+ }),
1549
+ ({ context, self }) => {
1550
+ // Trigger property refresh after properties are created
1551
+ // This ensures _liveQueryPropertyIds is updated in Node.js where liveQuery isn't available
1552
+ setTimeout(() => {
1553
+ self.send({ type: 'refreshProperties' });
1554
+ }, 100); // Small delay to ensure properties are written to DB
1555
+ },
1556
+ ],
1557
+ },
1558
+ createModelPropertiesError: {
1559
+ target: 'error',
1560
+ actions: ({ event }) => {
1561
+ logger$1('Error creating model properties:', event.error);
1562
+ },
1563
+ },
1564
+ },
1565
+ },
1566
+ error: {},
1567
+ },
1568
+ });
1569
+
1570
+ /**
1571
+ * Error thrown when a conflict is detected between actor context and database
1572
+ */
1573
+ class ConflictError extends Error {
1574
+ constructor(message, conflict) {
1575
+ super(message);
1576
+ this.name = 'ConflictError';
1577
+ this.conflict = conflict;
1578
+ Object.setPrototypeOf(this, ConflictError.prototype);
1579
+ }
1580
+ }
1581
+
1582
+ /**
1583
+ * Set up liveQuery subscription for an entity
1584
+ * This enables cross-instance synchronization (e.g., changes in other tabs/windows)
1585
+ *
1586
+ * @param instance - Entity instance
1587
+ * @param config - LiveQuery configuration
1588
+ */
1589
+ function setupEntityLiveQuery(instance, config) {
1590
+ const isBrowser = typeof window !== 'undefined';
1591
+ const logger = debug(config.loggerName);
1592
+ // Use a closure variable to track setup state per instance
1593
+ const setupState = { subscriptionSetUp: false };
1594
+ const setupLiveQuery = async (entityId) => {
1595
+ if (setupState.subscriptionSetUp) {
1596
+ return;
1597
+ }
1598
+ setupState.subscriptionSetUp = true;
1599
+ logger(`Setting up liveQuery for entity ID: ${entityId}`);
1600
+ try {
1601
+ // Query initial data if provided (for Node.js compatibility)
1602
+ if (config.queryInitialData) {
1603
+ const initialRows = await config.queryInitialData(entityId);
1604
+ const initialIds = config.extractEntityIds(initialRows);
1605
+ if (initialIds.length > 0) {
1606
+ logger(`Initial query returned ${initialIds.length} entities`);
1607
+ // Create child instances if provided
1608
+ if (config.createChildInstances) {
1609
+ await config.createChildInstances(initialIds);
1610
+ }
1611
+ // Update context with initial IDs
1612
+ config.updateContext(instance, initialIds);
1613
+ }
1614
+ }
1615
+ // Only set up liveQuery subscription in browser environment
1616
+ if (isBrowser) {
1617
+ // buildQuery can be async to allow dynamic imports
1618
+ const queryResult = config.buildQuery(entityId);
1619
+ const query$ = queryResult instanceof Promise ? await queryResult : queryResult;
1620
+ const instanceState = config.instanceState.get(instance);
1621
+ if (!instanceState) {
1622
+ logger('Instance state not found');
1623
+ return;
1624
+ }
1625
+ // Subscribe to liveQuery updates
1626
+ const subscription = query$.subscribe({
1627
+ next: async (rows) => {
1628
+ // Check if instance state still exists (hasn't been cleaned up)
1629
+ const currentInstanceState = config.instanceState.get(instance);
1630
+ if (!currentInstanceState) {
1631
+ logger('Instance state was cleaned up, skipping update');
1632
+ return;
1633
+ }
1634
+ logger(`Query returned ${rows.length} rows`);
1635
+ const ids = config.extractEntityIds(rows);
1636
+ // Create child instances if provided (before updating context)
1637
+ if (config.createChildInstances && ids.length > 0) {
1638
+ await config.createChildInstances(ids);
1639
+ }
1640
+ // Update context with new IDs
1641
+ config.updateContext(instance, ids);
1642
+ },
1643
+ error: (error) => {
1644
+ logger(`LiveQuery error: ${error}`);
1645
+ },
1646
+ });
1647
+ instanceState.liveQuerySubscription = subscription;
1648
+ logger(`LiveQuery subscription set up for entity ID: ${entityId}`);
1649
+ }
1650
+ else {
1651
+ logger('Skipping liveQuery subscription in Node.js environment');
1652
+ }
1653
+ }
1654
+ catch (error) {
1655
+ logger(`Error setting up subscription: ${error}`);
1656
+ setupState.subscriptionSetUp = false; // Reset on error so we can retry
1657
+ }
1658
+ };
1659
+ // Set up liveQuery subscription as soon as we have entity ID
1660
+ const setupSubscription = instance.getService().subscribe(async (snapshot) => {
1661
+ // Check if entity is ready
1662
+ if (config.isReady && !config.isReady(instance)) {
1663
+ if (config.waitForReady) {
1664
+ await config.waitForReady(instance);
1665
+ }
1666
+ else {
1667
+ return; // Not ready yet, will retry on next snapshot
1668
+ }
1669
+ }
1670
+ // Get entity ID
1671
+ const entityId = await config.getEntityId(instance);
1672
+ if (!entityId) {
1673
+ return; // Need entity ID to proceed
1674
+ }
1675
+ // Once we have entity ID, set up the liveQuery subscription (only once)
1676
+ if (!setupState.subscriptionSetUp) {
1677
+ await setupLiveQuery(entityId);
1678
+ if (setupState.subscriptionSetUp) {
1679
+ setupSubscription.unsubscribe();
1680
+ }
1681
+ }
1682
+ });
1683
+ // Also check current state immediately in case entity ID is already available
1684
+ instance.getService().getSnapshot();
1685
+ if (config.isReady && !config.isReady(instance)) {
1686
+ // Not ready yet, will be handled by subscription
1687
+ return;
1688
+ }
1689
+ config.getEntityId(instance).then((entityId) => {
1690
+ if (entityId && !setupState.subscriptionSetUp) {
1691
+ setupLiveQuery(entityId).catch((error) => {
1692
+ logger(`Error in immediate setup: ${error}`);
1693
+ });
1694
+ }
1695
+ }).catch((error) => {
1696
+ logger(`Error getting entity ID: ${error}`);
1697
+ });
1698
+ }
1699
+
1700
+ /**
1701
+ * Returns model rows (id, name, schemaFileId, isEdited) for building Model instances.
1702
+ * When schemaIdentifier is provided, only returns models for that schema (join models -> model_schemas -> schemas).
1703
+ * schemaIdentifier can be either schema name or schema file ID.
1704
+ */
1705
+ const getModelsData = async (schemaIdentifier) => {
1706
+ const appDb = BaseDb.getAppDb();
1707
+ if (!appDb) {
1708
+ return [];
1709
+ }
1710
+ if (schemaIdentifier !== undefined && schemaIdentifier !== '') {
1711
+ const runQuery = () => appDb
1712
+ .select({
1713
+ id: models.id,
1714
+ name: models.name,
1715
+ schemaFileId: models.schemaFileId,
1716
+ isEdited: models.isEdited,
1717
+ })
1718
+ .from(models)
1719
+ .innerJoin(modelSchemas, eq(models.id, modelSchemas.modelId))
1720
+ .innerJoin(schemas, eq(modelSchemas.schemaId, schemas.id))
1721
+ .where(or(eq(schemas.name, schemaIdentifier), eq(schemas.schemaFileId, schemaIdentifier)));
1722
+ let rows = await runQuery();
1723
+ // Retry when we get 0 rows (read-after-write consistency; SQLocal/async may not see the write immediately).
1724
+ for (let i = 0; i < 4 && rows.length === 0; i++) {
1725
+ await new Promise((r) => setTimeout(r, 150));
1726
+ rows = await runQuery();
1727
+ }
1728
+ return rows;
1729
+ }
1730
+ const rows = await appDb
1731
+ .select({
1732
+ id: models.id,
1733
+ name: models.name,
1734
+ schemaFileId: models.schemaFileId,
1735
+ isEdited: models.isEdited,
1736
+ })
1737
+ .from(models);
1738
+ return rows;
1739
+ };
1740
+
1741
+ const logger = debug('seedSdk:model:Model');
1742
+ const saveDraftLogger = debug('seedSdk:model:saveDraftToDb');
1743
+ // Lazy import cache for ModelProperty to avoid circular dependency
1744
+ // Eagerly start loading to minimize delay on first access
1745
+ let ModelPropertyClass = null;
1746
+ const modelPropertyImportPromise = import('./ModelProperty-DuB5nk6A.js')
1747
+ .then(module => {
1748
+ ModelPropertyClass = module.ModelProperty;
1749
+ return ModelPropertyClass;
1750
+ })
1751
+ .catch(() => {
1752
+ // If import fails, ModelPropertyClass remains null
1753
+ return null;
1754
+ });
1755
+ function getModelProperty() {
1756
+ // Return cached class if available (synchronous access)
1757
+ // Note: On first access before import completes, this will return null
1758
+ // and the getter will return empty array, which matches the original behavior
1759
+ return ModelPropertyClass;
1760
+ }
1761
+ // Lazy import cache for Schema to avoid circular dependency
1762
+ let SchemaClass = null;
1763
+ const schemaImportPromise = (async () => { const _mod_7 = await import('./Schema-BYM-GNdE.js'); return _mod_7.b; })()
1764
+ .then(module => {
1765
+ SchemaClass = module.Schema;
1766
+ return SchemaClass;
1767
+ })
1768
+ .catch(() => {
1769
+ // If import fails, SchemaClass remains null
1770
+ return null;
1771
+ });
1772
+ // WeakMap to store mutable state per Model instance
1773
+ // This avoids issues with read-only properties when instances are frozen by Immer
1774
+ const modelInstanceState = new WeakMap();
1775
+ // Define tracked properties for the Proxy
1776
+ // These properties will be read from/written to the actor context
1777
+ const TRACKED_PROPERTIES = [
1778
+ 'id', // schemaFileId (string) - public ID
1779
+ 'modelName',
1780
+ 'schemaName',
1781
+ ];
1782
+ class Model {
1783
+ static trackPendingWrite(modelFileId, schemaId) {
1784
+ this.pendingWrites.set(modelFileId, {
1785
+ modelFileId,
1786
+ schemaId,
1787
+ status: 'pending',
1788
+ timestamp: Date.now(),
1789
+ });
1790
+ }
1791
+ static getPendingModelIds(schemaId) {
1792
+ return Array.from(this.pendingWrites.entries())
1793
+ .filter(([_, write]) => write.schemaId === schemaId && write.status !== 'error')
1794
+ .map(([modelFileId]) => modelFileId);
1795
+ }
1796
+ // 'name' is available as an alias for 'modelName' via the proxy
1797
+ constructor(modelName, schemaName, id, // schemaFileId (string) - public ID
1798
+ initialContext, idFromSchema // true when id was provided via options (schema model), not generated
1799
+ ) {
1800
+ const serviceInput = {
1801
+ modelName,
1802
+ schemaName,
1803
+ ...(id ? { id } : {}),
1804
+ ...(initialContext?._pendingPropertyDefinitions ? { _pendingPropertyDefinitions: initialContext._pendingPropertyDefinitions } : {}),
1805
+ _idFromSchema: !!idFromSchema,
1806
+ };
1807
+ this._service = createActor(modelMachine, {
1808
+ input: serviceInput,
1809
+ });
1810
+ this._service.start();
1811
+ // Initialize instance state in WeakMap
1812
+ modelInstanceState.set(this, {
1813
+ liveQuerySubscription: null,
1814
+ });
1815
+ // Set up liveQuery subscription for cross-instance property updates
1816
+ this._setupLiveQuerySubscription();
1817
+ // Subscribe to refresh properties after they're created (for Node.js where liveQuery isn't available)
1818
+ let previousState;
1819
+ this._service.subscribe((snapshot) => {
1820
+ // Check if we just transitioned to idle from creatingProperties state
1821
+ if (snapshot.value === 'idle' && previousState === 'creatingProperties') {
1822
+ // Small delay to ensure properties are written to DB
1823
+ setTimeout(() => {
1824
+ this._refreshPropertiesFromDb().catch((error) => {
1825
+ logger(`[Model constructor] Error refreshing properties: ${error}`);
1826
+ });
1827
+ }, 200);
1828
+ }
1829
+ previousState = snapshot.value;
1830
+ });
1831
+ // Note: Property getters/setters are now handled by the Proxy in create()
1832
+ }
1833
+ /**
1834
+ * Find a unique model name by checking for duplicates (case-insensitive) in the cache
1835
+ * If duplicates are found, appends an incrementing number to make it unique
1836
+ *
1837
+ * @param modelName - The desired model name
1838
+ * @param schemaName - The schema name
1839
+ * @param skipAllChecks - If true, skip all duplicate checks and return original name (used when creating schema models to preserve original names)
1840
+ * @returns A unique model name
1841
+ */
1842
+ static findUniqueModelName(modelName, schemaName, skipAllChecks = false) {
1843
+ // If skipAllChecks is true (schema models), preserve original name
1844
+ if (skipAllChecks) {
1845
+ return modelName;
1846
+ }
1847
+ const lowerModelName = modelName.toLowerCase();
1848
+ // Check all cached models for this schema
1849
+ const existingNames = new Set();
1850
+ const existingNumbers = new Set();
1851
+ // Check name-based cache
1852
+ for (const [nameKey, modelFileId] of this.instanceCacheByName.entries()) {
1853
+ const [cachedSchemaName, cachedModelName] = nameKey.split(':', 2);
1854
+ if (cachedSchemaName === schemaName && cachedModelName) {
1855
+ const lowerCachedName = cachedModelName.toLowerCase();
1856
+ existingNames.add(lowerCachedName);
1857
+ // If it matches the base name (case-insensitive), check if it has a number suffix
1858
+ if (lowerCachedName === lowerModelName) {
1859
+ existingNumbers.add(0); // Base name exists
1860
+ }
1861
+ else if (lowerCachedName.startsWith(lowerModelName + ' ')) {
1862
+ // Check if it's the base name followed by a space and a number
1863
+ const suffix = lowerCachedName.slice(lowerModelName.length + 1);
1864
+ const number = parseInt(suffix, 10);
1865
+ if (!isNaN(number) && suffix === number.toString()) {
1866
+ existingNumbers.add(number);
1867
+ }
1868
+ }
1869
+ }
1870
+ }
1871
+ // Check legacy cache
1872
+ for (const [nameKey] of this.instanceCache.keys()) {
1873
+ const [cachedSchemaName, cachedModelName] = nameKey.split(':', 2);
1874
+ if (cachedSchemaName === schemaName && cachedModelName) {
1875
+ const lowerCachedName = cachedModelName.toLowerCase();
1876
+ existingNames.add(lowerCachedName);
1877
+ // If it matches the base name (case-insensitive), check if it has a number suffix
1878
+ if (lowerCachedName === lowerModelName) {
1879
+ existingNumbers.add(0); // Base name exists
1880
+ }
1881
+ else if (lowerCachedName.startsWith(lowerModelName + ' ')) {
1882
+ // Check if it's the base name followed by a space and a number
1883
+ const suffix = lowerCachedName.slice(lowerModelName.length + 1);
1884
+ const number = parseInt(suffix, 10);
1885
+ if (!isNaN(number) && suffix === number.toString()) {
1886
+ existingNumbers.add(number);
1887
+ }
1888
+ }
1889
+ }
1890
+ }
1891
+ // Also check schema context models (case-insensitive)
1892
+ // This ensures runtime-created models are renamed if they conflict with schema-defined models
1893
+ try {
1894
+ // Use lazy-loaded Schema class to avoid circular dependency
1895
+ if (SchemaClass) {
1896
+ const schema = SchemaClass.create(schemaName);
1897
+ const schemaContext = schema.getService().getSnapshot().context;
1898
+ if (schemaContext.models) {
1899
+ for (const schemaModelName of Object.keys(schemaContext.models)) {
1900
+ const lowerSchemaModelName = schemaModelName.toLowerCase();
1901
+ // If it matches the base name (case-insensitive), check if it has a number suffix
1902
+ if (lowerSchemaModelName === lowerModelName) {
1903
+ existingNumbers.add(0); // Base name exists in schema
1904
+ }
1905
+ else if (lowerSchemaModelName.startsWith(lowerModelName + ' ')) {
1906
+ // Check if it's the base name followed by a space and a number
1907
+ const suffix = lowerSchemaModelName.slice(lowerModelName.length + 1);
1908
+ const number = parseInt(suffix, 10);
1909
+ if (!isNaN(number) && suffix === number.toString()) {
1910
+ existingNumbers.add(number);
1911
+ }
1912
+ }
1913
+ }
1914
+ }
1915
+ }
1916
+ }
1917
+ catch (error) {
1918
+ // If schema check fails, continue with cache-only check
1919
+ // This is a best-effort check and shouldn't block model creation
1920
+ }
1921
+ // If no duplicates found (no base name match and no numbered variants), return original name
1922
+ if (existingNumbers.size === 0) {
1923
+ return modelName;
1924
+ }
1925
+ // Find the next available number
1926
+ // Start from 1 if base name exists, otherwise check if we can use 1
1927
+ let nextNumber = existingNumbers.has(0) ? 1 : 1;
1928
+ while (existingNumbers.has(nextNumber)) {
1929
+ nextNumber++;
1930
+ }
1931
+ // Return the name with the number appended
1932
+ return `${modelName} ${nextNumber}`;
1933
+ }
1934
+ static create(modelName, schemaNameOrSchema, // Schema type - using any to avoid circular dependency
1935
+ options) {
1936
+ if (!modelName) {
1937
+ throw new Error('Model name is required');
1938
+ }
1939
+ const waitForReady = options?.waitForReady !== false;
1940
+ const readyTimeout = options?.readyTimeout ?? 5000;
1941
+ // Extract schema name and instance
1942
+ let schemaName;
1943
+ let schemaInstance; // Using any to avoid circular dependency with Schema
1944
+ if (typeof schemaNameOrSchema === 'string') {
1945
+ schemaName = schemaNameOrSchema;
1946
+ }
1947
+ else {
1948
+ schemaInstance = schemaNameOrSchema;
1949
+ schemaName = schemaInstance.schemaName;
1950
+ }
1951
+ if (!schemaName) {
1952
+ throw new Error('Schema name is required');
1953
+ }
1954
+ // Support both modelFileId (documented) and id (backward compatibility)
1955
+ const id = options?.modelFileId || options?.id; // schemaFileId (string) - public ID
1956
+ const registerWithSchema = options?.registerWithSchema !== false && schemaInstance !== undefined;
1957
+ // Step 1: Check ID-based cache first (if ID provided)
1958
+ if (id && this.instanceCacheById.has(id)) {
1959
+ const { instance, refCount } = this.instanceCacheById.get(id);
1960
+ this.instanceCacheById.set(id, {
1961
+ instance,
1962
+ refCount: refCount + 1,
1963
+ });
1964
+ logger(`Model.create: Found instance in ID cache for "${modelName}" (ID: ${id})`);
1965
+ if (!waitForReady)
1966
+ return instance;
1967
+ return waitForEntityIdle(instance, { timeout: readyTimeout }).then(() => instance);
1968
+ }
1969
+ // Step 2: Check for duplicate names and generate unique name if needed (before checking cache)
1970
+ // This allows us to create new models with renamed versions when duplicates exist
1971
+ // Skip all duplicate checks if modelFileId is provided (schema models should preserve original names)
1972
+ const skipAllChecks = !!id; // If id (modelFileId) is provided, this is a schema model - skip schema check to preserve name
1973
+ const uniqueModelName = this.findUniqueModelName(modelName, schemaName, skipAllChecks);
1974
+ // Step 3: Check name-based index with ORIGINAL name first (before using unique name)
1975
+ // Only return existing instance if the unique name matches the original (no rename needed)
1976
+ // If a rename is needed, we'll create a new model with the unique name
1977
+ let resolvedId = id;
1978
+ const originalNameKey = `${schemaName}:${modelName}`;
1979
+ // Step 3: When we have no id (runtime create), do not return from name cache. Each create('My Model')
1980
+ // must get a new instance with an incremented name (My Model, My Model 1, ...). If the only cached
1981
+ // instance with this name is the schema model (_idFromSchema), ensure we fall through to create new.
1982
+ if (!resolvedId) {
1983
+ const cachedId = this.instanceCacheByName.get(originalNameKey);
1984
+ if (cachedId && this.instanceCacheById.has(cachedId)) {
1985
+ const { instance, refCount } = this.instanceCacheById.get(cachedId);
1986
+ const ctx = instance._getSnapshotContext();
1987
+ if (ctx._idFromSchema) {
1988
+ resolvedId = undefined;
1989
+ }
1990
+ // Never return by name when !id (runtime create); fall through to create new with unique name
1991
+ }
1992
+ }
1993
+ // Step 4: Check legacy cache with original name. Skip when !id (runtime create) so duplicate
1994
+ // create('My Model') calls get new instances with incremented names.
1995
+ if (id && uniqueModelName === modelName) {
1996
+ const legacyKey = `${schemaName}:${modelName}`;
1997
+ if (this.instanceCache.has(legacyKey)) {
1998
+ const { instance, refCount } = this.instanceCache.get(legacyKey);
1999
+ const ctx = instance._getSnapshotContext();
2000
+ if (!ctx._idFromSchema) {
2001
+ this.instanceCache.set(legacyKey, {
2002
+ instance,
2003
+ refCount: refCount + 1,
2004
+ });
2005
+ logger(`Model.create: Found instance in legacy cache for "${modelName}"`);
2006
+ const existingId = instance._getSnapshotContext().id;
2007
+ if (existingId) {
2008
+ this.instanceCacheById.set(existingId, { instance, refCount: refCount + 1 });
2009
+ this.instanceCacheByName.set(legacyKey, existingId);
2010
+ }
2011
+ if (!waitForReady)
2012
+ return instance;
2013
+ return waitForEntityIdle(instance, { timeout: readyTimeout }).then(() => instance);
2014
+ }
2015
+ }
2016
+ }
2017
+ // Step 5: When a rename is needed (uniqueModelName !== modelName), do NOT return a cached
2018
+ // instance. The only cached instance with the original name at this point is the
2019
+ // schema-defined model (created with modelFileId); returning it would give the user the
2020
+ // schema model instead of a new runtime model with the unique name (e.g. "New model 1").
2021
+ if (uniqueModelName !== modelName) {
2022
+ logger(`Model.create: Found duplicate name "${modelName}" in schema "${schemaName}", using unique name "${uniqueModelName}"`);
2023
+ }
2024
+ // Step 6a: Generate ID now when needed so we can use a placeholder cache key for runtime creates
2025
+ if (!resolvedId) {
2026
+ resolvedId = generateId();
2027
+ logger(`Model.create: Generated new id (schemaFileId) "${resolvedId}" for model "${modelName}"`);
2028
+ }
2029
+ // Create name-based cache key. When no id (runtime create), use a placeholder so we don't
2030
+ // overwrite the schema model's entry (e.g. "New model"); loadOrCreateModel will replace it with the final name.
2031
+ const nameKey = id
2032
+ ? `${schemaName}:${uniqueModelName}`
2033
+ : `${schemaName}:__pending__${resolvedId}`;
2034
+ // Step 6: Check legacy cache with unique name (backward compatibility during migration)
2035
+ // This is a fallback in case an instance was cached with a unique name
2036
+ if (this.instanceCache.has(nameKey)) {
2037
+ const { instance, refCount } = this.instanceCache.get(nameKey);
2038
+ this.instanceCache.set(nameKey, {
2039
+ instance,
2040
+ refCount: refCount + 1,
2041
+ });
2042
+ logger(`Model.create: Found instance in legacy cache for "${modelName}"`);
2043
+ // Migrate to new cache structure
2044
+ const context = instance._getSnapshotContext();
2045
+ const existingId = context.id; // id is now the schemaFileId (string)
2046
+ if (existingId) {
2047
+ this.instanceCacheById.set(existingId, { instance, refCount: refCount + 1 });
2048
+ this.instanceCacheByName.set(nameKey, existingId);
2049
+ }
2050
+ if (!waitForReady)
2051
+ return instance;
2052
+ return waitForEntityIdle(instance, { timeout: readyTimeout }).then(() => instance);
2053
+ }
2054
+ // Step 7: Create new instance with id in initial context
2055
+ // This ensures loadOrCreateModel sees the ID immediately
2056
+ // Pass _pendingPropertyDefinitions in initial context to avoid race condition with loadOrCreateModel
2057
+ // This is the proper XState way - include it in the initial input so it's available from the start
2058
+ const initialContext = options?.properties
2059
+ ? { _pendingPropertyDefinitions: JSON.parse(JSON.stringify(options.properties)) }
2060
+ : undefined;
2061
+ // Use the unique model name for the instance (idFromSchema: true when id was provided, so schema model is not renamed)
2062
+ const newInstance = new this(uniqueModelName, schemaName, resolvedId, initialContext, !!id);
2063
+ if (options?.properties) {
2064
+ logger(`[Model.create] Created instance with _pendingPropertyDefinitions in initial context (${Object.keys(options.properties).length} properties) for model "${uniqueModelName}"`);
2065
+ }
2066
+ // Step 7: Set initial model data if provided
2067
+ // Note: description is not supported - JSON files can have it but we ignore it at runtime
2068
+ if (options) {
2069
+ const updates = {};
2070
+ if (Object.keys(updates).length > 0) {
2071
+ newInstance._service.send({
2072
+ type: 'updateContext',
2073
+ ...updates,
2074
+ });
2075
+ // Initialize original values
2076
+ // Note: properties are not stored in context, they're computed from ModelProperty instances
2077
+ newInstance._service.send({
2078
+ type: 'initializeOriginalValues',
2079
+ originalValues: {},
2080
+ isEdited: false,
2081
+ });
2082
+ }
2083
+ }
2084
+ // Wrap instance in Proxy for reactive property access
2085
+ // Create a custom proxy that handles 'name' as an alias for 'modelName'
2086
+ const proxiedInstance = new Proxy(newInstance, {
2087
+ get(target, prop) {
2088
+ // Handle special properties
2089
+ if (prop === '_service') {
2090
+ return Reflect.get(target, prop);
2091
+ }
2092
+ // Handle 'name' as an alias for 'modelName'
2093
+ if (prop === 'name') {
2094
+ const context = target._getSnapshotContext();
2095
+ return context.modelName;
2096
+ }
2097
+ // Handle 'id' property - returns the schemaFileId (string)
2098
+ if (prop === 'id') {
2099
+ const context = target._getSnapshotContext();
2100
+ return context.id; // id is now the schemaFileId (string)
2101
+ }
2102
+ // Handle tracked properties
2103
+ if (typeof prop === 'string' && TRACKED_PROPERTIES.includes(prop)) {
2104
+ const context = target._getSnapshotContext();
2105
+ return context[prop];
2106
+ }
2107
+ // Handle 'properties' getter - not tracked, but accessible via getter
2108
+ if (typeof prop === 'string' && prop === 'properties') {
2109
+ // Delegate to the getter method on the instance
2110
+ return target.properties;
2111
+ }
2112
+ // For methods and other properties, use Reflect
2113
+ return Reflect.get(target, prop);
2114
+ },
2115
+ set(target, prop, value) {
2116
+ // Handle special properties
2117
+ if (prop === '_service') {
2118
+ return Reflect.set(target, prop, value);
2119
+ }
2120
+ // Handle 'name' as an alias for 'modelName'
2121
+ if (prop === 'name') {
2122
+ prop = 'modelName';
2123
+ }
2124
+ // Handle tracked properties
2125
+ if (typeof prop === 'string' && TRACKED_PROPERTIES.includes(prop)) {
2126
+ // Use the sendUpdate logic
2127
+ if (prop === 'modelName') {
2128
+ // Model name change - need to update Schema and database
2129
+ const context = target._getSnapshotContext();
2130
+ const oldName = context.modelName;
2131
+ const newName = value;
2132
+ if (oldName === newName) {
2133
+ logger(`Model name unchanged: "${oldName}"`);
2134
+ return true;
2135
+ }
2136
+ logger(`Updating model name from "${oldName}" to "${newName}"`);
2137
+ // Update Model instance cache using new ID-based structure
2138
+ const modelFileId = context.id; // id is now the schemaFileId (string)
2139
+ if (modelFileId) {
2140
+ Model.updateNameIndex(oldName, newName, context.schemaName, modelFileId);
2141
+ }
2142
+ else {
2143
+ // Fallback to legacy cache if no ID (shouldn't happen, but safety)
2144
+ const oldCacheKey = `${context.schemaName}:${oldName}`;
2145
+ const newCacheKey = `${context.schemaName}:${newName}`;
2146
+ if (Model.instanceCache.has(oldCacheKey)) {
2147
+ const entry = Model.instanceCache.get(oldCacheKey);
2148
+ Model.instanceCache.delete(oldCacheKey);
2149
+ Model.instanceCache.set(newCacheKey, entry);
2150
+ }
2151
+ }
2152
+ // Update Model context
2153
+ target._service.send({
2154
+ type: 'updateContext',
2155
+ modelName: newName,
2156
+ });
2157
+ // Mark model as draft
2158
+ target._service.send({
2159
+ type: 'markAsDraft',
2160
+ propertyKey: 'modelName',
2161
+ });
2162
+ // Save draft to database immediately so changes persist
2163
+ target._saveDraftToDb(oldName, newName).catch((error) => {
2164
+ logger(`Failed to save draft to database: ${error instanceof Error ? error.message : String(error)}`);
2165
+ });
2166
+ // NOTE: Schema is not notified of model name changes during edits.
2167
+ // Schema will read from Model instances when persisting/saving.
2168
+ }
2169
+ else if (prop === 'properties') {
2170
+ // Properties are read-only computed values from ModelProperty instances
2171
+ // Cannot be set directly - properties are managed via ModelProperty instances
2172
+ throw new Error('Cannot set model.properties directly. Properties are computed from ModelProperty instances.');
2173
+ }
2174
+ else if (prop === 'description') {
2175
+ // Description is not supported - JSON files can have it but we ignore it at runtime
2176
+ throw new Error('Cannot set model.description. Description is not supported in runtime Model instances.');
2177
+ }
2178
+ else {
2179
+ // Standard property update
2180
+ target._service.send({
2181
+ type: 'updateContext',
2182
+ [prop]: value,
2183
+ });
2184
+ }
2185
+ return true;
2186
+ }
2187
+ // For non-tracked properties, use Reflect
2188
+ return Reflect.set(target, prop, value);
2189
+ },
2190
+ has(target, prop) {
2191
+ // Handle 'name' as an alias for 'modelName'
2192
+ if (prop === 'name') {
2193
+ return true;
2194
+ }
2195
+ // Handle 'id' property
2196
+ if (prop === 'id') {
2197
+ return true;
2198
+ }
2199
+ // Check tracked properties
2200
+ if (typeof prop === 'string' && TRACKED_PROPERTIES.includes(prop)) {
2201
+ const context = target._getSnapshotContext();
2202
+ return prop in context;
2203
+ }
2204
+ return Reflect.has(target, prop);
2205
+ },
2206
+ ownKeys(target) {
2207
+ // Include 'name' and 'id' in ownKeys
2208
+ const keys = Reflect.ownKeys(target);
2209
+ const additionalKeys = [];
2210
+ if (!keys.includes('name')) {
2211
+ additionalKeys.push('name');
2212
+ }
2213
+ if (!keys.includes('id')) {
2214
+ additionalKeys.push('id');
2215
+ }
2216
+ return additionalKeys.length > 0 ? [...keys, ...additionalKeys] : keys;
2217
+ },
2218
+ getOwnPropertyDescriptor(target, prop) {
2219
+ // Handle 'name' as an alias for 'modelName'
2220
+ if (prop === 'name') {
2221
+ return getInvariantSafePropertyDescriptor(target, prop, {
2222
+ value: target._getSnapshotContext().modelName,
2223
+ });
2224
+ }
2225
+ // Handle 'id' property - returns the modelFileId
2226
+ if (prop === 'id') {
2227
+ return getInvariantSafePropertyDescriptor(target, prop, {
2228
+ value: target._getSnapshotContext().id, // id is now the schemaFileId (string)
2229
+ writable: false, // id is read-only
2230
+ });
2231
+ }
2232
+ let invent;
2233
+ if (typeof prop === 'string' && TRACKED_PROPERTIES.includes(prop)) {
2234
+ const context = target._getSnapshotContext();
2235
+ if (prop in context) {
2236
+ invent = { value: context[prop] };
2237
+ }
2238
+ }
2239
+ return getInvariantSafePropertyDescriptor(target, prop, invent);
2240
+ },
2241
+ });
2242
+ // Step 8: Store in new ID-based cache
2243
+ // resolvedId is guaranteed to be defined at this point (generated if not provided)
2244
+ this.instanceCacheById.set(resolvedId, {
2245
+ instance: proxiedInstance,
2246
+ refCount: 1,
2247
+ });
2248
+ // Step 9: Store in name-based index (placeholder key when !id; loadOrCreateModel will replace with final name)
2249
+ this.instanceCacheByName.set(nameKey, resolvedId);
2250
+ // Step 9b: When created with id (schema model) and unique name, also register original name so create(modelName) returns same instance
2251
+ if (id && uniqueModelName !== modelName) {
2252
+ this.instanceCacheByName.set(originalNameKey, resolvedId);
2253
+ }
2254
+ // Step 10: Also store in legacy cache (for backward compatibility during migration)
2255
+ this.instanceCache.set(nameKey, {
2256
+ instance: proxiedInstance,
2257
+ refCount: 1,
2258
+ });
2259
+ logger(`Model.create: Created new instance for "${uniqueModelName}" (ID: ${id})`);
2260
+ // Model instance is now cached and accessible via Model.getById() and Model.getByName()
2261
+ // No wrapper or store registration needed
2262
+ // Step 11: Register with schema if requested, OR trigger write if properties are provided
2263
+ // If properties are provided, we need to write the model to get modelId for property creation
2264
+ // If schema provided, trigger write process instead of registration
2265
+ // When schemaName is passed as string (e.g. useCreateModel), we must trigger write so runtime-created
2266
+ // models get persisted to DB and show up in useModels (which queries the database).
2267
+ const shouldTriggerWrite = (registerWithSchema && schemaInstance) ||
2268
+ (options?.properties && Object.keys(options.properties).length > 0) ||
2269
+ typeof schemaNameOrSchema === 'string';
2270
+ if (shouldTriggerWrite) {
2271
+ queueMicrotask(async () => {
2272
+ try {
2273
+ // Wait for model to reach idle state (validation complete)
2274
+ await new Promise((resolve) => {
2275
+ const subscription = proxiedInstance._service.subscribe((snapshot) => {
2276
+ // Wait for idle state (validation complete) or error state
2277
+ if (snapshot.value === 'idle' || snapshot.value === 'error') {
2278
+ subscription.unsubscribe();
2279
+ resolve();
2280
+ }
2281
+ });
2282
+ // Timeout after 10 seconds
2283
+ setTimeout(() => {
2284
+ subscription.unsubscribe();
2285
+ resolve();
2286
+ }, 10000);
2287
+ });
2288
+ // Only write if validation passed (model is in idle state, not error)
2289
+ const finalSnapshot = proxiedInstance._service.getSnapshot();
2290
+ if (finalSnapshot.value === 'idle' && (!finalSnapshot.context._validationErrors || finalSnapshot.context._validationErrors.length === 0)) {
2291
+ logger(`Model validation passed, triggering write process`);
2292
+ // Get schema ID from schema instance or look it up by schema name
2293
+ let schemaId;
2294
+ if (schemaInstance) {
2295
+ const schemaContext = schemaInstance._getSnapshotContext();
2296
+ // First, try to use _dbId directly from schema context (most reliable)
2297
+ // But verify it exists in database before using it
2298
+ if (schemaContext._dbId && typeof schemaContext._dbId === 'number' && schemaContext._dbId > 0) {
2299
+ try {
2300
+ const db = BaseDb.getAppDb();
2301
+ if (db) {
2302
+ const verifySchema = await db
2303
+ .select({ id: schemas.id })
2304
+ .from(schemas)
2305
+ .where(eq(schemas.id, schemaContext._dbId))
2306
+ .limit(1);
2307
+ if (verifySchema.length > 0) {
2308
+ schemaId = schemaContext._dbId;
2309
+ logger(`Using schema _dbId directly from Schema context: ${schemaId} (verified in database)`);
2310
+ }
2311
+ else {
2312
+ logger(`WARNING: Schema _dbId ${schemaContext._dbId} from context does not exist in database. Falling back to schemaFileId lookup.`);
2313
+ }
2314
+ }
2315
+ }
2316
+ catch (error) {
2317
+ logger(`WARNING: Could not verify schema _dbId: ${error}. Falling back to schemaFileId lookup.`);
2318
+ }
2319
+ }
2320
+ // Fall back to database lookup by schemaFileId if _dbId wasn't available or invalid
2321
+ if (!schemaId) {
2322
+ // Fall back to database lookup by schemaFileId
2323
+ const schemaFileId = schemaContext.id; // id is the schemaFileId (string) in SchemaMachineContext
2324
+ if (schemaFileId) {
2325
+ try {
2326
+ const db = BaseDb.getAppDb();
2327
+ if (db) {
2328
+ const schemaRecords = await db
2329
+ .select()
2330
+ .from(schemas)
2331
+ .where(eq(schemas.schemaFileId, schemaFileId))
2332
+ .limit(1);
2333
+ if (schemaRecords.length > 0) {
2334
+ schemaId = schemaRecords[0].id;
2335
+ logger(`Found schemaId ${schemaId} by schemaFileId lookup: ${schemaFileId}`);
2336
+ }
2337
+ else {
2338
+ logger(`WARNING: Schema not found in database by schemaFileId: ${schemaFileId}. Schema may not be fully loaded yet.`);
2339
+ }
2340
+ }
2341
+ }
2342
+ catch (error) {
2343
+ logger(`Error looking up schemaId: ${error}`);
2344
+ }
2345
+ }
2346
+ else {
2347
+ logger(`WARNING: Schema instance has no schemaFileId (id) in context`);
2348
+ }
2349
+ }
2350
+ }
2351
+ else if (schemaName) {
2352
+ // If no schema instance but we have schema name, look it up by name
2353
+ try {
2354
+ const db = BaseDb.getAppDb();
2355
+ if (db) {
2356
+ const schemaRecords = await db
2357
+ .select()
2358
+ .from(schemas)
2359
+ .where(eq(schemas.name, schemaName))
2360
+ .limit(1);
2361
+ if (schemaRecords.length > 0) {
2362
+ schemaId = schemaRecords[0].id;
2363
+ logger(`Found schemaId ${schemaId} by schema name lookup: ${schemaName}`);
2364
+ }
2365
+ else {
2366
+ logger(`WARNING: Schema not found in database by name: ${schemaName}`);
2367
+ }
2368
+ }
2369
+ }
2370
+ catch (error) {
2371
+ logger(`Error looking up schemaId by name: ${error}`);
2372
+ }
2373
+ }
2374
+ // Validate schemaId before proceeding
2375
+ if (!schemaId || !Number.isInteger(schemaId) || schemaId <= 0) {
2376
+ logger(`ERROR: Invalid schemaId (${schemaId}) for model "${finalSnapshot.context.modelName}". Cannot trigger write process.`);
2377
+ logger(`Schema context:`, {
2378
+ schemaFileId: schemaInstance ? schemaInstance._getSnapshotContext().id : 'N/A',
2379
+ _dbId: schemaInstance ? schemaInstance._getSnapshotContext()._dbId : 'N/A',
2380
+ schemaName: schemaName || 'N/A',
2381
+ });
2382
+ }
2383
+ else if (finalSnapshot.context.id) {
2384
+ // Verify schema exists in database before proceeding (additional safety check)
2385
+ try {
2386
+ const db = BaseDb.getAppDb();
2387
+ if (db) {
2388
+ const schemaCheck = await db
2389
+ .select({ id: schemas.id })
2390
+ .from(schemas)
2391
+ .where(eq(schemas.id, schemaId))
2392
+ .limit(1);
2393
+ if (schemaCheck.length === 0) {
2394
+ const errorMsg = `Schema with id ${schemaId} does not exist in database. Cannot create model "${finalSnapshot.context.modelName}".`;
2395
+ logger(`ERROR: ${errorMsg}`);
2396
+ throw new Error(errorMsg);
2397
+ }
2398
+ logger(`Verified schema ${schemaId} exists in database before creating model "${finalSnapshot.context.modelName}"`);
2399
+ }
2400
+ else {
2401
+ logger(`WARNING: Database not available for schema verification. Proceeding anyway.`);
2402
+ }
2403
+ }
2404
+ catch (error) {
2405
+ // If it's our validation error, re-throw it
2406
+ if (error instanceof Error && error.message.includes('does not exist in database')) {
2407
+ throw error;
2408
+ }
2409
+ logger(`WARNING: Could not verify schema exists in database: ${error}. Proceeding anyway.`);
2410
+ }
2411
+ // Track pending write
2412
+ Model.trackPendingWrite(finalSnapshot.context.id, schemaId); // id is now the schemaFileId (string)
2413
+ // Wait for writeProcess to be spawned (it's spawned in idle state entry action)
2414
+ // Retry a few times if writeProcess isn't available yet
2415
+ let retries = 0;
2416
+ const maxRetries = 10;
2417
+ const checkAndSend = async () => {
2418
+ const currentSnapshot = proxiedInstance._service.getSnapshot();
2419
+ if (currentSnapshot.context.writeProcess) {
2420
+ logger(`Triggering write process for model "${finalSnapshot.context.modelName}" (schemaId: ${schemaId})`);
2421
+ // Use pending property definitions if available, otherwise convert from ModelProperty instances
2422
+ let propertiesObject = {};
2423
+ if (currentSnapshot.context._pendingPropertyDefinitions) {
2424
+ // Use the original property definitions for the write
2425
+ // Convert dataType to type for validation compatibility
2426
+ propertiesObject = JSON.parse(JSON.stringify(currentSnapshot.context._pendingPropertyDefinitions));
2427
+ for (const [propName, propData] of Object.entries(propertiesObject)) {
2428
+ if (propData.dataType && !propData.type) {
2429
+ propertiesObject[propName] = {
2430
+ ...propData,
2431
+ type: propData.dataType,
2432
+ };
2433
+ }
2434
+ }
2435
+ }
2436
+ else {
2437
+ // Fallback: convert ModelProperty instances to object format
2438
+ const properties = proxiedInstance.properties || [];
2439
+ propertiesObject = modelPropertiesToObject(properties);
2440
+ }
2441
+ // Set up subscription to writeProcess to detect write success BEFORE sending request
2442
+ const writeProcess = currentSnapshot.context.writeProcess;
2443
+ // Check current state - if already in success, handle immediately
2444
+ const currentWriteState = writeProcess.getSnapshot();
2445
+ if (currentWriteState.value === 'success') {
2446
+ proxiedInstance._service.send({
2447
+ type: 'writeSuccess',
2448
+ output: currentWriteState.context.entityData,
2449
+ });
2450
+ }
2451
+ else {
2452
+ // Set up subscription to catch future state changes
2453
+ const writeSubscription = writeProcess.subscribe((writeSnapshot) => {
2454
+ if (writeSnapshot.value === 'success') {
2455
+ writeSubscription.unsubscribe();
2456
+ const currentContext = proxiedInstance._service.getSnapshot().context;
2457
+ logger(`[writeProcess subscription] Write succeeded, sending writeSuccess event. Has _pendingPropertyDefinitions: ${!!(currentContext._pendingPropertyDefinitions && Object.keys(currentContext._pendingPropertyDefinitions).length > 0)}`);
2458
+ // Send writeSuccess event to model machine
2459
+ proxiedInstance._service.send({
2460
+ type: 'writeSuccess',
2461
+ output: writeSnapshot.context.entityData,
2462
+ });
2463
+ }
2464
+ else if (writeSnapshot.value === 'error') {
2465
+ writeSubscription.unsubscribe();
2466
+ const errorContext = writeSnapshot.context;
2467
+ logger(`Write process failed for model "${finalSnapshot.context.modelName}": ${errorContext.error?.message || 'Unknown error'}`);
2468
+ logger(`Write process error details:`, errorContext.error);
2469
+ logger(`Validation errors:`, errorContext.validationErrors);
2470
+ }
2471
+ });
2472
+ }
2473
+ proxiedInstance._service.send({
2474
+ type: 'requestWrite',
2475
+ data: {
2476
+ modelFileId: finalSnapshot.context.id, // id is now the schemaFileId (string)
2477
+ modelName: finalSnapshot.context.modelName,
2478
+ schemaName: finalSnapshot.context.schemaName,
2479
+ schemaId,
2480
+ properties: propertiesObject,
2481
+ },
2482
+ });
2483
+ }
2484
+ else if (retries < maxRetries) {
2485
+ retries++;
2486
+ setTimeout(checkAndSend, 50); // Retry after 50ms
2487
+ }
2488
+ else {
2489
+ logger(`ERROR: writeProcess not available after ${maxRetries} retries for model "${finalSnapshot.context.modelName}"`);
2490
+ }
2491
+ };
2492
+ // Start checking immediately, but also allow time for entry action to complete
2493
+ setTimeout(checkAndSend, 0);
2494
+ }
2495
+ else {
2496
+ logger(`Cannot trigger write process: missing schemaId (${schemaId}) or id (${finalSnapshot.context.id})`);
2497
+ }
2498
+ }
2499
+ else {
2500
+ logger(`Model validation failed or model in error state, skipping write process`);
2501
+ }
2502
+ }
2503
+ catch (error) {
2504
+ logger(`Failed to trigger write process: ${error instanceof Error ? error.message : String(error)}`);
2505
+ }
2506
+ });
2507
+ }
2508
+ if (!waitForReady)
2509
+ return proxiedInstance;
2510
+ return waitForEntityIdle(proxiedInstance, { timeout: readyTimeout }).then(() => proxiedInstance);
2511
+ }
2512
+ /**
2513
+ * Get Model instance by modelFileId (O(1) lookup)
2514
+ */
2515
+ static getById(modelFileId) {
2516
+ const entry = this.instanceCacheById.get(modelFileId);
2517
+ if (entry) {
2518
+ // Increment ref count
2519
+ this.instanceCacheById.set(modelFileId, {
2520
+ instance: entry.instance,
2521
+ refCount: entry.refCount + 1,
2522
+ });
2523
+ return entry.instance;
2524
+ }
2525
+ return undefined;
2526
+ }
2527
+ /**
2528
+ * Get Model instance by name (O(1) lookup via name→ID mapping)
2529
+ *
2530
+ * @param modelName - The name of the model
2531
+ * @param schemaName - The schema name (optional, will query DB if not provided)
2532
+ * @returns The Model instance if found, undefined otherwise
2533
+ */
2534
+ static getByName(modelName, schemaName) {
2535
+ if (schemaName) {
2536
+ const nameKey = `${schemaName}:${modelName}`;
2537
+ const id = this.instanceCacheByName.get(nameKey);
2538
+ if (!id) {
2539
+ return undefined;
2540
+ }
2541
+ return this.getById(id);
2542
+ }
2543
+ // If schemaName not provided, try to find in cache by searching all name keys
2544
+ for (const [nameKey, id] of this.instanceCacheByName.entries()) {
2545
+ const [, cachedModelName] = nameKey.split(':');
2546
+ if (cachedModelName === modelName) {
2547
+ return this.getById(id);
2548
+ }
2549
+ }
2550
+ return undefined;
2551
+ }
2552
+ /**
2553
+ * Find Model by modelType (snake_case from DB/metadata).
2554
+ * Handles model names with spaces: "new_model" -> finds "New model" (toSnakeCase("New model") === "new_model").
2555
+ */
2556
+ static findByModelType(modelType) {
2557
+ if (!modelType)
2558
+ return undefined;
2559
+ for (const [nameKey, id] of this.instanceCacheByName.entries()) {
2560
+ const parts = nameKey.split(':', 2);
2561
+ const cachedModelName = parts[1];
2562
+ if (cachedModelName && toSnakeCase(cachedModelName) === modelType) {
2563
+ return this.getById(id);
2564
+ }
2565
+ }
2566
+ return undefined;
2567
+ }
2568
+ /**
2569
+ * Get all Model instances for a schema from cache only (synchronous).
2570
+ * Includes models created at runtime via Model.create() that may not yet be in schema context.
2571
+ */
2572
+ static getCachedInstancesForSchema(schemaName) {
2573
+ const instances = [];
2574
+ const seen = new Set();
2575
+ for (const [nameKey, id] of this.instanceCacheByName.entries()) {
2576
+ const [cachedSchemaName] = nameKey.split(':');
2577
+ if (cachedSchemaName === schemaName && id && !seen.has(id)) {
2578
+ const instance = this.getById(id);
2579
+ if (instance) {
2580
+ instances.push(instance);
2581
+ seen.add(id);
2582
+ }
2583
+ }
2584
+ }
2585
+ for (const [nameKey, entry] of this.instanceCache.entries()) {
2586
+ const [cachedSchemaName] = nameKey.split(':');
2587
+ if (cachedSchemaName === schemaName) {
2588
+ const context = entry.instance._getSnapshotContext();
2589
+ const id = context.id;
2590
+ if (id && !seen.has(id)) {
2591
+ instances.push(entry.instance);
2592
+ seen.add(id);
2593
+ }
2594
+ }
2595
+ }
2596
+ return instances;
2597
+ }
2598
+ /**
2599
+ * Get Model instance by name, querying database if not in cache
2600
+ * This is an async version that can query the database when schemaName is not provided
2601
+ *
2602
+ * @param modelName - The name of the model
2603
+ * @param schemaName - Optional schema name (will query DB if not provided)
2604
+ * @returns The Model instance if found, undefined otherwise
2605
+ */
2606
+ static async getByNameAsync(modelName, schemaName) {
2607
+ // First try cache
2608
+ const cached = this.getByName(modelName, schemaName);
2609
+ if (cached) {
2610
+ return cached;
2611
+ }
2612
+ // If not in cache and schemaName not provided, query database
2613
+ if (!schemaName) {
2614
+ const db = BaseDb.getAppDb();
2615
+ if (!db) {
2616
+ return undefined;
2617
+ }
2618
+ try {
2619
+ // Query model by name
2620
+ const modelRecords = await db
2621
+ .select({
2622
+ modelFileId: models.schemaFileId,
2623
+ modelName: models.name,
2624
+ schemaName: schemas.name,
2625
+ })
2626
+ .from(models)
2627
+ .innerJoin(modelSchemas, eq(models.id, modelSchemas.modelId))
2628
+ .innerJoin(schemas, eq(modelSchemas.schemaId, schemas.id))
2629
+ .where(eq(models.name, modelName))
2630
+ .limit(1);
2631
+ if (modelRecords.length === 0) {
2632
+ return undefined;
2633
+ }
2634
+ const record = modelRecords[0];
2635
+ if (!record.modelName || !record.schemaName || !record.modelFileId) {
2636
+ return undefined;
2637
+ }
2638
+ // Create model instance (will be cached)
2639
+ return this.create(record.modelName, record.schemaName, {
2640
+ id: record.modelFileId, // id is now the schemaFileId (string)
2641
+ });
2642
+ }
2643
+ catch (error) {
2644
+ logger(`Model.getByNameAsync: Error looking up model by name "${modelName}": ${error instanceof Error ? error.message : String(error)}`);
2645
+ return undefined;
2646
+ }
2647
+ }
2648
+ return undefined;
2649
+ }
2650
+ /**
2651
+ * Create or get Model instance by modelFileId
2652
+ * Checks cache first, then database if not found
2653
+ *
2654
+ * @param modelFileId - The model file ID to look up
2655
+ * @returns The Model instance if found, undefined otherwise
2656
+ */
2657
+ static async createById(modelFileId) {
2658
+ if (!modelFileId) {
2659
+ return undefined;
2660
+ }
2661
+ // Step 1: Check cache first (fast path)
2662
+ const cached = this.getById(modelFileId);
2663
+ if (cached) {
2664
+ logger(`Model.createById: Found instance in cache for ID "${modelFileId}"`);
2665
+ return cached;
2666
+ }
2667
+ // Step 2: Query database to get modelName and schemaName
2668
+ const db = BaseDb.getAppDb();
2669
+ if (!db) {
2670
+ logger(`Model.createById: Database not available for ID "${modelFileId}"`);
2671
+ return undefined;
2672
+ }
2673
+ try {
2674
+ // Query model by schemaFileId and join to get schema name
2675
+ const modelRecords = await db
2676
+ .select({
2677
+ modelId: models.id,
2678
+ modelName: models.name,
2679
+ schemaName: schemas.name,
2680
+ })
2681
+ .from(models)
2682
+ .innerJoin(modelSchemas, eq(models.id, modelSchemas.modelId))
2683
+ .innerJoin(schemas, eq(modelSchemas.schemaId, schemas.id))
2684
+ .where(eq(models.schemaFileId, modelFileId))
2685
+ .limit(1);
2686
+ if (modelRecords.length === 0) {
2687
+ logger(`Model.createById: Model not found in database for ID "${modelFileId}"`);
2688
+ return undefined;
2689
+ }
2690
+ const { modelName, schemaName } = modelRecords[0];
2691
+ if (!modelName || !schemaName) {
2692
+ logger(`Model.createById: Missing modelName or schemaName for ID "${modelFileId}"`);
2693
+ return undefined;
2694
+ }
2695
+ // Step 3: Create model using existing create method (which will handle caching)
2696
+ logger(`Model.createById: Creating model "${modelName}" in schema "${schemaName}" from ID "${modelFileId}"`);
2697
+ return this.create(modelName, schemaName, {
2698
+ id: modelFileId, // id is now the schemaFileId (string)
2699
+ });
2700
+ }
2701
+ catch (error) {
2702
+ logger(`Model.createById: Error looking up model by ID "${modelFileId}": ${error instanceof Error ? error.message : String(error)}`);
2703
+ return undefined;
2704
+ }
2705
+ }
2706
+ /**
2707
+ * Find Model instance by modelFileId, modelName/schemaName, or both
2708
+ * Waits for the model to be fully loaded (idle state) by default
2709
+ * @param options - Find options including lookup parameters and wait configuration
2710
+ * @returns Model instance if found, undefined otherwise
2711
+ */
2712
+ static async find({ modelFileId, modelName, schemaName, waitForReady = true, readyTimeout = 5000, }) {
2713
+ return await findEntity({
2714
+ getById: (id) => Model.getById(id),
2715
+ createById: (id) => Model.createById(id),
2716
+ getByName: (name, ...args) => {
2717
+ const schemaName = args[0];
2718
+ return Model.getByName(name, schemaName);
2719
+ },
2720
+ createByName: async (name, ...args) => {
2721
+ const schemaName = args[0];
2722
+ return Model.getByNameAsync(name, schemaName);
2723
+ },
2724
+ }, {
2725
+ id: modelFileId,
2726
+ name: modelName,
2727
+ schemaName,
2728
+ }, {
2729
+ waitForReady,
2730
+ readyTimeout,
2731
+ });
2732
+ }
2733
+ /**
2734
+ * Get all Model instances for a schema by schemaFileId or schemaName
2735
+ * Queries database for all models with the given schemaId/schemaName and returns Model instances
2736
+ *
2737
+ * @param schemaIdentifier - The schema file ID or schema name to get models for
2738
+ * @returns Array of Model instances for the schema
2739
+ */
2740
+ static async createBySchemaId(schemaIdentifier) {
2741
+ if (!schemaIdentifier) {
2742
+ return [];
2743
+ }
2744
+ const db = BaseDb.getAppDb();
2745
+ if (!db) {
2746
+ logger(`Model.createBySchemaId: Database not available for schema "${schemaIdentifier}"`);
2747
+ return [];
2748
+ }
2749
+ try {
2750
+ // Query all models for this schema - support both schemaFileId and schema name
2751
+ const modelRecords = await db
2752
+ .select({
2753
+ modelFileId: models.schemaFileId,
2754
+ modelName: models.name,
2755
+ schemaName: schemas.name,
2756
+ })
2757
+ .from(schemas)
2758
+ .innerJoin(modelSchemas, eq(schemas.id, modelSchemas.schemaId))
2759
+ .innerJoin(models, eq(modelSchemas.modelId, models.id))
2760
+ .where(or(eq(schemas.schemaFileId, schemaIdentifier), eq(schemas.name, schemaIdentifier)));
2761
+ if (modelRecords.length === 0) {
2762
+ logger(`Model.createBySchemaId: No models found in database for schema "${schemaIdentifier}"`);
2763
+ return [];
2764
+ }
2765
+ // Create Model instances for each model (create() handles caching)
2766
+ const modelInstances = [];
2767
+ for (const record of modelRecords) {
2768
+ if (!record.modelName || !record.schemaName || !record.modelFileId) {
2769
+ continue;
2770
+ }
2771
+ // Check cache first
2772
+ const cached = this.getById(record.modelFileId);
2773
+ if (cached) {
2774
+ modelInstances.push(cached);
2775
+ }
2776
+ else {
2777
+ // Create model instance (will be cached)
2778
+ const model = this.create(record.modelName, record.schemaName, {
2779
+ id: record.modelFileId, // id is now the schemaFileId (string)
2780
+ waitForReady: false,
2781
+ });
2782
+ modelInstances.push(model);
2783
+ }
2784
+ }
2785
+ logger(`Model.createBySchemaId: Found ${modelInstances.length} models for schema "${schemaIdentifier}"`);
2786
+ return modelInstances;
2787
+ }
2788
+ catch (error) {
2789
+ logger(`Model.createBySchemaId: Error looking up models for schema "${schemaIdentifier}": ${error instanceof Error ? error.message : String(error)}`);
2790
+ return [];
2791
+ }
2792
+ }
2793
+ /**
2794
+ * Get all Model instances, optionally filtered by schema.
2795
+ * When DB is available, loads from DB via getModelsData; otherwise returns from cache.
2796
+ * Supports waitForReady to wait for each model to reach idle state before returning.
2797
+ */
2798
+ static async all(schemaName, options) {
2799
+ const { waitForReady = false, readyTimeout = 5000 } = options ?? {};
2800
+ const appDb = BaseDb.getAppDb();
2801
+ if (!appDb) {
2802
+ const instances = [];
2803
+ const seen = new Set();
2804
+ if (schemaName !== undefined && schemaName !== '') {
2805
+ for (const [nameKey, id] of this.instanceCacheByName.entries()) {
2806
+ const [cachedSchemaName] = nameKey.split(':');
2807
+ if (cachedSchemaName === schemaName) {
2808
+ const instance = this.getById(id);
2809
+ if (instance && !seen.has(id)) {
2810
+ instances.push(instance);
2811
+ seen.add(id);
2812
+ }
2813
+ }
2814
+ }
2815
+ for (const [nameKey, entry] of this.instanceCache.entries()) {
2816
+ const [cachedSchemaName] = nameKey.split(':');
2817
+ if (cachedSchemaName === schemaName) {
2818
+ const context = entry.instance._getSnapshotContext();
2819
+ const id = context.id;
2820
+ if (id && !seen.has(id)) {
2821
+ instances.push(entry.instance);
2822
+ seen.add(id);
2823
+ }
2824
+ }
2825
+ }
2826
+ }
2827
+ else {
2828
+ for (const [id, entry] of this.instanceCacheById.entries()) {
2829
+ if (!seen.has(id)) {
2830
+ instances.push(entry.instance);
2831
+ seen.add(id);
2832
+ }
2833
+ }
2834
+ for (const [nameKey, entry] of this.instanceCache.entries()) {
2835
+ const context = entry.instance._getSnapshotContext();
2836
+ const id = context.id;
2837
+ if (id && !seen.has(id)) {
2838
+ instances.push(entry.instance);
2839
+ seen.add(id);
2840
+ }
2841
+ }
2842
+ }
2843
+ return instances;
2844
+ }
2845
+ const rows = await getModelsData(schemaName);
2846
+ const instances = [];
2847
+ const seen = new Set();
2848
+ for (const row of rows) {
2849
+ if (row.schemaFileId) {
2850
+ const instance = await this.createById(row.schemaFileId);
2851
+ if (instance) {
2852
+ instances.push(instance);
2853
+ seen.add(instance.id ?? row.schemaFileId);
2854
+ }
2855
+ }
2856
+ }
2857
+ // Merge in any instances from cache for this schema (e.g. just-created model not yet visible in DB read).
2858
+ if (schemaName !== undefined && schemaName !== '') {
2859
+ for (const [nameKey, id] of this.instanceCacheByName.entries()) {
2860
+ const [cachedSchemaName] = nameKey.split(':');
2861
+ if (cachedSchemaName === schemaName && id && !seen.has(id)) {
2862
+ const instance = this.getById(id);
2863
+ if (instance) {
2864
+ instances.push(instance);
2865
+ seen.add(id);
2866
+ }
2867
+ }
2868
+ }
2869
+ for (const [nameKey, entry] of this.instanceCache.entries()) {
2870
+ const [cachedSchemaName] = nameKey.split(':');
2871
+ if (cachedSchemaName === schemaName) {
2872
+ const context = entry.instance._getSnapshotContext();
2873
+ const id = context.id;
2874
+ if (id && !seen.has(id)) {
2875
+ instances.push(entry.instance);
2876
+ seen.add(id);
2877
+ }
2878
+ }
2879
+ }
2880
+ }
2881
+ if (waitForReady && instances.length > 0) {
2882
+ await Promise.all(instances.map((m) => waitForEntityIdle(m, {
2883
+ timeout: readyTimeout,
2884
+ throwOnError: false,
2885
+ })));
2886
+ return instances.filter((m) => m._getSnapshot?.().value === 'idle');
2887
+ }
2888
+ return instances;
2889
+ }
2890
+ /**
2891
+ * Update name index when model name changes
2892
+ */
2893
+ static updateNameIndex(oldName, newName, schemaName, modelFileId) {
2894
+ const oldKey = `${schemaName}:${oldName}`;
2895
+ const newKey = `${schemaName}:${newName}`;
2896
+ // Update name index
2897
+ if (this.instanceCacheByName.has(oldKey)) {
2898
+ this.instanceCacheByName.delete(oldKey);
2899
+ }
2900
+ this.instanceCacheByName.set(newKey, modelFileId);
2901
+ // Also update legacy cache if it exists
2902
+ if (this.instanceCache.has(oldKey)) {
2903
+ const entry = this.instanceCache.get(oldKey);
2904
+ this.instanceCache.delete(oldKey);
2905
+ this.instanceCache.set(newKey, entry);
2906
+ }
2907
+ logger(`Model.updateNameIndex: Updated name mapping from "${oldName}" to "${newName}" (ID: ${modelFileId})`);
2908
+ }
2909
+ getService() {
2910
+ return this._service;
2911
+ }
2912
+ _getSnapshot() {
2913
+ return this._service.getSnapshot();
2914
+ }
2915
+ _getSnapshotContext() {
2916
+ return this._getSnapshot().context;
2917
+ }
2918
+ /**
2919
+ * Check for conflicts between actor context and database
2920
+ * @returns ConflictResult indicating if a conflict exists
2921
+ */
2922
+ async _checkForConflicts() {
2923
+ const context = this._getSnapshotContext();
2924
+ // If we don't have load metadata, can't check for conflicts
2925
+ if (!context._dbUpdatedAt || !context.id) {
2926
+ return { hasConflict: false };
2927
+ }
2928
+ try {
2929
+ const db = BaseDb.getAppDb();
2930
+ if (!db) {
2931
+ return { hasConflict: false }; // Can't check without DB
2932
+ }
2933
+ // Get current DB record by id (schemaFileId) - most reliable lookup
2934
+ const dbModels = await db
2935
+ .select()
2936
+ .from(models)
2937
+ .where(eq(models.schemaFileId, context.id)) // id is now the schemaFileId (string)
2938
+ .limit(1);
2939
+ if (dbModels.length === 0) {
2940
+ return { hasConflict: false }; // No DB record, no conflict
2941
+ }
2942
+ const dbRecord = dbModels[0];
2943
+ // Check if DB was updated after we loaded
2944
+ // Note: models table doesn't have updatedAt, so we can't do timestamp comparison
2945
+ // For now, we'll just check if the record exists and matches
2946
+ // In the future, if models table gets updatedAt, we can compare timestamps
2947
+ return { hasConflict: false };
2948
+ }
2949
+ catch (error) {
2950
+ logger(`Error checking for conflicts: ${error instanceof Error ? error.message : String(error)}`);
2951
+ return { hasConflict: false }; // On error, assume no conflict to allow save
2952
+ }
2953
+ }
2954
+ /**
2955
+ * Saves model name changes to the database immediately (draft save)
2956
+ * This is called when modelName is changed to persist the change immediately
2957
+ * @param oldName - The old model name (if name changed)
2958
+ * @param newName - The new model name
2959
+ */
2960
+ async _saveDraftToDb(oldName, newName) {
2961
+ saveDraftLogger(`_saveDraftToDb called for model (oldName: "${oldName}", newName: "${newName}")`);
2962
+ // Don't save during initialization - models are being loaded from files, not created as drafts
2963
+ // Check this FIRST before doing any expensive work like _getSnapshotContext()
2964
+ if (typeof window !== 'undefined') {
2965
+ const now = Date.now();
2966
+ // Use cached check to avoid expensive operations
2967
+ // Always check if cache is stale or if we previously got false (to allow recovery)
2968
+ const cacheIsStale = Model.cachedClientInitialized === null || (now - Model.clientCheckTime) > Model.CLIENT_CHECK_CACHE_MS;
2969
+ const shouldRecheck = cacheIsStale || Model.cachedClientInitialized === false;
2970
+ saveDraftLogger(`Client check: cacheIsStale=${cacheIsStale}, shouldRecheck=${shouldRecheck}, cachedValue=${Model.cachedClientInitialized}, timeSinceCheck=${now - Model.clientCheckTime}ms`);
2971
+ if (shouldRecheck) {
2972
+ try {
2973
+ // Use dynamic import for browser compatibility (require() doesn't work in browsers)
2974
+ const client = getClient();
2975
+ const clientSnapshot = client.getService().getSnapshot();
2976
+ // Check if state is IDLE (primary check) - isInitialized is set in entry action so should be true
2977
+ // But we check it as a secondary safeguard
2978
+ const isIdle = clientSnapshot.value === ClientManagerState.IDLE;
2979
+ const isInitialized = clientSnapshot.context.isInitialized;
2980
+ // If state is IDLE, trust it even if isInitialized isn't set yet (entry action should set it)
2981
+ // This aligns with useIsClientReady which only checks the state value
2982
+ Model.cachedClientInitialized = isIdle && (isInitialized !== false);
2983
+ Model.clientCheckTime = now;
2984
+ saveDraftLogger(`Client state checked: state=${clientSnapshot.value}, isIdle=${isIdle}, isInitialized=${isInitialized}, result=${Model.cachedClientInitialized}`);
2985
+ }
2986
+ catch (error) {
2987
+ // If we can't check client state, assume not initialized to be safe
2988
+ // But only cache for a short time to allow recovery
2989
+ Model.cachedClientInitialized = false;
2990
+ Model.clientCheckTime = now;
2991
+ saveDraftLogger(`Error checking client state: ${error instanceof Error ? error.message : String(error)}, setting cachedClientInitialized=false`);
2992
+ }
2993
+ }
2994
+ else {
2995
+ saveDraftLogger(`Using cached client state: cachedClientInitialized=${Model.cachedClientInitialized}`);
2996
+ }
2997
+ if (!Model.cachedClientInitialized) {
2998
+ saveDraftLogger(`Client not initialized, skipping save (oldName: "${oldName}", newName: "${newName}")`);
2999
+ // Skip silently during initialization to avoid log spam and reduce overhead
3000
+ return;
3001
+ }
3002
+ }
3003
+ saveDraftLogger(`Client is initialized, proceeding with save (oldName: "${oldName}", newName: "${newName}")`);
3004
+ // Check for conflicts before saving
3005
+ const conflictCheck = await this._checkForConflicts();
3006
+ if (conflictCheck.hasConflict) {
3007
+ const errorMessage = conflictCheck.message || 'Database was updated externally. Please reload the model and try again.';
3008
+ saveDraftLogger(`CONFLICT DETECTED: ${errorMessage}`);
3009
+ throw new ConflictError(errorMessage, conflictCheck);
3010
+ }
3011
+ const context = this._getSnapshotContext();
3012
+ const modelFileId = context.id || ''; // id is now the schemaFileId (string)
3013
+ const modelName = newName || context.modelName || oldName || '';
3014
+ // Use modelFileId as the key if available, otherwise use schemaName:modelName
3015
+ const saveKey = modelFileId || `${context.schemaName}:${modelName}`;
3016
+ // Prevent concurrent saves for the same model
3017
+ if (Model.savingModels.has(saveKey)) {
3018
+ saveDraftLogger(`Model ${modelName} (key: ${saveKey}) is already being saved, skipping concurrent save`);
3019
+ return;
3020
+ }
3021
+ Model.savingModels.add(saveKey);
3022
+ saveDraftLogger(`Starting save for model ${modelName} (key: ${saveKey}, id: ${modelFileId})`);
3023
+ try {
3024
+ const db = BaseDb.getAppDb();
3025
+ if (!db) {
3026
+ throw new Error('Database not found');
3027
+ }
3028
+ // If name changed, use renameModelInDb
3029
+ if (oldName && newName && oldName !== newName) {
3030
+ saveDraftLogger(`Renaming model from "${oldName}" to "${newName}"`);
3031
+ await renameModelInDb(oldName, newName, context.schemaName);
3032
+ saveDraftLogger(`Successfully renamed model from "${oldName}" to "${newName}"`);
3033
+ }
3034
+ else {
3035
+ // For other tracked properties, we would save here
3036
+ // Currently only modelName is saved immediately, so this is a no-op
3037
+ saveDraftLogger(`No name change detected, skipping save (modelName: "${modelName}")`);
3038
+ }
3039
+ // Update conflict detection metadata after successful save
3040
+ // Note: models table doesn't have updatedAt, so we use current timestamp
3041
+ const now = Date.now();
3042
+ this._service.send({
3043
+ type: 'updateContext',
3044
+ _dbUpdatedAt: now,
3045
+ });
3046
+ }
3047
+ catch (error) {
3048
+ logger(`Failed to save draft to database: ${error instanceof Error ? error.message : String(error)}`);
3049
+ // Don't throw - allow operation to continue
3050
+ }
3051
+ finally {
3052
+ // Always remove from saving set
3053
+ Model.savingModels.delete(saveKey);
3054
+ saveDraftLogger(`Finished save for model ${modelName} (key: ${saveKey})`);
3055
+ }
3056
+ }
3057
+ get status() {
3058
+ return this._getSnapshot().value;
3059
+ }
3060
+ get validationErrors() {
3061
+ return this._getSnapshotContext()._validationErrors || [];
3062
+ }
3063
+ get isValid() {
3064
+ const errors = this.validationErrors;
3065
+ return errors.length === 0;
3066
+ }
3067
+ get isEdited() {
3068
+ const context = this._getSnapshotContext();
3069
+ // First check in-memory state
3070
+ if (context._isEdited !== undefined) {
3071
+ return context._isEdited;
3072
+ }
3073
+ // Fall back to reading from database if we have _dbId
3074
+ if (context._dbId) {
3075
+ // Load from database asynchronously (fire-and-forget for now)
3076
+ // For synchronous getter, we'll need to cache it or make it async
3077
+ // For now, return false if not in context
3078
+ return false;
3079
+ }
3080
+ return false;
3081
+ }
3082
+ get id() {
3083
+ return this._getSnapshotContext().id; // id is now the schemaFileId (string)
3084
+ }
3085
+ get name() {
3086
+ return this._getSnapshotContext().modelName;
3087
+ }
3088
+ /**
3089
+ * Returns ModelProperty instances for this model
3090
+ * This is a computed property that reads from the service context
3091
+ * Note: This is NOT reactive - use useModelProperties() hook for reactivity
3092
+ */
3093
+ get properties() {
3094
+ const snapshot = this._service.getSnapshot();
3095
+ const context = snapshot.context;
3096
+ // Get property IDs from liveQuery (stored in context, not instanceState)
3097
+ const liveQueryIds = context._liveQueryPropertyIds || [];
3098
+ // Get _dbId from context for pending writes lookup
3099
+ const dbId = context._dbId; // _dbId is the database integer ID
3100
+ // Get pending property IDs (synchronous - uses static Map)
3101
+ // Lazy import ModelProperty to avoid circular dependency
3102
+ const ModelProperty = getModelProperty();
3103
+ if (!ModelProperty) {
3104
+ // If ModelProperty is not yet loaded, return empty array
3105
+ // It will be available on subsequent accesses after the async import completes
3106
+ return [];
3107
+ }
3108
+ const pendingIds = dbId ? ModelProperty.getPendingPropertyIds(dbId) : [];
3109
+ // Combine and deduplicate
3110
+ const allPropertyIds = [...new Set([...liveQueryIds, ...pendingIds])];
3111
+ // Get ModelProperty instances from static cache (synchronous)
3112
+ const propertyInstances = [];
3113
+ for (const propertyFileId of allPropertyIds) {
3114
+ const property = ModelProperty.getById(propertyFileId);
3115
+ if (property) {
3116
+ propertyInstances.push(property);
3117
+ }
3118
+ // Note: Cannot create properties asynchronously in this synchronous getter
3119
+ // Properties will be created elsewhere when needed
3120
+ }
3121
+ // Return a new array reference (snapshot at time of access)
3122
+ return [...propertyInstances];
3123
+ }
3124
+ /**
3125
+ * Validate the model
3126
+ * @returns Validation result
3127
+ */
3128
+ async validate() {
3129
+ return new Promise((resolve) => {
3130
+ let resolved = false;
3131
+ const subscription = this._service.subscribe((snapshot) => {
3132
+ if (snapshot.value === 'idle' || snapshot.value === 'error') {
3133
+ if (!resolved) {
3134
+ resolved = true;
3135
+ subscription.unsubscribe();
3136
+ const errors = snapshot.context._validationErrors || [];
3137
+ resolve({
3138
+ isValid: errors.length === 0,
3139
+ errors,
3140
+ });
3141
+ }
3142
+ }
3143
+ });
3144
+ this._service.send({ type: 'validateModel' });
3145
+ // Timeout fallback to ensure we always resolve (15 seconds - longer than actor timeout)
3146
+ setTimeout(() => {
3147
+ if (!resolved) {
3148
+ resolved = true;
3149
+ subscription.unsubscribe();
3150
+ const errors = this._getSnapshotContext()._validationErrors || [];
3151
+ resolve({
3152
+ isValid: errors.length === 0,
3153
+ errors,
3154
+ });
3155
+ }
3156
+ }, 15000);
3157
+ });
3158
+ }
3159
+ /**
3160
+ * Create a new item instance from this model
3161
+ * Automatically injects the model name, so you don't need to pass it explicitly.
3162
+ *
3163
+ * @example
3164
+ * const Post = Model.create('Post', schema)
3165
+ * const post = await Post.create({ title: 'My Post', content: '...' })
3166
+ *
3167
+ * @param values - Item property values (modelName is automatically injected)
3168
+ * @returns The created item instance
3169
+ */
3170
+ async create(values) {
3171
+ if (!this.modelName) {
3172
+ throw new Error('Cannot create item: model name is not set on this Model instance');
3173
+ }
3174
+ const item = await Item.create({
3175
+ modelName: this.modelName,
3176
+ schemaName: this.schemaName,
3177
+ modelInstance: this,
3178
+ ...values,
3179
+ });
3180
+ return item;
3181
+ }
3182
+ /**
3183
+ * Reload model from database
3184
+ * This refreshes the actor context with the latest data from the database
3185
+ */
3186
+ async reload() {
3187
+ logger(`Reloading model ${this.modelName} from database`);
3188
+ // Send reload event to machine (which will trigger loadOrCreateModel)
3189
+ this._service.send({ type: 'reloadFromDb' });
3190
+ // Wait for reload to complete
3191
+ return new Promise((resolve, reject) => {
3192
+ const subscription = this._service.subscribe((snapshot) => {
3193
+ if (snapshot.value === 'idle') {
3194
+ subscription.unsubscribe();
3195
+ resolve();
3196
+ }
3197
+ else if (snapshot.value === 'error') {
3198
+ subscription.unsubscribe();
3199
+ reject(new Error('Failed to reload model from database'));
3200
+ }
3201
+ });
3202
+ // Timeout after 10 seconds
3203
+ setTimeout(() => {
3204
+ subscription.unsubscribe();
3205
+ reject(new Error('Timeout waiting for model reload'));
3206
+ }, 10000);
3207
+ });
3208
+ }
3209
+ /**
3210
+ * Unload the model instance and clean up resources
3211
+ */
3212
+ unload() {
3213
+ try {
3214
+ const context = this._getSnapshotContext();
3215
+ const modelFileId = context.id; // id is now the schemaFileId (string)
3216
+ const nameKey = `${context.schemaName}:${context.modelName}`;
3217
+ const cacheKeys = [];
3218
+ if (modelFileId) {
3219
+ cacheKeys.push(modelFileId);
3220
+ }
3221
+ cacheKeys.push(nameKey);
3222
+ unloadEntity(this, {
3223
+ getCacheKeys: () => cacheKeys,
3224
+ caches: [Model.instanceCacheById, Model.instanceCache],
3225
+ secondaryCaches: [Model.instanceCacheByName],
3226
+ instanceState: modelInstanceState,
3227
+ getService: (instance) => instance._service,
3228
+ });
3229
+ }
3230
+ catch (error) {
3231
+ logger(`Error during unload: ${error instanceof Error ? error.message : String(error)}`);
3232
+ // Still try to stop service
3233
+ try {
3234
+ this._service.stop();
3235
+ }
3236
+ catch {
3237
+ // Service might already be stopped
3238
+ }
3239
+ }
3240
+ }
3241
+ /**
3242
+ * Destroy the model instance completely: remove from caches, delete from database,
3243
+ * update Schema context, stop service. Uses shared destroy helpers.
3244
+ */
3245
+ async destroy() {
3246
+ const context = this._getSnapshotContext();
3247
+ const modelFileId = context.id;
3248
+ const modelName = context.modelName;
3249
+ const schemaName = context.schemaName;
3250
+ const nameKey = schemaName && modelName ? `${schemaName}:${modelName}` : '';
3251
+ clearDestroySubscriptions(this, { instanceState: modelInstanceState });
3252
+ const cacheKeys = [];
3253
+ if (modelFileId)
3254
+ cacheKeys.push(modelFileId);
3255
+ if (nameKey)
3256
+ cacheKeys.push(nameKey);
3257
+ forceRemoveFromCaches(this, {
3258
+ getCacheKeys: () => cacheKeys,
3259
+ caches: [
3260
+ Model.instanceCacheById,
3261
+ Model.instanceCacheByName,
3262
+ Model.instanceCache,
3263
+ ],
3264
+ });
3265
+ await runDestroyLifecycle(this, {
3266
+ getService: (instance) => instance._service,
3267
+ doDestroy: async () => {
3268
+ const db = BaseDb.getAppDb();
3269
+ if (db && modelFileId) {
3270
+ const drizzleMod = await import('drizzle-orm');
3271
+ const { eq } = drizzleMod;
3272
+ const modelRecords = await db
3273
+ .select()
3274
+ .from(models)
3275
+ .where(eq(models.schemaFileId, modelFileId))
3276
+ .limit(1);
3277
+ if (modelRecords.length > 0) {
3278
+ const modelId = modelRecords[0].id;
3279
+ await db.delete(modelSchemas).where(eq(modelSchemas.modelId, modelId));
3280
+ await db
3281
+ .delete(properties)
3282
+ .where(eq(properties.modelId, typeof modelId === 'string' ? parseInt(modelId, 10) : modelId));
3283
+ await db.delete(models).where(eq(models.id, modelId));
3284
+ }
3285
+ }
3286
+ if (schemaName && modelName) {
3287
+ const Schema = await schemaImportPromise;
3288
+ if (!Schema)
3289
+ throw new Error('Schema not loaded');
3290
+ const schema = Schema.create(schemaName, { waitForReady: false });
3291
+ const schemaContext = schema.getService().getSnapshot().context;
3292
+ if (schemaContext.models?.[modelName]) {
3293
+ const updatedModels = { ...schemaContext.models };
3294
+ delete updatedModels[modelName];
3295
+ schema.getService().send({ type: 'updateContext', models: updatedModels });
3296
+ schema.getService().send({ type: 'markAsDraft', propertyKey: `model:${modelName}` });
3297
+ await new Promise((resolve) => setTimeout(resolve, 100));
3298
+ }
3299
+ }
3300
+ },
3301
+ });
3302
+ }
3303
+ /**
3304
+ * Set up liveQuery subscription to watch for property changes in the database
3305
+ * This enables cross-instance synchronization (e.g., changes in other tabs/windows)
3306
+ */
3307
+ _setupLiveQuerySubscription() {
3308
+ setupEntityLiveQuery(this, {
3309
+ getEntityId: async (model) => {
3310
+ const context = model._getSnapshotContext();
3311
+ let dbId = context._dbId; // _dbId is the database integer ID
3312
+ const modelName = context.modelName;
3313
+ if (!modelName) {
3314
+ return undefined;
3315
+ }
3316
+ // If we don't have _dbId yet, try to find it in the database
3317
+ if (!dbId) {
3318
+ try {
3319
+ const db = BaseDb.getAppDb();
3320
+ if (!db) {
3321
+ return undefined;
3322
+ }
3323
+ const modelRecords = await db
3324
+ .select()
3325
+ .from(models)
3326
+ .where(eq(models.name, modelName))
3327
+ .limit(1);
3328
+ if (modelRecords.length === 0 || !modelRecords[0].id) {
3329
+ return undefined;
3330
+ }
3331
+ dbId = modelRecords[0].id;
3332
+ // Update context with _dbId
3333
+ model._service.send({
3334
+ type: 'updateContext',
3335
+ _dbId: dbId,
3336
+ });
3337
+ }
3338
+ catch (error) {
3339
+ logger(`[Model._setupLiveQuerySubscription] Error finding model: ${error}`);
3340
+ return undefined;
3341
+ }
3342
+ }
3343
+ return dbId;
3344
+ },
3345
+ buildQuery: async (modelId) => {
3346
+ return BaseDb.liveQuery((sql) => sql `
3347
+ SELECT id, name, data_type as dataType, model_id as modelId, ref_model_id as refModelId, ref_value_type as refValueType, schema_file_id as schemaFileId
3348
+ FROM properties
3349
+ WHERE model_id = ${modelId}
3350
+ `);
3351
+ },
3352
+ extractEntityIds: (rows) => rows
3353
+ .map(row => row.schemaFileId)
3354
+ .filter((id) => id !== null && id !== undefined),
3355
+ updateContext: (model, ids) => {
3356
+ model._service.send({
3357
+ type: 'updateContext',
3358
+ _liveQueryPropertyIds: ids, // Store in service context for reactivity
3359
+ _propertiesUpdated: Date.now(), // Internal field for tracking
3360
+ });
3361
+ },
3362
+ createChildInstances: async (ids) => {
3363
+ await modelPropertyImportPromise;
3364
+ const ModelProperty = getModelProperty();
3365
+ if (!ModelProperty) {
3366
+ logger('[Model._setupLiveQuerySubscription] ModelProperty not yet loaded');
3367
+ return;
3368
+ }
3369
+ const createPromises = ids.map(async (propertyFileId) => {
3370
+ try {
3371
+ await ModelProperty.createById(propertyFileId);
3372
+ }
3373
+ catch (error) {
3374
+ logger(`[Model._setupLiveQuerySubscription] Error creating ModelProperty instance for propertyFileId "${propertyFileId}": ${error}`);
3375
+ }
3376
+ });
3377
+ await Promise.all(createPromises);
3378
+ },
3379
+ queryInitialData: async (modelId) => {
3380
+ const db = BaseDb.getAppDb();
3381
+ if (!db) {
3382
+ return [];
3383
+ }
3384
+ const initialProperties = await db
3385
+ .select({ schemaFileId: properties.schemaFileId })
3386
+ .from(properties)
3387
+ .where(eq(properties.modelId, typeof modelId === 'string' ? parseInt(modelId, 10) : modelId));
3388
+ return initialProperties.map((row) => ({
3389
+ schemaFileId: row.schemaFileId,
3390
+ }));
3391
+ },
3392
+ instanceState: modelInstanceState,
3393
+ loggerName: 'seedSdk:model:liveQuery',
3394
+ });
3395
+ }
3396
+ /**
3397
+ * Refresh property IDs from database (useful in Node.js where liveQuery isn't available)
3398
+ */
3399
+ async _refreshPropertiesFromDb() {
3400
+ const snapshot = this._service.getSnapshot();
3401
+ const dbId = snapshot.context._dbId; // _dbId is the database integer ID
3402
+ const modelName = snapshot.context.modelName;
3403
+ if (!dbId || !modelName) {
3404
+ return;
3405
+ }
3406
+ try {
3407
+ const db = BaseDb.getAppDb();
3408
+ if (!db) {
3409
+ return;
3410
+ }
3411
+ const propertyRows = await db
3412
+ .select({ schemaFileId: properties.schemaFileId })
3413
+ .from(properties)
3414
+ .where(eq(properties.modelId, dbId)); // Use _dbId (database integer ID)
3415
+ const propertyIds = propertyRows
3416
+ .map((row) => row.schemaFileId)
3417
+ .filter((id) => id !== null && id !== undefined);
3418
+ // CRITICAL: Create ModelProperty instances BEFORE updating context
3419
+ // This ensures they're in the cache when the properties getter is called
3420
+ if (propertyIds.length > 0) {
3421
+ try {
3422
+ await modelPropertyImportPromise;
3423
+ const ModelProperty = getModelProperty();
3424
+ if (!ModelProperty) {
3425
+ logger('[Model._refreshPropertiesFromDb] ModelProperty not yet loaded');
3426
+ }
3427
+ else {
3428
+ const createPromises = propertyIds.map(async (propertyFileId) => {
3429
+ try {
3430
+ const property = await ModelProperty.createById(propertyFileId);
3431
+ if (property) {
3432
+ logger(`[Model._refreshPropertiesFromDb] Created/cached ModelProperty instance for propertyFileId "${propertyFileId}"`);
3433
+ }
3434
+ else {
3435
+ logger(`[Model._refreshPropertiesFromDb] ModelProperty.createById returned undefined for propertyFileId "${propertyFileId}"`);
3436
+ }
3437
+ }
3438
+ catch (error) {
3439
+ logger(`[Model._refreshPropertiesFromDb] Error creating ModelProperty instance for propertyFileId "${propertyFileId}": ${error}`);
3440
+ }
3441
+ });
3442
+ await Promise.all(createPromises);
3443
+ }
3444
+ }
3445
+ catch (error) {
3446
+ logger(`[Model._refreshPropertiesFromDb] Error importing ModelProperty or creating instances: ${error}`);
3447
+ }
3448
+ }
3449
+ // Update context with refreshed property IDs AFTER creating instances
3450
+ this._service.send({
3451
+ type: 'updateContext',
3452
+ _liveQueryPropertyIds: propertyIds,
3453
+ _propertiesUpdated: Date.now(),
3454
+ });
3455
+ }
3456
+ catch (error) {
3457
+ logger(`[Model._refreshPropertiesFromDb] Error refreshing properties: ${error}`);
3458
+ }
3459
+ }
3460
+ /**
3461
+ * Public method to refresh properties from database
3462
+ */
3463
+ async refreshProperties() {
3464
+ await this._refreshPropertiesFromDb();
3465
+ }
3466
+ }
3467
+ // Primary cache: ID-based (never changes on rename)
3468
+ Model.instanceCacheById = new Map();
3469
+ // Secondary index: name lookup (updated on rename)
3470
+ Model.instanceCacheByName = new Map();
3471
+ // Legacy cache: kept temporarily for backward compatibility during migration
3472
+ Model.instanceCache = new Map();
3473
+ // Static tracking for pending writes
3474
+ Model.pendingWrites = new Map();
3475
+ // Track which models are currently being saved to prevent concurrent saves
3476
+ Model.savingModels = new Set();
3477
+ // Cache client initialization state globally to avoid repeated checks
3478
+ Model.cachedClientInitialized = null;
3479
+ Model.clientCheckTime = 0;
3480
+ Model.CLIENT_CHECK_CACHE_MS = 50; // Cache for 50ms to avoid excessive checks
3481
+
3482
+ var Model$1 = /*#__PURE__*/Object.freeze({
3483
+ __proto__: null,
3484
+ Model: Model
3485
+ });
3486
+
3487
+ // Export the Model class
3488
+ const TModelValues = Type.Record(Type.String(), Type.Any());
3489
+ const TModelSchema = Type.Record(Type.String(), TProperty);
3490
+
3491
+ export { ConflictError as C, Model as M, TModelSchema as T, TModelValues as a, Model$1 as b, setupEntityLiveQuery as s, writeProcessMachine as w };
3492
+ //# sourceMappingURL=index-BFpbtEsg.js.map