@seedprotocol/sdk 0.7.9-next.1177.ga3e08ed → 0.7.9-next.1306.g4ff6582
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BaseFileManager-BAlySv8w.js +650 -0
- package/dist/BaseFileManager-BAlySv8w.js.map +1 -0
- package/dist/{Item-0oDoTPhi.js → Item-Bf4kWKpH.js} +5647 -6573
- package/dist/Item-Bf4kWKpH.js.map +1 -0
- package/dist/{ModelProperty-i5flwzQG.js → ModelProperty-BlaEgb_U.js} +345 -709
- package/dist/ModelProperty-BlaEgb_U.js.map +1 -0
- package/dist/Schema-J_1f6p0c.js +1456 -0
- package/dist/Schema-J_1f6p0c.js.map +1 -0
- package/dist/{SchemaValidationService-DnGKel4V.js → SchemaValidationService-CnmKyzR-.js} +6 -9
- package/dist/{SchemaValidationService-DnGKel4V.js.map → SchemaValidationService-CnmKyzR-.js.map} +1 -1
- package/dist/{configurePlatform-CrMafpMW.js → configurePlatform-CiOJ1R3q.js} +3 -12
- package/dist/configurePlatform-CiOJ1R3q.js.map +1 -0
- package/dist/db/drizzle/0025_add_metadata_retract.sql +1 -0
- package/dist/db/drizzle/0026_add_content_hash_to_schemas.sql +1 -0
- package/dist/db/drizzle/0027_link_metadata_property_ids.sql +74 -0
- package/dist/db/drizzle/0028_add_definition_to_properties.sql +17 -0
- package/dist/db/drizzle/0029_dedupe_chain_uids_add_unique_indexes.sql +120 -0
- package/dist/db/drizzle/meta/0025_snapshot.json +1652 -0
- package/dist/db/drizzle/meta/0026_snapshot.json +1659 -0
- package/dist/db/drizzle/meta/0027_snapshot.json +1659 -0
- package/dist/db/drizzle/meta/0028_snapshot.json +1666 -0
- package/dist/db/drizzle/meta/0029_snapshot.json +1691 -0
- package/dist/db/drizzle/meta/_journal.json +36 -1
- package/dist/{detachRowsBeforeDelete-DXFYSHRR.js → detachRowsBeforeDelete-sEaUwlOU.js} +2 -13
- package/dist/{detachRowsBeforeDelete-DXFYSHRR.js.map → detachRowsBeforeDelete-sEaUwlOU.js.map} +1 -1
- package/dist/dist/{index-Dx_GTdyQ.d.ts → BaseFileManager-BAlySv8w.d.ts} +1424 -2696
- package/dist/dist/BaseFileManager-BAlySv8w.d.ts.map +1 -0
- package/dist/dist/{configurePlatform-CrMafpMW.d.ts → configurePlatform-CiOJ1R3q.d.ts} +1 -1
- package/dist/dist/configurePlatform-CiOJ1R3q.d.ts.map +1 -0
- package/dist/dist/{index-PbnKw2es.d.ts → index-D0V-mjCV.d.ts} +56 -1
- package/dist/dist/{index-PbnKw2es.d.ts.map → index-D0V-mjCV.d.ts.map} +1 -1
- package/dist/dist/{index.node-CzMwMr_d.d.ts → index.node-CYPemH-t.d.ts} +225 -5
- package/dist/dist/{index.node-CzMwMr_d.d.ts.map → index.node-CYPemH-t.d.ts.map} +1 -1
- package/dist/dist/platform.node.d.ts +2 -2
- package/dist/eas-wB_fQ8Tc.js +32 -0
- package/dist/eas-wB_fQ8Tc.js.map +1 -0
- package/dist/errors-DLSoqSHG.js +6550 -0
- package/dist/errors-DLSoqSHG.js.map +1 -0
- package/dist/getPublishPayload-BnIwpwz1.js +760 -0
- package/dist/getPublishPayload-BnIwpwz1.js.map +1 -0
- package/dist/getPublishPendingDiff-D6_-A5ns.js +125 -0
- package/dist/getPublishPendingDiff-D6_-A5ns.js.map +1 -0
- package/dist/getPublishUploads-sS6JfPgz.js +783 -0
- package/dist/getPublishUploads-sS6JfPgz.js.map +1 -0
- package/dist/{index-DJmWkVIK.js → index-BVBmz_cc.js} +11 -14
- package/dist/index-BVBmz_cc.js.map +1 -0
- package/dist/index-CYGWPoOx.js +27 -0
- package/dist/index-CYGWPoOx.js.map +1 -0
- package/dist/{index-PbnKw2es.js → index-D0V-mjCV.js} +2 -2
- package/dist/index-D0V-mjCV.js.map +1 -0
- package/dist/index-DmTtj1nk.js +817 -0
- package/dist/index-DmTtj1nk.js.map +1 -0
- package/dist/{index.browser-i9uPJice.js → index.browser-CUmBVYen.js} +436 -379
- package/dist/index.browser-CUmBVYen.js.map +1 -0
- package/dist/{index.node-CzMwMr_d.js → index.node-CYPemH-t.js} +10 -10
- package/dist/index.node-CYPemH-t.js.map +1 -0
- package/dist/main.js +23 -1105
- package/dist/main.js.map +1 -1
- package/dist/modelPropertyRecordSchemas-C7UgA5TH.js +48 -0
- package/dist/modelPropertyRecordSchemas-C7UgA5TH.js.map +1 -0
- package/dist/node.js +14 -18
- package/dist/node.js.map +1 -1
- package/dist/platform.browser.js +8 -13
- package/dist/platform.browser.js.map +1 -1
- package/dist/platform.node.js +6 -16
- package/dist/platform.node.js.map +1 -1
- package/dist/property-BxUYznH0.js +87 -0
- package/dist/property-BxUYznH0.js.map +1 -0
- package/dist/publishGraph-EcB40Uii.js +311 -0
- package/dist/publishGraph-EcB40Uii.js.map +1 -0
- package/dist/readHtmlBodyForStorageSeed-D6KPtWoR.js +110 -0
- package/dist/readHtmlBodyForStorageSeed-D6KPtWoR.js.map +1 -0
- package/dist/seedSchema/MetadataSchema.ts +3 -0
- package/dist/seedSchema/ModelSchema.ts +4 -0
- package/dist/seedSchema/ModelSchemaSchema.ts +5 -2
- package/dist/seedSchema/PendingWriteSchema.ts +4 -1
- package/dist/seedSchema/SchemaSchema.ts +3 -0
- package/dist/seedSchema/SeedSchema.ts +8 -2
- package/dist/seedSchema/VersionSchema.ts +7 -2
- package/dist/seedSchema/attestedUid.ts +10 -0
- package/dist/src/Item/Item.d.ts +32 -30
- package/dist/src/Item/Item.d.ts.map +1 -1
- package/dist/src/Item/loadItem.d.ts +12 -0
- package/dist/src/Item/loadItem.d.ts.map +1 -0
- package/dist/src/Item/service/itemMachineSingle.d.ts +2 -13
- package/dist/src/Item/service/itemMachineSingle.d.ts.map +1 -1
- package/dist/src/ItemProperty/ItemProperty.d.ts +98 -41
- package/dist/src/ItemProperty/ItemProperty.d.ts.map +1 -1
- package/dist/src/ItemProperty/loadProperty.d.ts +11 -0
- package/dist/src/ItemProperty/loadProperty.d.ts.map +1 -0
- package/dist/src/ItemProperty/propertyWrite.d.ts +54 -0
- package/dist/src/ItemProperty/propertyWrite.d.ts.map +1 -0
- package/dist/src/ItemProperty/{service/actors/saveValueToDb/rowContext.d.ts → rowContext.d.ts} +1 -1
- package/dist/src/ItemProperty/rowContext.d.ts.map +1 -0
- package/dist/src/ItemProperty/service/actors/hydrateFromDb.d.ts +6 -0
- package/dist/src/ItemProperty/service/actors/hydrateFromDb.d.ts.map +1 -1
- package/dist/src/ItemProperty/service/actors/modelPropertyRecordSchemas.d.ts +5 -15
- package/dist/src/ItemProperty/service/actors/modelPropertyRecordSchemas.d.ts.map +1 -1
- package/dist/src/ItemProperty/service/propertyMachine.d.ts +3 -46
- package/dist/src/ItemProperty/service/propertyMachine.d.ts.map +1 -1
- package/dist/src/Model/Model.d.ts +26 -56
- package/dist/src/Model/Model.d.ts.map +1 -1
- package/dist/src/Model/createModelProperties.d.ts +10 -0
- package/dist/src/Model/createModelProperties.d.ts.map +1 -0
- package/dist/src/Model/loadModel.d.ts +13 -0
- package/dist/src/Model/loadModel.d.ts.map +1 -0
- package/dist/src/Model/service/actors/validateModel.d.ts.map +1 -1
- package/dist/src/Model/service/modelMachine.d.ts +5 -114
- package/dist/src/Model/service/modelMachine.d.ts.map +1 -1
- package/dist/src/ModelProperty/ModelProperty.d.ts +44 -37
- package/dist/src/ModelProperty/ModelProperty.d.ts.map +1 -1
- package/dist/src/ModelProperty/service/actors/compareAndMarkDraft.d.ts.map +1 -1
- package/dist/src/ModelProperty/service/actors/validateProperty.d.ts.map +1 -1
- package/dist/src/ModelProperty/service/modelPropertyMachine.d.ts +6 -77
- package/dist/src/ModelProperty/service/modelPropertyMachine.d.ts.map +1 -1
- package/dist/src/Schema/Schema.d.ts +36 -37
- package/dist/src/Schema/Schema.d.ts.map +1 -1
- package/dist/src/Schema/loadOrCreateSchema.d.ts +21 -0
- package/dist/src/Schema/loadOrCreateSchema.d.ts.map +1 -0
- package/dist/src/Schema/service/schemaMachine.d.ts +7 -125
- package/dist/src/Schema/service/schemaMachine.d.ts.map +1 -1
- package/dist/src/browser/db/Db.d.ts.map +1 -1
- package/dist/src/browser/db/changeFeed.d.ts +2 -18
- package/dist/src/browser/db/changeFeed.d.ts.map +1 -1
- package/dist/src/browser/db/drizzleFiles.d.ts +7 -2
- package/dist/src/browser/db/drizzleFiles.d.ts.map +1 -1
- package/dist/src/browser/workers/filesDownload.d.ts.map +1 -1
- package/dist/src/browser/workers/imageResize.d.ts.map +1 -1
- package/dist/src/client/ClientManager.d.ts +90 -57
- package/dist/src/client/ClientManager.d.ts.map +1 -1
- package/dist/src/client/actors/addModelsToDb.d.ts +9 -7
- package/dist/src/client/actors/addModelsToDb.d.ts.map +1 -1
- package/dist/src/client/actors/dbInit.d.ts +3 -3
- package/dist/src/client/actors/dbInit.d.ts.map +1 -1
- package/dist/src/client/actors/fileSystemInit.d.ts +3 -3
- package/dist/src/client/actors/fileSystemInit.d.ts.map +1 -1
- package/dist/src/client/actors/platformClassesInit.d.ts +10 -5
- package/dist/src/client/actors/platformClassesInit.d.ts.map +1 -1
- package/dist/src/client/actors/processSchemaFiles.d.ts +18 -3
- package/dist/src/client/actors/processSchemaFiles.d.ts.map +1 -1
- package/dist/src/client/actors/saveConfig.d.ts +6 -3
- package/dist/src/client/actors/saveConfig.d.ts.map +1 -1
- package/dist/src/client/clientManagerMachine.d.ts +22 -20
- package/dist/src/client/clientManagerMachine.d.ts.map +1 -1
- package/dist/src/client/constants.d.ts +1 -9
- package/dist/src/client/constants.d.ts.map +1 -1
- package/dist/src/client/readiness.d.ts +26 -0
- package/dist/src/client/readiness.d.ts.map +1 -0
- package/dist/src/db/read/currentPropertyRow.d.ts +61 -0
- package/dist/src/db/read/currentPropertyRow.d.ts.map +1 -0
- package/dist/src/db/read/findSeedRow.d.ts +20 -0
- package/dist/src/db/read/findSeedRow.d.ts.map +1 -0
- package/dist/src/db/read/findSeedRowsByUid.d.ts +4 -1
- package/dist/src/db/read/findSeedRowsByUid.d.ts.map +1 -1
- package/dist/src/db/read/getAttesterForSeed.d.ts +1 -1
- package/dist/src/db/read/getAttesterForSeed.d.ts.map +1 -1
- package/dist/src/db/read/getItemData.d.ts.map +1 -1
- package/dist/src/db/read/getItemProperties.d.ts +5 -0
- package/dist/src/db/read/getItemProperties.d.ts.map +1 -1
- package/dist/src/db/read/getMetadataAttestationUidsForSeedUid.d.ts.map +1 -1
- package/dist/src/db/read/getModelSchemas.d.ts +0 -7
- package/dist/src/db/read/getModelSchemas.d.ts.map +1 -1
- package/dist/src/db/read/getModelsData.d.ts.map +1 -1
- package/dist/src/db/read/getPropertyClear.d.ts +49 -0
- package/dist/src/db/read/getPropertyClear.d.ts.map +1 -0
- package/dist/src/db/read/getPublishPayload.d.ts +35 -40
- package/dist/src/db/read/getPublishPayload.d.ts.map +1 -1
- package/dist/src/db/read/getPublishPendingDiff.d.ts +7 -2
- package/dist/src/db/read/getPublishPendingDiff.d.ts.map +1 -1
- package/dist/src/db/read/getPublishUploads.d.ts +59 -28
- package/dist/src/db/read/getPublishUploads.d.ts.map +1 -1
- package/dist/src/db/read/getSchemaUidForModel.d.ts.map +1 -1
- package/dist/src/db/read/getSeedPublishState.d.ts.map +1 -1
- package/dist/src/db/read/getSeedPublisher.d.ts.map +1 -1
- package/dist/src/db/read/getVersionsForSeedUid.d.ts.map +1 -1
- package/dist/src/db/read/isSeedRevoked.d.ts.map +1 -1
- package/dist/src/db/read/localModelSchemaLookups.d.ts +8 -0
- package/dist/src/db/read/localModelSchemaLookups.d.ts.map +1 -0
- package/dist/src/db/read/metadataProperty.d.ts +104 -0
- package/dist/src/db/read/metadataProperty.d.ts.map +1 -0
- package/dist/src/db/read/publishGraph.d.ts +148 -0
- package/dist/src/db/read/publishGraph.d.ts.map +1 -0
- package/dist/src/db/read/resolveModelRecord.d.ts +26 -2
- package/dist/src/db/read/resolveModelRecord.d.ts.map +1 -1
- package/dist/src/db/read/resolveRelatedSeedRef.d.ts +1 -7
- package/dist/src/db/read/resolveRelatedSeedRef.d.ts.map +1 -1
- package/dist/src/db/read/subqueries/metadataLatest.d.ts +3 -1
- package/dist/src/db/read/subqueries/metadataLatest.d.ts.map +1 -1
- package/dist/src/db/read/summarizePublishWork.d.ts +18 -5
- package/dist/src/db/read/summarizePublishWork.d.ts.map +1 -1
- package/dist/src/db/write/applyPropertyAttestationUidsFromPublish.d.ts +20 -21
- package/dist/src/db/write/applyPropertyAttestationUidsFromPublish.d.ts.map +1 -1
- package/dist/src/db/write/createMetadata.d.ts.map +1 -1
- package/dist/src/db/write/createNewItem.d.ts.map +1 -1
- package/dist/src/db/write/createSeeds.d.ts +5 -1
- package/dist/src/db/write/createSeeds.d.ts.map +1 -1
- package/dist/src/db/write/definitions.d.ts +127 -0
- package/dist/src/db/write/definitions.d.ts.map +1 -0
- package/dist/src/db/write/markPropertyAttestationsRetracted.d.ts +11 -0
- package/dist/src/db/write/markPropertyAttestationsRetracted.d.ts.map +1 -0
- package/dist/src/db/write/mergeChainRows.d.ts +22 -0
- package/dist/src/db/write/mergeChainRows.d.ts.map +1 -0
- package/dist/src/db/write/saveDefinition.d.ts +29 -0
- package/dist/src/db/write/saveDefinition.d.ts.map +1 -0
- package/dist/src/db/write/updateItemPropertyValue.d.ts +2 -0
- package/dist/src/db/write/updateItemPropertyValue.d.ts.map +1 -1
- package/dist/src/db/write/updateSeedUid.d.ts +4 -2
- package/dist/src/db/write/updateSeedUid.d.ts.map +1 -1
- package/dist/src/db/write/updateVersionUid.d.ts +12 -4
- package/dist/src/db/write/updateVersionUid.d.ts.map +1 -1
- package/dist/src/db/write/writeModelUids.d.ts +10 -0
- package/dist/src/db/write/writeModelUids.d.ts.map +1 -0
- package/dist/src/eas.d.ts +8 -6
- package/dist/src/eas.d.ts.map +1 -1
- package/dist/src/events/files/download.d.ts.map +1 -1
- package/dist/src/events/item/syncDbWithEas.d.ts.map +1 -1
- package/dist/src/helpers/arweaveContentTags.d.ts +1 -1
- package/dist/src/helpers/arweaveContentTags.d.ts.map +1 -1
- package/dist/src/helpers/compareMetadataRowsLatestFirst.d.ts +27 -12
- package/dist/src/helpers/compareMetadataRowsLatestFirst.d.ts.map +1 -1
- package/dist/src/helpers/constants.d.ts +5 -2
- package/dist/src/helpers/constants.d.ts.map +1 -1
- package/dist/src/helpers/dataUrl.d.ts +10 -0
- package/dist/src/helpers/dataUrl.d.ts.map +1 -0
- package/dist/src/helpers/db.d.ts +54 -105
- package/dist/src/helpers/db.d.ts.map +1 -1
- package/dist/src/helpers/ensureImageLocal.d.ts.map +1 -1
- package/dist/src/helpers/entity/contextUpdate.d.ts +20 -0
- package/dist/src/helpers/entity/contextUpdate.d.ts.map +1 -0
- package/dist/src/helpers/entity/draftSaveErrors.d.ts +5 -0
- package/dist/src/helpers/entity/draftSaveErrors.d.ts.map +1 -1
- package/dist/src/helpers/entity/entityCache.d.ts +65 -3
- package/dist/src/helpers/entity/entityCache.d.ts.map +1 -1
- package/dist/src/helpers/entity/entityCommon.d.ts +0 -22
- package/dist/src/helpers/entity/entityCommon.d.ts.map +1 -1
- package/dist/src/helpers/entity/entityLifecycle.d.ts +75 -0
- package/dist/src/helpers/entity/entityLifecycle.d.ts.map +1 -0
- package/dist/src/helpers/entity/entityLiveQuery.d.ts +1 -23
- package/dist/src/helpers/entity/entityLiveQuery.d.ts.map +1 -1
- package/dist/src/helpers/htmlEmbeddedDataUriPublish.d.ts +4 -9
- package/dist/src/helpers/htmlEmbeddedDataUriPublish.d.ts.map +1 -1
- package/dist/src/helpers/index.d.ts +2 -24
- package/dist/src/helpers/index.d.ts.map +1 -1
- package/dist/src/helpers/localImageBlobUrl.d.ts +7 -0
- package/dist/src/helpers/localImageBlobUrl.d.ts.map +1 -0
- package/dist/src/helpers/mediaRef.d.ts +3 -70
- package/dist/src/helpers/mediaRef.d.ts.map +1 -1
- package/dist/src/helpers/metadataPropertyNames.d.ts +34 -21
- package/dist/src/helpers/metadataPropertyNames.d.ts.map +1 -1
- package/dist/src/helpers/mimeTypes.d.ts +2 -1
- package/dist/src/helpers/mimeTypes.d.ts.map +1 -1
- package/dist/src/helpers/model.d.ts.map +1 -1
- package/dist/src/helpers/ownership.d.ts.map +1 -1
- package/dist/src/helpers/property.d.ts +21 -26
- package/dist/src/helpers/property.d.ts.map +1 -1
- package/dist/src/helpers/propertyDefinition.d.ts +59 -0
- package/dist/src/helpers/propertyDefinition.d.ts.map +1 -0
- package/dist/src/helpers/readHtmlBodyForStorageSeed.d.ts.map +1 -1
- package/dist/src/helpers/relationSeedRef.d.ts +0 -7
- package/dist/src/helpers/relationSeedRef.d.ts.map +1 -1
- package/dist/src/helpers/schema.d.ts +24 -38
- package/dist/src/helpers/schema.d.ts.map +1 -1
- package/dist/src/helpers/schemaApply.d.ts +135 -0
- package/dist/src/helpers/schemaApply.d.ts.map +1 -0
- package/dist/src/helpers/tabEvents.d.ts.map +1 -1
- package/dist/src/helpers/updateSchema.d.ts +2 -18
- package/dist/src/helpers/updateSchema.d.ts.map +1 -1
- package/dist/src/imports/index.d.ts +1 -1
- package/dist/src/imports/index.d.ts.map +1 -1
- package/dist/src/imports/json.d.ts +8 -78
- package/dist/src/imports/json.d.ts.map +1 -1
- package/dist/src/index.d.ts +16 -14
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/interfaces/IItem.d.ts +5 -0
- package/dist/src/interfaces/IItem.d.ts.map +1 -1
- package/dist/src/interfaces/IItemProperty.d.ts +13 -7
- package/dist/src/interfaces/IItemProperty.d.ts.map +1 -1
- package/dist/src/node/db/Db.d.ts.map +1 -1
- package/dist/src/node/index.d.ts +3 -2
- package/dist/src/node/index.d.ts.map +1 -1
- package/dist/src/outbox/conflicts.d.ts +2 -0
- package/dist/src/outbox/conflicts.d.ts.map +1 -1
- package/dist/src/outbox/itemAppliers.d.ts +70 -14
- package/dist/src/outbox/itemAppliers.d.ts.map +1 -1
- package/dist/src/outbox/outbox.d.ts +14 -3
- package/dist/src/outbox/outbox.d.ts.map +1 -1
- package/dist/src/outbox/pendingValues.d.ts +3 -1
- package/dist/src/outbox/pendingValues.d.ts.map +1 -1
- package/dist/src/outbox/storageValueRow.d.ts +7 -0
- package/dist/src/outbox/storageValueRow.d.ts.map +1 -0
- package/dist/src/platform/configurePlatform.d.ts.map +1 -1
- package/dist/src/platform/index.browser.d.ts.map +1 -1
- package/dist/src/platform/index.node.d.ts.map +1 -1
- package/dist/src/seedSchema/MetadataSchema.d.ts +17 -0
- package/dist/src/seedSchema/MetadataSchema.d.ts.map +1 -1
- package/dist/src/seedSchema/ModelSchema.d.ts +19 -0
- package/dist/src/seedSchema/ModelSchema.d.ts.map +1 -1
- package/dist/src/seedSchema/ModelSchemaSchema.d.ts.map +1 -1
- package/dist/src/seedSchema/PendingWriteSchema.d.ts.map +1 -1
- package/dist/src/seedSchema/SchemaSchema.d.ts +19 -0
- package/dist/src/seedSchema/SchemaSchema.d.ts.map +1 -1
- package/dist/src/seedSchema/SeedSchema.d.ts.map +1 -1
- package/dist/src/seedSchema/VersionSchema.d.ts.map +1 -1
- package/dist/src/seedSchema/attestedUid.d.ts +9 -0
- package/dist/src/seedSchema/attestedUid.d.ts.map +1 -0
- package/dist/src/storageValue/content.d.ts +45 -0
- package/dist/src/storageValue/content.d.ts.map +1 -0
- package/dist/src/storageValue/findFile.d.ts +7 -0
- package/dist/src/storageValue/findFile.d.ts.map +1 -0
- package/dist/src/storageValue/resolve.d.ts +71 -0
- package/dist/src/storageValue/resolve.d.ts.map +1 -0
- package/dist/src/storageValue/save.d.ts +24 -0
- package/dist/src/storageValue/save.d.ts.map +1 -0
- package/dist/src/storageValue/stage.d.ts +22 -0
- package/dist/src/storageValue/stage.d.ts.map +1 -0
- package/dist/src/types/db.d.ts +0 -14
- package/dist/src/types/db.d.ts.map +1 -1
- package/dist/src/types/index.d.ts +3 -3
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/src/types/machines.d.ts +1 -1
- package/dist/src/types/machines.d.ts.map +1 -1
- package/dist/src/types/property.d.ts +9 -16
- package/dist/src/types/property.d.ts.map +1 -1
- package/dist/src/types/publish.d.ts +1 -1
- package/dist/src/types/publish.d.ts.map +1 -1
- package/dist/{syncDbWithEas-DUu_wjUG.js → syncDbWithEas-CA7PvBgu.js} +142 -168
- package/dist/syncDbWithEas-CA7PvBgu.js.map +1 -0
- package/dist/tabEvents-CaVmngqr.js +3804 -0
- package/dist/tabEvents-CaVmngqr.js.map +1 -0
- package/dist/{tolerantWebAccess-DecKDGen.js → tolerantWebAccess-BJqViq08.js} +10 -15
- package/dist/{tolerantWebAccess-DecKDGen.js.map → tolerantWebAccess-BJqViq08.js.map} +1 -1
- package/dist/{resolveModelForSyncedSeed-4gO9Hfn2.js → updateSeedRevokedAt-BL9Bg-G7.js} +30 -4
- package/dist/updateSeedRevokedAt-BL9Bg-G7.js.map +1 -0
- package/package.json +6 -6
- package/dist/ClientManager-GZfXErDX.js +0 -5279
- package/dist/ClientManager-GZfXErDX.js.map +0 -1
- package/dist/Item-0oDoTPhi.js.map +0 -1
- package/dist/ModelProperty-i5flwzQG.js.map +0 -1
- package/dist/Schema-PFZwG9Nl.js +0 -4370
- package/dist/Schema-PFZwG9Nl.js.map +0 -1
- package/dist/backfillMetadataPropertyIds-BbxgavEQ.js +0 -72
- package/dist/backfillMetadataPropertyIds-BbxgavEQ.js.map +0 -1
- package/dist/changeFeed-DKdVlUmt.js +0 -726
- package/dist/changeFeed-DKdVlUmt.js.map +0 -1
- package/dist/configurePlatform-CrMafpMW.js.map +0 -1
- package/dist/dist/ClientManager-GZfXErDX.d.ts +0 -952
- package/dist/dist/ClientManager-GZfXErDX.d.ts.map +0 -1
- package/dist/dist/Item-0oDoTPhi.d.ts +0 -1076
- package/dist/dist/Item-0oDoTPhi.d.ts.map +0 -1
- package/dist/dist/ModelProperty-i5flwzQG.d.ts +0 -315
- package/dist/dist/ModelProperty-i5flwzQG.d.ts.map +0 -1
- package/dist/dist/Schema-PFZwG9Nl.d.ts +0 -432
- package/dist/dist/Schema-PFZwG9Nl.d.ts.map +0 -1
- package/dist/dist/SchemaValidationService-DnGKel4V.d.ts +0 -118
- package/dist/dist/SchemaValidationService-DnGKel4V.d.ts.map +0 -1
- package/dist/dist/backfillMetadataPropertyIds-BbxgavEQ.d.ts +0 -7
- package/dist/dist/backfillMetadataPropertyIds-BbxgavEQ.d.ts.map +0 -1
- package/dist/dist/changeFeed-DKdVlUmt.d.ts +0 -160
- package/dist/dist/changeFeed-DKdVlUmt.d.ts.map +0 -1
- package/dist/dist/configurePlatform-CrMafpMW.d.ts.map +0 -1
- package/dist/dist/detachRowsBeforeDelete-DXFYSHRR.d.ts +0 -13
- package/dist/dist/detachRowsBeforeDelete-DXFYSHRR.d.ts.map +0 -1
- package/dist/dist/eas-dyepi5jz.d.ts +0 -12
- package/dist/dist/eas-dyepi5jz.d.ts.map +0 -1
- package/dist/dist/errors-C1qUEZ7A.d.ts +0 -25
- package/dist/dist/errors-C1qUEZ7A.d.ts.map +0 -1
- package/dist/dist/errors-D1zHAqjr.d.ts +0 -515
- package/dist/dist/errors-D1zHAqjr.d.ts.map +0 -1
- package/dist/dist/eventBus-7vxTCc3a.d.ts +0 -18
- package/dist/dist/eventBus-7vxTCc3a.d.ts.map +0 -1
- package/dist/dist/findSeedRowsByUid-CT3sOXXw.d.ts +0 -13
- package/dist/dist/findSeedRowsByUid-CT3sOXXw.d.ts.map +0 -1
- package/dist/dist/getItem-Bhjecbia.d.ts +0 -6
- package/dist/dist/getItem-Bhjecbia.d.ts.map +0 -1
- package/dist/dist/getPublishPayload-DH4rMqXG.d.ts +0 -47
- package/dist/dist/getPublishPayload-DH4rMqXG.d.ts.map +0 -1
- package/dist/dist/getPublishUploads-BzUIDA2G.d.ts +0 -89
- package/dist/dist/getPublishUploads-BzUIDA2G.d.ts.map +0 -1
- package/dist/dist/index-CkYb3wcU.d.ts +0 -14
- package/dist/dist/index-CkYb3wcU.d.ts.map +0 -1
- package/dist/dist/index-DJmWkVIK.d.ts +0 -6
- package/dist/dist/index-DJmWkVIK.d.ts.map +0 -1
- package/dist/dist/index-Dx_GTdyQ.d.ts.map +0 -1
- package/dist/dist/index.browser-i9uPJice.d.ts +0 -9908
- package/dist/dist/index.browser-i9uPJice.d.ts.map +0 -1
- package/dist/dist/mimeTypes-C9r76DCH.d.ts +0 -30
- package/dist/dist/mimeTypes-C9r76DCH.d.ts.map +0 -1
- package/dist/dist/model-oHA3YhOd.d.ts +0 -6
- package/dist/dist/model-oHA3YhOd.d.ts.map +0 -1
- package/dist/dist/modelPropertyRecordSchemas-C5lW9aA-.d.ts +0 -7
- package/dist/dist/modelPropertyRecordSchemas-C5lW9aA-.d.ts.map +0 -1
- package/dist/dist/platform.browser.d.ts +0 -3
- package/dist/dist/platform.browser.d.ts.map +0 -1
- package/dist/dist/property-BwYtg5Q_.d.ts +0 -37
- package/dist/dist/property-BwYtg5Q_.d.ts.map +0 -1
- package/dist/dist/propertyValidationRules-CyHdpcPP.d.ts +0 -13
- package/dist/dist/propertyValidationRules-CyHdpcPP.d.ts.map +0 -1
- package/dist/dist/publishDraftGraph-C5F6b7GK.d.ts +0 -93
- package/dist/dist/publishDraftGraph-C5F6b7GK.d.ts.map +0 -1
- package/dist/dist/resolveModelForSyncedSeed-4gO9Hfn2.d.ts +0 -21
- package/dist/dist/resolveModelForSyncedSeed-4gO9Hfn2.d.ts.map +0 -1
- package/dist/dist/syncDbWithEas-DUu_wjUG.d.ts +0 -13
- package/dist/dist/syncDbWithEas-DUu_wjUG.d.ts.map +0 -1
- package/dist/dist/tolerantWebAccess-DecKDGen.d.ts +0 -113
- package/dist/dist/tolerantWebAccess-DecKDGen.d.ts.map +0 -1
- package/dist/dist/updateSeedRevokedAt-CP606vKs.d.ts +0 -11
- package/dist/dist/updateSeedRevokedAt-CP606vKs.d.ts.map +0 -1
- package/dist/eas-dyepi5jz.js +0 -67
- package/dist/eas-dyepi5jz.js.map +0 -1
- package/dist/errors-D1zHAqjr.js +0 -4006
- package/dist/errors-D1zHAqjr.js.map +0 -1
- package/dist/eventBus-7vxTCc3a.js +0 -64
- package/dist/eventBus-7vxTCc3a.js.map +0 -1
- package/dist/findSeedRowsByUid-CT3sOXXw.js +0 -47
- package/dist/findSeedRowsByUid-CT3sOXXw.js.map +0 -1
- package/dist/getItem-Bhjecbia.js +0 -45
- package/dist/getItem-Bhjecbia.js.map +0 -1
- package/dist/getPublishPayload-DH4rMqXG.js +0 -1634
- package/dist/getPublishPayload-DH4rMqXG.js.map +0 -1
- package/dist/getPublishUploads-BzUIDA2G.js +0 -1080
- package/dist/getPublishUploads-BzUIDA2G.js.map +0 -1
- package/dist/index-CkYb3wcU.js +0 -29
- package/dist/index-CkYb3wcU.js.map +0 -1
- package/dist/index-DJmWkVIK.js.map +0 -1
- package/dist/index-Dx_GTdyQ.js +0 -4745
- package/dist/index-Dx_GTdyQ.js.map +0 -1
- package/dist/index-PbnKw2es.js.map +0 -1
- package/dist/index.browser-i9uPJice.js.map +0 -1
- package/dist/index.node-CzMwMr_d.js.map +0 -1
- package/dist/mimeTypes-C9r76DCH.js +0 -209
- package/dist/mimeTypes-C9r76DCH.js.map +0 -1
- package/dist/model-oHA3YhOd.js +0 -29
- package/dist/model-oHA3YhOd.js.map +0 -1
- package/dist/modelPropertyRecordSchemas-C5lW9aA-.js +0 -94
- package/dist/modelPropertyRecordSchemas-C5lW9aA-.js.map +0 -1
- package/dist/property-BwYtg5Q_.js +0 -289
- package/dist/property-BwYtg5Q_.js.map +0 -1
- package/dist/propertyValidationRules-CyHdpcPP.js +0 -73
- package/dist/propertyValidationRules-CyHdpcPP.js.map +0 -1
- package/dist/publishDraftGraph-C5F6b7GK.js +0 -505
- package/dist/publishDraftGraph-C5F6b7GK.js.map +0 -1
- package/dist/resolveModelForSyncedSeed-4gO9Hfn2.js.map +0 -1
- package/dist/src/Item/service/actors/loadOrCreateItem.d.ts +0 -5
- package/dist/src/Item/service/actors/loadOrCreateItem.d.ts.map +0 -1
- package/dist/src/Item/service/actors/waitForDb.d.ts +0 -4
- package/dist/src/Item/service/actors/waitForDb.d.ts.map +0 -1
- package/dist/src/ItemProperty/service/actors/loadOrCreateProperty.d.ts +0 -5
- package/dist/src/ItemProperty/service/actors/loadOrCreateProperty.d.ts.map +0 -1
- package/dist/src/ItemProperty/service/actors/saveValueToDb/analyzeInput.d.ts +0 -5
- package/dist/src/ItemProperty/service/actors/saveValueToDb/analyzeInput.d.ts.map +0 -1
- package/dist/src/ItemProperty/service/actors/saveValueToDb/index.d.ts +0 -6
- package/dist/src/ItemProperty/service/actors/saveValueToDb/index.d.ts.map +0 -1
- package/dist/src/ItemProperty/service/actors/saveValueToDb/rowContext.d.ts.map +0 -1
- package/dist/src/ItemProperty/service/actors/saveValueToDb/saveFile.d.ts +0 -24
- package/dist/src/ItemProperty/service/actors/saveValueToDb/saveFile.d.ts.map +0 -1
- package/dist/src/ItemProperty/service/actors/saveValueToDb/saveHtml.d.ts +0 -5
- package/dist/src/ItemProperty/service/actors/saveValueToDb/saveHtml.d.ts.map +0 -1
- package/dist/src/ItemProperty/service/actors/saveValueToDb/saveImage.d.ts +0 -5
- package/dist/src/ItemProperty/service/actors/saveValueToDb/saveImage.d.ts.map +0 -1
- package/dist/src/ItemProperty/service/actors/saveValueToDb/saveItemStorage.d.ts +0 -10
- package/dist/src/ItemProperty/service/actors/saveValueToDb/saveItemStorage.d.ts.map +0 -1
- package/dist/src/ItemProperty/service/actors/saveValueToDb/storageValue.d.ts +0 -22
- package/dist/src/ItemProperty/service/actors/saveValueToDb/storageValue.d.ts.map +0 -1
- package/dist/src/ItemProperty/service/actors/waitForDb.d.ts +0 -4
- package/dist/src/ItemProperty/service/actors/waitForDb.d.ts.map +0 -1
- package/dist/src/Model/service/actors/createModelProperties.d.ts +0 -9
- package/dist/src/Model/service/actors/createModelProperties.d.ts.map +0 -1
- package/dist/src/Model/service/actors/loadOrCreateModel.d.ts +0 -5
- package/dist/src/Model/service/actors/loadOrCreateModel.d.ts.map +0 -1
- package/dist/src/Schema/service/actors/checkExistingSchema.d.ts +0 -5
- package/dist/src/Schema/service/actors/checkExistingSchema.d.ts.map +0 -1
- package/dist/src/Schema/service/actors/createModelInstances.d.ts +0 -11
- package/dist/src/Schema/service/actors/createModelInstances.d.ts.map +0 -1
- package/dist/src/Schema/service/actors/createPropertyInstances.d.ts +0 -12
- package/dist/src/Schema/service/actors/createPropertyInstances.d.ts.map +0 -1
- package/dist/src/Schema/service/actors/validateSchema.d.ts +0 -5
- package/dist/src/Schema/service/actors/validateSchema.d.ts.map +0 -1
- package/dist/src/Schema/service/actors/verifyModelInstancesInCache.d.ts +0 -6
- package/dist/src/Schema/service/actors/verifyModelInstancesInCache.d.ts.map +0 -1
- package/dist/src/Schema/service/actors/verifyModelsInDb.d.ts +0 -7
- package/dist/src/Schema/service/actors/verifyModelsInDb.d.ts.map +0 -1
- package/dist/src/Schema/service/actors/verifyPropertiesInDb.d.ts +0 -8
- package/dist/src/Schema/service/actors/verifyPropertiesInDb.d.ts.map +0 -1
- package/dist/src/Schema/service/actors/verifyPropertyInstancesInCache.d.ts +0 -6
- package/dist/src/Schema/service/actors/verifyPropertyInstancesInCache.d.ts.map +0 -1
- package/dist/src/Schema/service/actors/verifySchemaInDb.d.ts +0 -7
- package/dist/src/Schema/service/actors/verifySchemaInDb.d.ts.map +0 -1
- package/dist/src/Schema/service/actors/writeModelsToDb.d.ts +0 -9
- package/dist/src/Schema/service/actors/writeModelsToDb.d.ts.map +0 -1
- package/dist/src/Schema/service/actors/writePropertiesToDb.d.ts +0 -10
- package/dist/src/Schema/service/actors/writePropertiesToDb.d.ts.map +0 -1
- package/dist/src/Schema/service/actors/writeSchemaToDb.d.ts +0 -11
- package/dist/src/Schema/service/actors/writeSchemaToDb.d.ts.map +0 -1
- package/dist/src/browser/helpers/ArweaveClient.d.ts +0 -14
- package/dist/src/browser/helpers/ArweaveClient.d.ts.map +0 -1
- package/dist/src/browser/helpers/EasClient.d.ts +0 -10
- package/dist/src/browser/helpers/EasClient.d.ts.map +0 -1
- package/dist/src/browser/helpers/QueryClient.d.ts +0 -9
- package/dist/src/browser/helpers/QueryClient.d.ts.map +0 -1
- package/dist/src/client/actors/addModelsToStore.d.ts +0 -4
- package/dist/src/client/actors/addModelsToStore.d.ts.map +0 -1
- package/dist/src/db/backfillMetadataPropertyIds.d.ts +0 -7
- package/dist/src/db/backfillMetadataPropertyIds.d.ts.map +0 -1
- package/dist/src/db/read/getItem.d.ts +0 -3
- package/dist/src/db/read/getItem.d.ts.map +0 -1
- package/dist/src/db/read/getPropertyData.d.ts +0 -4
- package/dist/src/db/read/getPropertyData.d.ts.map +0 -1
- package/dist/src/db/read/getRelatedItemsForPublish.d.ts +0 -10
- package/dist/src/db/read/getRelatedItemsForPublish.d.ts.map +0 -1
- package/dist/src/db/read/getSeedData.d.ts +0 -12
- package/dist/src/db/read/getSeedData.d.ts.map +0 -1
- package/dist/src/db/read/itemLoads.d.ts +0 -20
- package/dist/src/db/read/itemLoads.d.ts.map +0 -1
- package/dist/src/db/read/publishDraftGraph.d.ts +0 -64
- package/dist/src/db/read/publishDraftGraph.d.ts.map +0 -1
- package/dist/src/db/write/linkModelToSchema.d.ts +0 -12
- package/dist/src/db/write/linkModelToSchema.d.ts.map +0 -1
- package/dist/src/helpers/ArweaveClient/BaseArweaveClient.d.ts +0 -2
- package/dist/src/helpers/ArweaveClient/BaseArweaveClient.d.ts.map +0 -1
- package/dist/src/helpers/ArweaveClient/arweaveL1UploadApi.d.ts +0 -2
- package/dist/src/helpers/ArweaveClient/arweaveL1UploadApi.d.ts.map +0 -1
- package/dist/src/helpers/ArweaveClient/selectReadGateway.d.ts +0 -2
- package/dist/src/helpers/ArweaveClient/selectReadGateway.d.ts.map +0 -1
- package/dist/src/helpers/ArweaveClient/uploadApiVerification.d.ts +0 -2
- package/dist/src/helpers/ArweaveClient/uploadApiVerification.d.ts.map +0 -1
- package/dist/src/helpers/EasClient/BaseEasClient.d.ts +0 -2
- package/dist/src/helpers/EasClient/BaseEasClient.d.ts.map +0 -1
- package/dist/src/helpers/QueryClient/BaseQueryClient.d.ts +0 -2
- package/dist/src/helpers/QueryClient/BaseQueryClient.d.ts.map +0 -1
- package/dist/src/helpers/easUid.d.ts +0 -2
- package/dist/src/helpers/easUid.d.ts.map +0 -1
- package/dist/src/helpers/entity/entityDestroy.d.ts +0 -49
- package/dist/src/helpers/entity/entityDestroy.d.ts.map +0 -1
- package/dist/src/helpers/entity/entityFind.d.ts +0 -52
- package/dist/src/helpers/entity/entityFind.d.ts.map +0 -1
- package/dist/src/helpers/entity/entityState.d.ts +0 -28
- package/dist/src/helpers/entity/entityState.d.ts.map +0 -1
- package/dist/src/helpers/entity/entityUnload.d.ts +0 -56
- package/dist/src/helpers/entity/entityUnload.d.ts.map +0 -1
- package/dist/src/helpers/entity/evictionEpoch.d.ts +0 -43
- package/dist/src/helpers/entity/evictionEpoch.d.ts.map +0 -1
- package/dist/src/helpers/entity/index.d.ts +0 -12
- package/dist/src/helpers/entity/index.d.ts.map +0 -1
- package/dist/src/helpers/gateway/gatewayState.d.ts +0 -2
- package/dist/src/helpers/gateway/gatewayState.d.ts.map +0 -1
- package/dist/src/helpers/gateway/resolveSeedGatewayEndpoints.d.ts +0 -2
- package/dist/src/helpers/gateway/resolveSeedGatewayEndpoints.d.ts.map +0 -1
- package/dist/src/helpers/gateway/seedGatewayConfig.d.ts +0 -2
- package/dist/src/helpers/gateway/seedGatewayConfig.d.ts.map +0 -1
- package/dist/src/helpers/getSchemaForItemProperty.d.ts +0 -15
- package/dist/src/helpers/getSchemaForItemProperty.d.ts.map +0 -1
- package/dist/src/helpers/getSegmentedItemProperties.d.ts +0 -10
- package/dist/src/helpers/getSegmentedItemProperties.d.ts.map +0 -1
- package/dist/src/helpers/isSqliteUniqueConstraintError.d.ts +0 -7
- package/dist/src/helpers/isSqliteUniqueConstraintError.d.ts.map +0 -1
- package/dist/src/helpers/propertyValidationRules.d.ts +0 -20
- package/dist/src/helpers/propertyValidationRules.d.ts.map +0 -1
- package/dist/src/helpers/propertyWriteTickets.d.ts +0 -11
- package/dist/src/helpers/propertyWriteTickets.d.ts.map +0 -1
- package/dist/src/helpers/reconcilePropertyDraftRows.d.ts +0 -10
- package/dist/src/helpers/reconcilePropertyDraftRows.d.ts.map +0 -1
- package/dist/src/helpers/waitForEntityIdle.d.ts +0 -21
- package/dist/src/helpers/waitForEntityIdle.d.ts.map +0 -1
- package/dist/src/interfaces/IQueryClient.d.ts +0 -15
- package/dist/src/interfaces/IQueryClient.d.ts.map +0 -1
- package/dist/src/node/helpers/ArweaveClient.d.ts +0 -5
- package/dist/src/node/helpers/ArweaveClient.d.ts.map +0 -1
- package/dist/src/node/helpers/EasClient.d.ts +0 -5
- package/dist/src/node/helpers/EasClient.d.ts.map +0 -1
- package/dist/src/node/helpers/QueryClient.d.ts +0 -5
- package/dist/src/node/helpers/QueryClient.d.ts.map +0 -1
- package/dist/src/services/write/actors/validateEntity.d.ts +0 -6
- package/dist/src/services/write/actors/validateEntity.d.ts.map +0 -1
- package/dist/src/services/write/actors/writeToDatabase.d.ts +0 -14
- package/dist/src/services/write/actors/writeToDatabase.d.ts.map +0 -1
- package/dist/src/services/write/writeProcessMachine.d.ts +0 -56
- package/dist/src/services/write/writeProcessMachine.d.ts.map +0 -1
- package/dist/src/stores/eas.d.ts +0 -2
- package/dist/src/stores/eas.d.ts.map +0 -1
- package/dist/src/types/arweave.d.ts +0 -57
- package/dist/src/types/arweave.d.ts.map +0 -1
- package/dist/src/types/gateway.d.ts +0 -2
- package/dist/src/types/gateway.d.ts.map +0 -1
- package/dist/syncDbWithEas-DUu_wjUG.js.map +0 -1
- package/dist/updateSeedRevokedAt-CP606vKs.js +0 -31
- package/dist/updateSeedRevokedAt-CP606vKs.js.map +0 -1
package/dist/errors-D1zHAqjr.js
DELETED
|
@@ -1,4006 +0,0 @@
|
|
|
1
|
-
import { fromCallback, fromPromise, setup, assign, createActor } from 'xstate';
|
|
2
|
-
import { L as BaseDb, p as models, E as properties, ad as generateId, aF as isInternalSchema, aG as writeModelToDb, aH as writePropertyToDb, z as modelSchemas, J as schemas, A as AmbiguousModelError, aI as renameModelInDb, aJ as currentNameMatchLevels, a as TProperty } from './index-Dx_GTdyQ.js';
|
|
3
|
-
import { eq, and, isNull, or } from 'drizzle-orm';
|
|
4
|
-
import debug from 'debug';
|
|
5
|
-
import { L as isActorStopped, aj as ActorKeyedState, B as waitForEntityIdle, N as DEFAULT_READY_TIMEOUT_MS, P as createReactiveProxy, R as findEntity, W as ClientManagerState, I as Item, ak as clearDestroySubscriptions, S as forceRemoveFromCaches, al as unloadEntity, T as runDestroyLifecycle } from './Item-0oDoTPhi.js';
|
|
6
|
-
import { v as getClient } from './ClientManager-GZfXErDX.js';
|
|
7
|
-
import { modelPropertiesToObject } from './model-oHA3YhOd.js';
|
|
8
|
-
import { localNamesInBestMatchTier } from '@seedprotocol/eas';
|
|
9
|
-
import { Type } from '@sinclair/typebox';
|
|
10
|
-
|
|
11
|
-
const logger$7 = debug('seedSdk:model:actors:loadOrCreateModel');
|
|
12
|
-
/**
|
|
13
|
-
* Create ModelProperty instances for all property IDs to ensure they're cached
|
|
14
|
-
* This ensures that ModelProperty.getById() in Model.properties getter will find the instances
|
|
15
|
-
* @param propertyFileIds - Array of property file IDs to create instances for
|
|
16
|
-
*/
|
|
17
|
-
const createPropertyInstances = async (propertyFileIds) => {
|
|
18
|
-
if (propertyFileIds.length === 0) {
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
try {
|
|
22
|
-
const mod = await import('./ModelProperty-i5flwzQG.js');
|
|
23
|
-
const ModelProperty = mod?.ModelProperty ?? mod?.default;
|
|
24
|
-
if (!ModelProperty) {
|
|
25
|
-
logger$7('createPropertyInstances: ModelProperty not available from dynamic import');
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
// Create instances for all property IDs in parallel
|
|
29
|
-
// ModelProperty.createById() will check cache first, then query DB and create if needed
|
|
30
|
-
const createPromises = propertyFileIds.map(async (propertyFileId) => {
|
|
31
|
-
try {
|
|
32
|
-
const property = await ModelProperty.createById(propertyFileId);
|
|
33
|
-
if (property) {
|
|
34
|
-
logger$7(`Created/cached ModelProperty instance for propertyFileId "${propertyFileId}"`);
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
logger$7(`ModelProperty.createById returned undefined for propertyFileId "${propertyFileId}" (may not exist in DB yet)`);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
catch (error) {
|
|
41
|
-
logger$7(`Error creating ModelProperty instance for propertyFileId "${propertyFileId}": ${error}`);
|
|
42
|
-
// Don't throw - continue with other properties
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
await Promise.all(createPromises);
|
|
46
|
-
logger$7(`Finished creating/caching ${propertyFileIds.length} ModelProperty instances`);
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
49
|
-
logger$7(`Error in createPropertyInstances: ${error}`);
|
|
50
|
-
// Don't throw - this is best-effort to pre-populate cache
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
const loadOrCreateModel = fromCallback(({ sendBack, input: { context }, self }) => {
|
|
54
|
-
const _loadOrCreateModel = async () => {
|
|
55
|
-
const { modelName, schemaName, id, _idFromSchema } = context; // id is now the schemaFileId (string)
|
|
56
|
-
if (!modelName || !schemaName) {
|
|
57
|
-
throw new Error('Model name and schema name are required');
|
|
58
|
-
}
|
|
59
|
-
// CRITICAL: Model instances load their data from database, not from Schema context.
|
|
60
|
-
// Schema is read-only with respect to Model instances.
|
|
61
|
-
const db = BaseDb.getAppDb();
|
|
62
|
-
let schemaFileId = id; // id is now the schemaFileId (string)
|
|
63
|
-
let modelRecord = null;
|
|
64
|
-
let foundBySchemaFileId = false; // Track if model was found by schemaFileId
|
|
65
|
-
let existingNumbersFromSchemaContext = null; // When we fall through (same name, different id), schema model occupies base name
|
|
66
|
-
// Step 1: Load from database FIRST (primary source of truth)
|
|
67
|
-
if (db) {
|
|
68
|
-
try {
|
|
69
|
-
// Try to find model by schemaFileId if provided
|
|
70
|
-
if (schemaFileId) {
|
|
71
|
-
const dbModels = await db
|
|
72
|
-
.select()
|
|
73
|
-
.from(models)
|
|
74
|
-
.where(eq(models.schemaFileId, schemaFileId))
|
|
75
|
-
.limit(1);
|
|
76
|
-
if (dbModels.length > 0) {
|
|
77
|
-
modelRecord = dbModels[0];
|
|
78
|
-
foundBySchemaFileId = true;
|
|
79
|
-
logger$7(`Found model "${modelName}" in database by schemaFileId "${schemaFileId}"`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
// If not found by ID, try by name
|
|
83
|
-
// But if we have a schemaFileId and the model found by name has a different schemaFileId,
|
|
84
|
-
// don't use it - we're creating a new model from a schema file with a specific ID
|
|
85
|
-
if (!modelRecord) {
|
|
86
|
-
// Model names are only unique per schema: prefer this schema's row, and otherwise only
|
|
87
|
-
// consider same-name rows that no schema claims yet (never another schema's model).
|
|
88
|
-
const _mod_0 = await import('./index-Dx_GTdyQ.js');
|
|
89
|
-
const _ns_0 = _mod_0.bd;
|
|
90
|
-
const { modelSchemas } = _ns_0;
|
|
91
|
-
const _mod_1 = await import('./index-Dx_GTdyQ.js');
|
|
92
|
-
const _ns_1 = _mod_1.bc;
|
|
93
|
-
const { schemas: schemasTable } = _ns_1;
|
|
94
|
-
let dbModels = await db
|
|
95
|
-
.select({
|
|
96
|
-
id: models.id,
|
|
97
|
-
name: models.name,
|
|
98
|
-
schemaFileId: models.schemaFileId,
|
|
99
|
-
isEdited: models.isEdited,
|
|
100
|
-
})
|
|
101
|
-
.from(models)
|
|
102
|
-
.innerJoin(modelSchemas, eq(models.id, modelSchemas.modelId))
|
|
103
|
-
.innerJoin(schemasTable, eq(modelSchemas.schemaId, schemasTable.id))
|
|
104
|
-
.where(and(eq(models.name, modelName), eq(schemasTable.name, schemaName)))
|
|
105
|
-
.limit(1);
|
|
106
|
-
if (dbModels.length === 0) {
|
|
107
|
-
dbModels = await db
|
|
108
|
-
.select({
|
|
109
|
-
id: models.id,
|
|
110
|
-
name: models.name,
|
|
111
|
-
schemaFileId: models.schemaFileId,
|
|
112
|
-
isEdited: models.isEdited,
|
|
113
|
-
})
|
|
114
|
-
.from(models)
|
|
115
|
-
.leftJoin(modelSchemas, eq(models.id, modelSchemas.modelId))
|
|
116
|
-
// Stubs only: an unlinked row with a schemaFileId is a deleted schema's leftover, not ours
|
|
117
|
-
.where(and(eq(models.name, modelName), isNull(modelSchemas.id), isNull(models.schemaFileId)))
|
|
118
|
-
.limit(1);
|
|
119
|
-
}
|
|
120
|
-
if (dbModels.length > 0) {
|
|
121
|
-
const foundModel = dbModels[0];
|
|
122
|
-
const dbSchemaFileId = foundModel.schemaFileId;
|
|
123
|
-
// CRITICAL: If we found a model in the database by name, check if there's already a cached instance
|
|
124
|
-
// with that schemaFileId. If so, we should use that instance's schemaFileId for the current instance.
|
|
125
|
-
// This handles the case where Model.create was called with a generated ID, but the model
|
|
126
|
-
// already exists in the database with a different ID. By updating the current instance's
|
|
127
|
-
// schemaFileId to match the database, both will point to the same cached instance.
|
|
128
|
-
// Skip for schema models (_idFromSchema): their id is authoritative, and a same-name row
|
|
129
|
-
// with a different id belongs to another schema (e.g. two schemas that each define "Post").
|
|
130
|
-
if (dbSchemaFileId && !(_idFromSchema && schemaFileId && schemaFileId !== dbSchemaFileId)) {
|
|
131
|
-
try {
|
|
132
|
-
const modelMod = await Promise.resolve().then(function () { return Model$1; });
|
|
133
|
-
const { Model } = modelMod;
|
|
134
|
-
// Access instanceCacheById via type assertion since it's protected
|
|
135
|
-
const cacheById = Model.instanceCacheById;
|
|
136
|
-
if (cacheById.has(dbSchemaFileId)) {
|
|
137
|
-
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.`);
|
|
138
|
-
// Update the current instance's schemaFileId to match the database
|
|
139
|
-
// This ensures both instances point to the same cached instance
|
|
140
|
-
modelRecord = foundModel;
|
|
141
|
-
schemaFileId = dbSchemaFileId;
|
|
142
|
-
logger$7(`Using existing cached instance with schemaFileId "${schemaFileId}" for model "${modelName}"`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
catch (error) {
|
|
146
|
-
logger$7(`Error checking Model cache for schemaFileId "${dbSchemaFileId}": ${error}`);
|
|
147
|
-
// Fall through to normal processing
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
// If we have a schemaFileId and it doesn't match the found model's schemaFileId,
|
|
151
|
-
// this is a new model - don't use the existing model (will be renamed if duplicate)
|
|
152
|
-
// UNLESS we found a cached instance above (in which case modelRecord is already set)
|
|
153
|
-
if (!modelRecord && schemaFileId && dbSchemaFileId && schemaFileId !== dbSchemaFileId) {
|
|
154
|
-
logger$7(`Model "${modelName}" found by name but has different schemaFileId (provided: "${schemaFileId}", db: "${dbSchemaFileId}"). Creating new model (will check for duplicates).`);
|
|
155
|
-
// Don't set modelRecord - we'll create a new model with potentially renamed name
|
|
156
|
-
}
|
|
157
|
-
else if (!modelRecord && schemaFileId && !dbSchemaFileId) {
|
|
158
|
-
// We have a schemaFileId but the found model doesn't have one - this is a new model
|
|
159
|
-
// Don't reuse the existing model, create a new one (will be renamed if duplicate)
|
|
160
|
-
logger$7(`Model "${modelName}" found by name but found model has no schemaFileId. Creating new model with schemaFileId "${schemaFileId}" (will check for duplicates).`);
|
|
161
|
-
// Don't set modelRecord - we'll create a new model
|
|
162
|
-
}
|
|
163
|
-
else if (!modelRecord) {
|
|
164
|
-
// Only reuse existing model if:
|
|
165
|
-
// 1. No schemaFileId was provided (loading existing model), OR
|
|
166
|
-
// 2. schemaFileId matches (same model)
|
|
167
|
-
logger$7(`Model "${modelName}" found by name, reusing existing model (schemaFileId: provided="${schemaFileId}", db="${dbSchemaFileId}")`);
|
|
168
|
-
modelRecord = foundModel;
|
|
169
|
-
// Only use database schemaFileId if no id was explicitly provided
|
|
170
|
-
// If an id was provided, we should use it (it might be creating a new model with a specific ID)
|
|
171
|
-
if (!id && dbSchemaFileId) {
|
|
172
|
-
schemaFileId = dbSchemaFileId;
|
|
173
|
-
logger$7(`Using database schemaFileId "${schemaFileId}" for model "${modelName}" (no ID was provided)`);
|
|
174
|
-
}
|
|
175
|
-
else if (id) {
|
|
176
|
-
logger$7(`Preserving provided id (schemaFileId) "${schemaFileId}" for model "${modelName}" (ignoring database schemaFileId "${dbSchemaFileId}")`);
|
|
177
|
-
}
|
|
178
|
-
logger$7(`Found model "${modelName}" in database by name, schemaFileId: "${schemaFileId}"`);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
// If we found the model record, load its properties from database
|
|
183
|
-
if (modelRecord && modelRecord.id) {
|
|
184
|
-
const propertyRecords = await db
|
|
185
|
-
.select()
|
|
186
|
-
.from(properties)
|
|
187
|
-
.where(eq(properties.modelId, modelRecord.id));
|
|
188
|
-
// Properties are now loaded via liveQuery, not passed through context
|
|
189
|
-
// The property records are used by liveQuery to populate ModelProperty instances
|
|
190
|
-
// Pre-create ModelProperty instances for all property IDs to ensure they're cached
|
|
191
|
-
// This ensures that ModelProperty.getById() in Model.properties getter will find the instances
|
|
192
|
-
const propertyFileIds = propertyRecords
|
|
193
|
-
.map((record) => record.schemaFileId)
|
|
194
|
-
.filter((id) => id !== null && id !== undefined);
|
|
195
|
-
if (propertyFileIds.length > 0) {
|
|
196
|
-
// Not for a model stopped meanwhile (unloaded, or evicted with its schema)
|
|
197
|
-
if (!isActorStopped(self))
|
|
198
|
-
await createPropertyInstances(propertyFileIds);
|
|
199
|
-
}
|
|
200
|
-
// Generate schemaFileId if not set
|
|
201
|
-
if (!schemaFileId) {
|
|
202
|
-
schemaFileId = generateId();
|
|
203
|
-
logger$7(`Generated id (schemaFileId) "${schemaFileId}" for model "${modelName}"`);
|
|
204
|
-
}
|
|
205
|
-
// Include _dbId from database record so properties can be created if needed
|
|
206
|
-
// Even if we found an existing model, we may still need to create properties
|
|
207
|
-
// if _pendingPropertyDefinitions are provided
|
|
208
|
-
// Track conflict detection metadata
|
|
209
|
-
const loadedAt = Date.now();
|
|
210
|
-
// Note: models table doesn't have updatedAt or version, so we use loadedAt for _dbUpdatedAt
|
|
211
|
-
// In the future, if models table gets these fields, we should use them
|
|
212
|
-
const dbUpdatedAt = loadedAt;
|
|
213
|
-
// CRITICAL: Include _liveQueryPropertyIds in loadOrCreateModelSuccess event
|
|
214
|
-
// This ensures the properties getter works immediately, even before _setupLiveQuerySubscription runs
|
|
215
|
-
// We already have the property IDs from the database query, so include them now
|
|
216
|
-
sendBack({
|
|
217
|
-
type: 'loadOrCreateModelSuccess',
|
|
218
|
-
model: {
|
|
219
|
-
id: schemaFileId, // schemaFileId (string) - public ID
|
|
220
|
-
_dbId: modelRecord.id, // Database integer ID - internal only
|
|
221
|
-
_liveQueryPropertyIds: propertyFileIds, // Property IDs from database query
|
|
222
|
-
_propertiesUpdated: Date.now(), // Timestamp for tracking
|
|
223
|
-
_isEdited: modelRecord.isEdited ?? false, // Load isEdited from database
|
|
224
|
-
_loadedAt: loadedAt,
|
|
225
|
-
_dbUpdatedAt: dbUpdatedAt,
|
|
226
|
-
},
|
|
227
|
-
});
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
catch (error) {
|
|
232
|
-
logger$7(`Error loading model from database: ${error}`);
|
|
233
|
-
// Fall through to create new model
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
// Step 2: Fallback to Schema context (only if database doesn't have the model)
|
|
237
|
-
// This handles the case where model exists in schema file but not yet in database
|
|
238
|
-
try {
|
|
239
|
-
const _mod_2 = await import('./Schema-PFZwG9Nl.js');
|
|
240
|
-
const _ns_2 = _mod_2.a;
|
|
241
|
-
const schemaMod = _ns_2;
|
|
242
|
-
const { Schema } = schemaMod;
|
|
243
|
-
// Stopped meanwhile (unloaded, or evicted with its schema): don't load the schema again
|
|
244
|
-
if (isActorStopped(self))
|
|
245
|
-
return;
|
|
246
|
-
const schema = Schema.create(schemaName, { waitForReady: false, hold: false });
|
|
247
|
-
const schemaSnapshot = schema.getService().getSnapshot();
|
|
248
|
-
// Wait for schema to load if it's still loading
|
|
249
|
-
// Check if state is a loading state object (XState v5 nested states)
|
|
250
|
-
const isLoading = typeof schemaSnapshot.value === 'object' && 'loading' in schemaSnapshot.value;
|
|
251
|
-
if (isLoading) {
|
|
252
|
-
await new Promise((resolve, reject) => {
|
|
253
|
-
const subscription = schema.getService().subscribe((snapshot) => {
|
|
254
|
-
if (snapshot.value === 'idle' || snapshot.value === 'error') {
|
|
255
|
-
subscription.unsubscribe();
|
|
256
|
-
if (snapshot.value === 'error') {
|
|
257
|
-
reject(new Error('Schema failed to load'));
|
|
258
|
-
}
|
|
259
|
-
else {
|
|
260
|
-
resolve();
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
const schemaContext = schema.getService().getSnapshot().context;
|
|
267
|
-
// Check by name first; also check by schemaFileId so we recognize the schema model even when context is keyed differently or not yet updated
|
|
268
|
-
if (schemaContext.models) {
|
|
269
|
-
const byName = schemaContext.models[modelName];
|
|
270
|
-
const schemaModelId = byName?.id ?? null;
|
|
271
|
-
if (schemaFileId === schemaModelId) {
|
|
272
|
-
logger$7(`Found model "${modelName}" in Schema context (database fallback)`);
|
|
273
|
-
if (!schemaFileId) {
|
|
274
|
-
schemaFileId = generateId();
|
|
275
|
-
logger$7(`Generated id (schemaFileId) "${schemaFileId}" for model "${modelName}"`);
|
|
276
|
-
}
|
|
277
|
-
sendBack({
|
|
278
|
-
type: 'loadOrCreateModelSuccess',
|
|
279
|
-
model: { id: schemaFileId },
|
|
280
|
-
});
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
// Same name but different id: runtime create colliding with schema model name; fall through
|
|
284
|
-
if (byName) {
|
|
285
|
-
existingNumbersFromSchemaContext = new Set([0]);
|
|
286
|
-
}
|
|
287
|
-
else if (schemaFileId) {
|
|
288
|
-
// 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.
|
|
289
|
-
for (const def of Object.values(schemaContext.models)) {
|
|
290
|
-
if (def?.id === schemaFileId) {
|
|
291
|
-
logger$7(`Found model in Schema context by id "${schemaFileId}" (database fallback)`);
|
|
292
|
-
sendBack({
|
|
293
|
-
type: 'loadOrCreateModelSuccess',
|
|
294
|
-
model: { id: schemaFileId },
|
|
295
|
-
});
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
catch (error) {
|
|
303
|
-
logger$7(`Error loading model from Schema context: ${error}`);
|
|
304
|
-
// Fall through to create new model
|
|
305
|
-
}
|
|
306
|
-
// Step 3: Check for duplicate names in database before creating new model
|
|
307
|
-
// Skip duplicate check only if we found a model record to use (modelRecord is set)
|
|
308
|
-
// OR if the model was found by schemaFileId (preserving original name from schema file)
|
|
309
|
-
// OR if this is an internal schema (Seed Protocol) - internal schemas should preserve original names
|
|
310
|
-
// Otherwise, check for duplicates when creating a new model
|
|
311
|
-
// NOTE: We should NOT skip duplicate check just because schemaFileId exists - that could be a newly generated ID
|
|
312
|
-
// for a new model that needs to be renamed if it's a duplicate
|
|
313
|
-
let finalModelName = modelName;
|
|
314
|
-
if (db) {
|
|
315
|
-
// Check if this is an internal schema (Seed Protocol)
|
|
316
|
-
const isInternal = isInternalSchema(schemaName);
|
|
317
|
-
// Only skip duplicate check if:
|
|
318
|
-
// 1. We found a model record to use (modelRecord is set), OR
|
|
319
|
-
// 2. The model was found by schemaFileId (preserving original name from schema file), OR
|
|
320
|
-
// 3. This is an internal schema (internal schemas should preserve original names from schema files), OR
|
|
321
|
-
// 4. This instance was created with modelFileId from schema (_idFromSchema) - schema models keep their name
|
|
322
|
-
const shouldSkipDuplicateCheck = modelRecord !== null || foundBySchemaFileId || isInternal || !!_idFromSchema;
|
|
323
|
-
logger$7(`Duplicate check: modelRecord=${modelRecord !== null}, foundBySchemaFileId=${foundBySchemaFileId}, isInternal=${isInternal}, _idFromSchema=${!!_idFromSchema}, schemaFileId=${schemaFileId}, shouldSkip=${shouldSkipDuplicateCheck}`);
|
|
324
|
-
if (!shouldSkipDuplicateCheck) {
|
|
325
|
-
try {
|
|
326
|
-
const lowerModelName = modelName.toLowerCase();
|
|
327
|
-
const existingNumbers = new Set(existingNumbersFromSchemaContext ?? []);
|
|
328
|
-
// First, check Model cache for models (includes models from imported schemas that may not be in DB yet)
|
|
329
|
-
try {
|
|
330
|
-
const modelMod = await Promise.resolve().then(function () { return Model$1; });
|
|
331
|
-
const { Model } = modelMod;
|
|
332
|
-
// Check name-based cache for this schema
|
|
333
|
-
// Skip the current model (context.id): Model.create() adds the new instance to the cache
|
|
334
|
-
// before this callback runs, so we must not count it as a duplicate.
|
|
335
|
-
logger$7(`Checking Model cache for duplicates in schema "${schemaName}"`);
|
|
336
|
-
const currentModelFileId = schemaFileId ?? null;
|
|
337
|
-
const cacheByName = Model.instanceCacheByName;
|
|
338
|
-
for (const [nameKey, cachedModelFileId] of cacheByName.entries()) {
|
|
339
|
-
if (currentModelFileId && cachedModelFileId === currentModelFileId)
|
|
340
|
-
continue; // skip self
|
|
341
|
-
const [cachedSchemaName, cachedModelName] = nameKey.split(':', 2);
|
|
342
|
-
if (cachedSchemaName === schemaName && cachedModelName) {
|
|
343
|
-
const lowerCachedName = cachedModelName.toLowerCase();
|
|
344
|
-
if (lowerCachedName === lowerModelName) {
|
|
345
|
-
existingNumbers.add(0); // Base name exists
|
|
346
|
-
logger$7(`Found duplicate in Model cache: "${cachedModelName}" matches "${modelName}"`);
|
|
347
|
-
}
|
|
348
|
-
else if (lowerCachedName.startsWith(lowerModelName + ' ')) {
|
|
349
|
-
// Check if it's the base name followed by a space and a number
|
|
350
|
-
const suffix = lowerCachedName.slice(lowerModelName.length + 1);
|
|
351
|
-
const number = parseInt(suffix, 10);
|
|
352
|
-
if (!isNaN(number) && suffix === number.toString()) {
|
|
353
|
-
existingNumbers.add(number);
|
|
354
|
-
logger$7(`Found numbered variant in Model cache: "${cachedModelName}" (number: ${number})`);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
// Also check legacy cache (skip current model by id)
|
|
360
|
-
const legacyCache = Model.instanceCache;
|
|
361
|
-
for (const [nameKey, entry] of legacyCache?.entries() || []) {
|
|
362
|
-
const legacyId = entry?.instance?._getSnapshotContext?.()?.id;
|
|
363
|
-
if (currentModelFileId && legacyId === currentModelFileId)
|
|
364
|
-
continue; // skip self
|
|
365
|
-
const [cachedSchemaName, cachedModelName] = nameKey.split(':', 2);
|
|
366
|
-
if (cachedSchemaName === schemaName && cachedModelName) {
|
|
367
|
-
const lowerCachedName = cachedModelName.toLowerCase();
|
|
368
|
-
if (lowerCachedName === lowerModelName) {
|
|
369
|
-
existingNumbers.add(0); // Base name exists
|
|
370
|
-
logger$7(`Found duplicate in Model legacy cache: "${cachedModelName}" matches "${modelName}"`);
|
|
371
|
-
}
|
|
372
|
-
else if (lowerCachedName.startsWith(lowerModelName + ' ')) {
|
|
373
|
-
// Check if it's the base name followed by a space and a number
|
|
374
|
-
const suffix = lowerCachedName.slice(lowerModelName.length + 1);
|
|
375
|
-
const number = parseInt(suffix, 10);
|
|
376
|
-
if (!isNaN(number) && suffix === number.toString()) {
|
|
377
|
-
existingNumbers.add(number);
|
|
378
|
-
logger$7(`Found numbered variant in Model legacy cache: "${cachedModelName}" (number: ${number})`);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
catch (error) {
|
|
385
|
-
logger$7(`Error checking Model cache for duplicates: ${error}`);
|
|
386
|
-
// Continue with database check
|
|
387
|
-
}
|
|
388
|
-
// Also check database for models (in case they're persisted but not in Schema context)
|
|
389
|
-
if (db) {
|
|
390
|
-
const _mod_3 = await import('./index-Dx_GTdyQ.js');
|
|
391
|
-
const _ns_3 = _mod_3.bd;
|
|
392
|
-
const modelSchemaSchemaMod = _ns_3;
|
|
393
|
-
const { modelSchemas } = modelSchemaSchemaMod;
|
|
394
|
-
const _mod_4 = await import('./index-Dx_GTdyQ.js');
|
|
395
|
-
const _ns_4 = _mod_4.bc;
|
|
396
|
-
const schemaSchemaMod = _ns_4;
|
|
397
|
-
const { schemas: schemasTable } = schemaSchemaMod;
|
|
398
|
-
// Query all models for this schema to check for duplicates (case-insensitive)
|
|
399
|
-
logger$7(`Checking database for duplicate model names in schema "${schemaName}"`);
|
|
400
|
-
const allModelsForSchema = await db
|
|
401
|
-
.select({
|
|
402
|
-
name: models.name,
|
|
403
|
-
})
|
|
404
|
-
.from(models)
|
|
405
|
-
.innerJoin(modelSchemas, eq(models.id, modelSchemas.modelId))
|
|
406
|
-
.innerJoin(schemasTable, eq(modelSchemas.schemaId, schemasTable.id))
|
|
407
|
-
.where(eq(schemasTable.name, schemaName));
|
|
408
|
-
logger$7(`Found ${allModelsForSchema.length} models in database for schema "${schemaName}": ${allModelsForSchema.map((m) => m.name).join(', ')}`);
|
|
409
|
-
if (allModelsForSchema.length === 0 && modelName.includes('Shared')) {
|
|
410
|
-
logger$7(`[DEBUG] No models found in database for schema "${schemaName}" when checking for duplicate "${modelName}"`);
|
|
411
|
-
}
|
|
412
|
-
// Check for exact match (case-insensitive) or matches with number suffix
|
|
413
|
-
for (const dbModel of allModelsForSchema) {
|
|
414
|
-
const lowerDbName = dbModel.name.toLowerCase();
|
|
415
|
-
if (lowerDbName === lowerModelName) {
|
|
416
|
-
existingNumbers.add(0); // Base name exists
|
|
417
|
-
logger$7(`Found duplicate in database: "${dbModel.name}" matches "${modelName}"`);
|
|
418
|
-
}
|
|
419
|
-
else if (lowerDbName.startsWith(lowerModelName + ' ')) {
|
|
420
|
-
// Check if it's the base name followed by a space and a number
|
|
421
|
-
const suffix = lowerDbName.slice(lowerModelName.length + 1);
|
|
422
|
-
const number = parseInt(suffix, 10);
|
|
423
|
-
if (!isNaN(number) && suffix === number.toString()) {
|
|
424
|
-
existingNumbers.add(number);
|
|
425
|
-
logger$7(`Found numbered variant in database: "${dbModel.name}" (number: ${number})`);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
// If duplicates found, generate unique name
|
|
431
|
-
logger$7(`Duplicate check results: existingNumbers=${Array.from(existingNumbers).join(', ')}, hasBaseName=${existingNumbers.has(0)}`);
|
|
432
|
-
if (existingNumbers.has(0)) {
|
|
433
|
-
let nextNumber = 1;
|
|
434
|
-
while (existingNumbers.has(nextNumber)) {
|
|
435
|
-
nextNumber++;
|
|
436
|
-
}
|
|
437
|
-
finalModelName = `${modelName} ${nextNumber}`;
|
|
438
|
-
logger$7(`Found duplicate model name "${modelName}" in schema "${schemaName}", using unique name "${finalModelName}"`);
|
|
439
|
-
}
|
|
440
|
-
else {
|
|
441
|
-
logger$7(`No duplicate found for model name "${modelName}" in schema "${schemaName}"`);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
catch (error) {
|
|
445
|
-
logger$7(`Error checking for duplicate model names: ${error}`);
|
|
446
|
-
// Continue with original name if check fails
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
else {
|
|
450
|
-
if (foundBySchemaFileId) {
|
|
451
|
-
logger$7(`Preserving original model name "${modelName}" for model found by schemaFileId (schemaFileId: ${schemaFileId})`);
|
|
452
|
-
}
|
|
453
|
-
else if (modelRecord) {
|
|
454
|
-
logger$7(`Using existing model name "${modelName}" from database (modelRecord found)`);
|
|
455
|
-
}
|
|
456
|
-
else if (isInternal) {
|
|
457
|
-
logger$7(`Preserving original model name "${modelName}" for internal schema "${schemaName}" (skipping duplicate check)`);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
// Step 4: Create new empty model (not found in database or Schema context)
|
|
462
|
-
logger$7(`Creating new model "${finalModelName}" in schema "${schemaName}"`);
|
|
463
|
-
// Generate schemaFileId for new model
|
|
464
|
-
if (!schemaFileId) {
|
|
465
|
-
schemaFileId = generateId();
|
|
466
|
-
logger$7(`Generated id (schemaFileId) "${schemaFileId}" for new model "${finalModelName}"`);
|
|
467
|
-
}
|
|
468
|
-
// Runtime creates use a placeholder cache key in Model.create; move it to the final name. Skip when schema model (_idFromSchema).
|
|
469
|
-
if (!_idFromSchema) {
|
|
470
|
-
const modelMod = await Promise.resolve().then(function () { return Model$1; });
|
|
471
|
-
const { Model } = modelMod;
|
|
472
|
-
Model.updateNameIndex('__pending__' + schemaFileId, finalModelName, schemaName, schemaFileId);
|
|
473
|
-
}
|
|
474
|
-
// If model name was changed, store it in a temporary internal field so loadOrCreateModelSuccess can apply it
|
|
475
|
-
// This avoids the issue where updateContext with modelName triggers validation and state transition
|
|
476
|
-
// We use _pendingModelName (internal field) which won't trigger validation
|
|
477
|
-
if (finalModelName !== modelName) {
|
|
478
|
-
logger$7(`Model name changed from "${modelName}" to "${finalModelName}", storing in _pendingModelName for loadOrCreateModelSuccess`);
|
|
479
|
-
// Store the final name in a temporary internal field that will be picked up by loadOrCreateModelSuccess
|
|
480
|
-
sendBack({
|
|
481
|
-
type: 'updateContext',
|
|
482
|
-
_pendingModelName: finalModelName,
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
else {
|
|
486
|
-
logger$7(`Model name unchanged: "${modelName}"`);
|
|
487
|
-
}
|
|
488
|
-
// Mark schema as draft when a new model is created so saveNewVersion() can persist it
|
|
489
|
-
try {
|
|
490
|
-
const _mod_5 = await import('./Schema-PFZwG9Nl.js');
|
|
491
|
-
const _ns_5 = _mod_5.a;
|
|
492
|
-
const schemaMod = _ns_5;
|
|
493
|
-
const { Schema } = schemaMod;
|
|
494
|
-
if (!isActorStopped(self)) {
|
|
495
|
-
const schema = Schema.create(schemaName, { waitForReady: false, hold: false });
|
|
496
|
-
schema.getService().send({ type: 'markAsDraft', propertyKey: 'schema:models' });
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
catch (err) {
|
|
500
|
-
logger$7(`Failed to mark schema as draft after creating model: ${err}`);
|
|
501
|
-
}
|
|
502
|
-
sendBack({
|
|
503
|
-
type: 'loadOrCreateModelSuccess',
|
|
504
|
-
model: {
|
|
505
|
-
id: schemaFileId, // schemaFileId (string) - public ID
|
|
506
|
-
},
|
|
507
|
-
});
|
|
508
|
-
};
|
|
509
|
-
_loadOrCreateModel().catch((error) => {
|
|
510
|
-
logger$7('Error loading or creating model:', error);
|
|
511
|
-
sendBack({ type: 'loadOrCreateModelError', error });
|
|
512
|
-
});
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
const logger$6 = debug('seedSdk:model:actors:validateModel');
|
|
516
|
-
// Validation timeout in milliseconds (10 seconds)
|
|
517
|
-
const VALIDATION_TIMEOUT_MS = 10000;
|
|
518
|
-
const validateModel = fromCallback(({ sendBack, input: { context }, self }) => {
|
|
519
|
-
let hasResponded = false;
|
|
520
|
-
// Timeout handler to ensure we always respond
|
|
521
|
-
const timeoutId = setTimeout(() => {
|
|
522
|
-
if (!hasResponded) {
|
|
523
|
-
hasResponded = true;
|
|
524
|
-
sendBack({
|
|
525
|
-
type: 'validationError',
|
|
526
|
-
errors: [{
|
|
527
|
-
field: 'model',
|
|
528
|
-
message: `Model validation timed out after ${VALIDATION_TIMEOUT_MS}ms`,
|
|
529
|
-
code: 'validation_timeout',
|
|
530
|
-
severity: 'error',
|
|
531
|
-
}],
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
}, VALIDATION_TIMEOUT_MS);
|
|
535
|
-
const _validateModel = async () => {
|
|
536
|
-
try {
|
|
537
|
-
// Use dynamic imports to break circular dependencies
|
|
538
|
-
const validationServiceMod = await import('./SchemaValidationService-DnGKel4V.js');
|
|
539
|
-
const { SchemaValidationService } = validationServiceMod;
|
|
540
|
-
const validationService = new SchemaValidationService();
|
|
541
|
-
const _mod_6 = await import('./Schema-PFZwG9Nl.js');
|
|
542
|
-
const _ns_6 = _mod_6.a;
|
|
543
|
-
const schemaMod = _ns_6;
|
|
544
|
-
const { Schema } = schemaMod;
|
|
545
|
-
// Validate model structure
|
|
546
|
-
const structureResult = validationService.validateModelStructure(context);
|
|
547
|
-
if (!structureResult.isValid) {
|
|
548
|
-
if (!hasResponded) {
|
|
549
|
-
hasResponded = true;
|
|
550
|
-
clearTimeout(timeoutId);
|
|
551
|
-
sendBack({ type: 'validationError', errors: structureResult.errors });
|
|
552
|
-
}
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
// If we have schema name, validate against schema
|
|
556
|
-
// CRITICAL: Use validateModelAgainstSchema which doesn't require the model to be in schema context
|
|
557
|
-
// This allows validation BEFORE registration, preventing update loops
|
|
558
|
-
// Stopped meanwhile (unloaded, or evicted with its schema): don't load the schema again
|
|
559
|
-
if (isActorStopped(self))
|
|
560
|
-
return;
|
|
561
|
-
if (context.schemaName) {
|
|
562
|
-
try {
|
|
563
|
-
const schema = Schema.create(context.schemaName, { waitForReady: false, hold: false });
|
|
564
|
-
const schemaSnapshot = schema.getService().getSnapshot();
|
|
565
|
-
const schemaStatus = schemaSnapshot.value;
|
|
566
|
-
// Only validate against schema if it's loaded (in idle state)
|
|
567
|
-
if (schemaStatus === 'idle') {
|
|
568
|
-
const schemaContext = schemaSnapshot.context;
|
|
569
|
-
// Use validateModelAgainstSchema which validates the model data directly
|
|
570
|
-
// without requiring it to be in schema.models. This prevents loops because:
|
|
571
|
-
// 1. Model validates against schema (model NOT in schema context yet)
|
|
572
|
-
// 2. If valid, model gets registered with schema
|
|
573
|
-
// 3. No need for manual updates because model is already correct (liveQuery handles updates)
|
|
574
|
-
const schemaResult = validationService.validateModelAgainstSchema(schemaContext, context.modelName, context);
|
|
575
|
-
if (!schemaResult.isValid) {
|
|
576
|
-
if (!hasResponded) {
|
|
577
|
-
hasResponded = true;
|
|
578
|
-
clearTimeout(timeoutId);
|
|
579
|
-
sendBack({ type: 'validationError', errors: schemaResult.errors });
|
|
580
|
-
}
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
else {
|
|
585
|
-
logger$6(`Schema is in ${schemaStatus} state, skipping schema validation`);
|
|
586
|
-
// Continue with structure validation only
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
catch (error) {
|
|
590
|
-
logger$6('Error validating model against schema:', error);
|
|
591
|
-
// Continue with structure validation only
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
// All validations passed
|
|
595
|
-
if (!hasResponded) {
|
|
596
|
-
hasResponded = true;
|
|
597
|
-
clearTimeout(timeoutId);
|
|
598
|
-
sendBack({ type: 'validationSuccess', errors: [] });
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
catch (error) {
|
|
602
|
-
if (!hasResponded) {
|
|
603
|
-
hasResponded = true;
|
|
604
|
-
clearTimeout(timeoutId);
|
|
605
|
-
logger$6('Error in validateModel:', error);
|
|
606
|
-
sendBack({
|
|
607
|
-
type: 'validationError',
|
|
608
|
-
errors: [{
|
|
609
|
-
field: 'model',
|
|
610
|
-
message: error instanceof Error ? error.message : 'Unknown validation error',
|
|
611
|
-
code: 'validation_exception',
|
|
612
|
-
severity: 'error',
|
|
613
|
-
}],
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
};
|
|
618
|
-
_validateModel().catch((error) => {
|
|
619
|
-
if (!hasResponded) {
|
|
620
|
-
hasResponded = true;
|
|
621
|
-
clearTimeout(timeoutId);
|
|
622
|
-
logger$6('Error in validateModel:', error);
|
|
623
|
-
sendBack({
|
|
624
|
-
type: 'validationError',
|
|
625
|
-
errors: [{
|
|
626
|
-
field: 'model',
|
|
627
|
-
message: error instanceof Error ? error.message : 'Unknown validation error',
|
|
628
|
-
code: 'validation_exception',
|
|
629
|
-
severity: 'error',
|
|
630
|
-
}],
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
});
|
|
634
|
-
return () => {
|
|
635
|
-
// Cleanup function - clear timeout if actor is stopped
|
|
636
|
-
clearTimeout(timeoutId);
|
|
637
|
-
};
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
const logger$5 = debug('seedSdk:model:actors:createModelProperties');
|
|
641
|
-
const createModelProperties = fromCallback(({ sendBack, input, self }) => {
|
|
642
|
-
// The model was stopped (unloaded, or evicted with its schema by Schema.destroy / test cleanup) while
|
|
643
|
-
// this ran: stopping it stops this actor too, but can't cancel the awaits below. A stopped model
|
|
644
|
-
// creates no properties (they'd be cached for a model that's gone, and write rows under it).
|
|
645
|
-
const stopped = () => isActorStopped(self);
|
|
646
|
-
const _createProperties = async () => {
|
|
647
|
-
const { context, propertyDefinitions } = input;
|
|
648
|
-
const { id, _dbId, modelName, schemaName } = context;
|
|
649
|
-
if (!id || !_dbId || !modelName) {
|
|
650
|
-
throw new Error('Model ID, file ID, and name are required to create properties');
|
|
651
|
-
}
|
|
652
|
-
if (!propertyDefinitions || Object.keys(propertyDefinitions).length === 0) {
|
|
653
|
-
logger$5('No property definitions provided, skipping property creation');
|
|
654
|
-
sendBack({ type: 'createModelPropertiesSuccess' });
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
logger$5(`Creating ${Object.keys(propertyDefinitions).length} properties for model "${modelName}" (id: ${_dbId})`);
|
|
658
|
-
const mod = await import('./ModelProperty-i5flwzQG.js');
|
|
659
|
-
const ModelProperty = mod?.ModelProperty ?? mod?.default;
|
|
660
|
-
if (!ModelProperty) {
|
|
661
|
-
logger$5('ModelProperty not available from dynamic import');
|
|
662
|
-
sendBack({ type: 'createModelPropertiesError', error: new Error('ModelProperty not available') });
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
const drizzleMod = await import('drizzle-orm');
|
|
666
|
-
const { and, eq } = drizzleMod;
|
|
667
|
-
const db = BaseDb.getAppDb();
|
|
668
|
-
for (const [propName, propData] of Object.entries(propertyDefinitions)) {
|
|
669
|
-
// The model's own write (writeModelToDb) normally stored this property already, under
|
|
670
|
-
// propData.id or an id it generated. Reuse that row so the instance gets the same id
|
|
671
|
-
// and doesn't write the property a second time.
|
|
672
|
-
const propertyRecord = db
|
|
673
|
-
? (await db
|
|
674
|
-
.select()
|
|
675
|
-
.from(properties)
|
|
676
|
-
.where(propData.id
|
|
677
|
-
? eq(properties.schemaFileId, propData.id)
|
|
678
|
-
: and(eq(properties.modelId, _dbId), eq(properties.name, propName)))
|
|
679
|
-
.limit(1))[0]
|
|
680
|
-
: undefined;
|
|
681
|
-
// Otherwise use provided ID or generate a random one
|
|
682
|
-
// IDs should be generated in the import process before creating properties
|
|
683
|
-
const _mod_7 = await import('./index-Dx_GTdyQ.js');
|
|
684
|
-
const _ns_7 = _mod_7.bh;
|
|
685
|
-
const { generateId } = _ns_7;
|
|
686
|
-
const propertyFileId = propData.id || propertyRecord?.schemaFileId || generateId();
|
|
687
|
-
logger$5(`Creating property "${propName}" with fileId "${propertyFileId}"`);
|
|
688
|
-
// Query database to get refModelId if the property has ref/refModelName
|
|
689
|
-
// The property should already be in DB from addModelsToDb
|
|
690
|
-
let refModelId = propData.refModelId;
|
|
691
|
-
if (!refModelId && (propData.ref || propData.refModelName) && db) {
|
|
692
|
-
try {
|
|
693
|
-
const refModelName = propData.refModelName || propData.ref;
|
|
694
|
-
// First try to get refModelId from the property record in the database
|
|
695
|
-
if (propertyRecord?.refModelId) {
|
|
696
|
-
refModelId = propertyRecord.refModelId;
|
|
697
|
-
logger$5(`Found refModelId ${refModelId} from database for property "${propName}"`);
|
|
698
|
-
}
|
|
699
|
-
else if (refModelName) {
|
|
700
|
-
// Fallback: resolve the ref by name within this model's schema
|
|
701
|
-
const _mod_8 = await import('./index-Dx_GTdyQ.js');
|
|
702
|
-
const _ns_8 = _mod_8.be;
|
|
703
|
-
const { resolveModelRecord } = _ns_8;
|
|
704
|
-
const refModelRecord = await resolveModelRecord(refModelName, { schemaName }, db);
|
|
705
|
-
if (refModelRecord?.id) {
|
|
706
|
-
refModelId = refModelRecord.id;
|
|
707
|
-
logger$5(`Resolved refModelId ${refModelId} from model name "${refModelName}" for property "${propName}"`);
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
catch (error) {
|
|
712
|
-
logger$5(`Error fetching refModelId for property "${propName}":`, error);
|
|
713
|
-
// Continue without refModelId - it will be resolved later in _initializeOriginalValues
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
if (stopped()) {
|
|
717
|
-
logger$5(`Model "${modelName}" was stopped while creating its properties; not creating the rest`);
|
|
718
|
-
return;
|
|
719
|
-
}
|
|
720
|
-
// Create ModelProperty instance
|
|
721
|
-
// This will load from DB if it exists, or create new instance
|
|
722
|
-
// The property should already be in DB from writeModelToDb
|
|
723
|
-
// Note: propertyFileId is the schemaFileId (string), not the database ID (number)
|
|
724
|
-
// We pass it as _propertyFileId so getById() can find the instance
|
|
725
|
-
const property = await ModelProperty.create({
|
|
726
|
-
name: propName,
|
|
727
|
-
modelName,
|
|
728
|
-
modelId: _dbId,
|
|
729
|
-
dataType: propData.dataType,
|
|
730
|
-
ref: propData.ref,
|
|
731
|
-
refModelName: propData.refModelName,
|
|
732
|
-
refModelId,
|
|
733
|
-
refValueType: propData.refValueType,
|
|
734
|
-
// Definitions built from DB rows may lack storage settings; the row has them.
|
|
735
|
-
storageType: propData.storageType ?? propertyRecord?.storageType ?? undefined,
|
|
736
|
-
localStorageDir: propData.localStorageDir ?? propertyRecord?.localStorageDir ?? undefined,
|
|
737
|
-
filenameSuffix: propData.filenameSuffix ?? propertyRecord?.filenameSuffix ?? undefined,
|
|
738
|
-
required: propData.required,
|
|
739
|
-
// Rules defined in code live only on the instance (the properties table has no column
|
|
740
|
-
// for them); getPropertySchema reads them from here when no schema file has them.
|
|
741
|
-
validation: propData.validation,
|
|
742
|
-
htmlEmbeddedDataUriPolicy: propData.htmlEmbeddedDataUriPolicy,
|
|
743
|
-
_propertyFileId: propertyFileId, // Store schemaFileId for getById() lookups
|
|
744
|
-
_dbId: propertyRecord?.schemaFileId === propertyFileId ? propertyRecord.id : undefined, // Already persisted: skip the write
|
|
745
|
-
}, { schemaName }); // Use 'as any' because _propertyFileId is not in TProperty type
|
|
746
|
-
// The model keeps its properties; an app's unload of the same instance doesn't stop them.
|
|
747
|
-
ModelProperty.adoptForModel(property);
|
|
748
|
-
}
|
|
749
|
-
logger$5(`Successfully created all properties for model "${modelName}"`);
|
|
750
|
-
sendBack({ type: 'createModelPropertiesSuccess' });
|
|
751
|
-
};
|
|
752
|
-
_createProperties().catch((error) => {
|
|
753
|
-
logger$5(`Error creating model properties: ${error}`);
|
|
754
|
-
sendBack({
|
|
755
|
-
type: 'createModelPropertiesError',
|
|
756
|
-
error: error instanceof Error ? error : new Error(String(error))
|
|
757
|
-
});
|
|
758
|
-
});
|
|
759
|
-
return () => {
|
|
760
|
-
// Cleanup function (optional)
|
|
761
|
-
};
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
/**
|
|
765
|
-
* Eviction epochs: lets work that was started before a schema's Model/ModelProperty instances were
|
|
766
|
-
* evicted (Schema.destroy, test cleanup) tell, when it finishes, that it must not put instances for
|
|
767
|
-
* that schema back in the caches. Stopping the evicted actors can't cancel such work (a DB lookup
|
|
768
|
-
* that ends in Model.create, a model's property creation), and finishing it after the eviction would
|
|
769
|
-
* rebuild instances from rows that are about to be deleted, which then write themselves again.
|
|
770
|
-
*
|
|
771
|
-
* Capture `currentEvictionEpoch()` when the work starts; before creating or caching an instance, skip
|
|
772
|
-
* it when `schemaEvictedSince(schemaName, epoch)`. Work started after an eviction is unaffected.
|
|
773
|
-
*/
|
|
774
|
-
let epoch = 0;
|
|
775
|
-
const lastEvictionBySchema = new Map();
|
|
776
|
-
// Schemas whose destroy() is still running (by name)
|
|
777
|
-
const destroyingSchemas = new Map();
|
|
778
|
-
// Single models destroyed (Model.destroy), by modelFileId
|
|
779
|
-
const lastDestroyByModelId = new Map();
|
|
780
|
-
// Models whose destroy() is still deleting their rows
|
|
781
|
-
const destroyingModelIds = new Map();
|
|
782
|
-
function currentEvictionEpoch() {
|
|
783
|
-
return epoch;
|
|
784
|
-
}
|
|
785
|
-
/** Called by Model.evictForSchema. */
|
|
786
|
-
function recordSchemaEviction(schemaName) {
|
|
787
|
-
epoch += 1;
|
|
788
|
-
lastEvictionBySchema.set(schemaName, epoch);
|
|
789
|
-
}
|
|
790
|
-
/**
|
|
791
|
-
* Called by Schema.destroy before it evicts the schema's models; `finishSchemaDestroy` once its rows
|
|
792
|
-
* are gone. The schema's actor stays live until then, and work it starts meanwhile (its live query
|
|
793
|
-
* reading a model row the drained write just landed) captures an epoch after the eviction, so the
|
|
794
|
-
* epoch alone can't tell it to stay away.
|
|
795
|
-
*/
|
|
796
|
-
function recordSchemaDestroy(schemaName) {
|
|
797
|
-
recordSchemaEviction(schemaName);
|
|
798
|
-
destroyingSchemas.set(schemaName, (destroyingSchemas.get(schemaName) ?? 0) + 1);
|
|
799
|
-
}
|
|
800
|
-
function finishSchemaDestroy(schemaName) {
|
|
801
|
-
const remaining = (destroyingSchemas.get(schemaName) ?? 1) - 1;
|
|
802
|
-
if (remaining > 0)
|
|
803
|
-
destroyingSchemas.set(schemaName, remaining);
|
|
804
|
-
else
|
|
805
|
-
destroyingSchemas.delete(schemaName);
|
|
806
|
-
// Work that read a row during the destroy and checks after it must still see the eviction
|
|
807
|
-
recordSchemaEviction(schemaName);
|
|
808
|
-
}
|
|
809
|
-
/**
|
|
810
|
-
* True when the schema is being destroyed, or its instances were evicted after `since` (an epoch
|
|
811
|
-
* from currentEvictionEpoch).
|
|
812
|
-
*/
|
|
813
|
-
function schemaEvictedSince(schemaName, since) {
|
|
814
|
-
if (!schemaName)
|
|
815
|
-
return false;
|
|
816
|
-
if (destroyingSchemas.has(schemaName))
|
|
817
|
-
return true;
|
|
818
|
-
if (epoch === since)
|
|
819
|
-
return false;
|
|
820
|
-
return (lastEvictionBySchema.get(schemaName) ?? 0) > since;
|
|
821
|
-
}
|
|
822
|
-
/**
|
|
823
|
-
* Called by Model.destroy before it clears the caches; `finishModelDestroy` once its rows are gone.
|
|
824
|
-
* Until then a lookup by id still finds the row, so it must not re-cache the model either.
|
|
825
|
-
*/
|
|
826
|
-
function recordModelDestroy(modelFileId) {
|
|
827
|
-
epoch += 1;
|
|
828
|
-
lastDestroyByModelId.set(modelFileId, epoch);
|
|
829
|
-
destroyingModelIds.set(modelFileId, (destroyingModelIds.get(modelFileId) ?? 0) + 1);
|
|
830
|
-
}
|
|
831
|
-
function finishModelDestroy(modelFileId) {
|
|
832
|
-
const remaining = (destroyingModelIds.get(modelFileId) ?? 1) - 1;
|
|
833
|
-
if (remaining > 0)
|
|
834
|
-
destroyingModelIds.set(modelFileId, remaining);
|
|
835
|
-
else
|
|
836
|
-
destroyingModelIds.delete(modelFileId);
|
|
837
|
-
}
|
|
838
|
-
/**
|
|
839
|
-
* True when the model is being destroyed, or was destroyed after `since` (an epoch from
|
|
840
|
-
* currentEvictionEpoch): a lookup that read its row must not create or cache an instance.
|
|
841
|
-
*/
|
|
842
|
-
function modelDestroyedSince(modelFileId, since) {
|
|
843
|
-
if (!modelFileId)
|
|
844
|
-
return false;
|
|
845
|
-
if (destroyingModelIds.has(modelFileId))
|
|
846
|
-
return true;
|
|
847
|
-
if (epoch === since)
|
|
848
|
-
return false;
|
|
849
|
-
return (lastDestroyByModelId.get(modelFileId) ?? 0) > since;
|
|
850
|
-
}
|
|
851
|
-
/**
|
|
852
|
-
* True when any schema was evicted after `since` or is being destroyed (cheap check before looking up
|
|
853
|
-
* which schemas apply).
|
|
854
|
-
*/
|
|
855
|
-
function anyEvictionSince(since) {
|
|
856
|
-
return epoch !== since || destroyingSchemas.size > 0;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
const logger$4 = debug('seedSdk:write:validateEntity');
|
|
860
|
-
// XState v5 type inference bug: fromPromise types are inverted
|
|
861
|
-
// We use type assertion to work around this
|
|
862
|
-
const validateEntity = fromPromise(async ({ input }) => {
|
|
863
|
-
// Type assertion to fix XState v5 type inference bug
|
|
864
|
-
// Convert through unknown to avoid type overlap error
|
|
865
|
-
const entityInput = input;
|
|
866
|
-
// Validation awaits imports before looking up the schema; the entity can be evicted with its
|
|
867
|
-
// schema meanwhile (Schema.destroy, test cleanup). Schema.create would then load the evicted
|
|
868
|
-
// schema again, and its load re-creates the schema's models from rows being deleted.
|
|
869
|
-
const evictionEpoch = currentEvictionEpoch();
|
|
870
|
-
/** The schema to validate against; throws (validation then skips the schema check) once it was evicted. */
|
|
871
|
-
const schemaForValidation = async (schemaName) => {
|
|
872
|
-
const _mod_9 = await import('./Schema-PFZwG9Nl.js');
|
|
873
|
-
const _ns_9 = _mod_9.a;
|
|
874
|
-
const { Schema } = _ns_9;
|
|
875
|
-
if (schemaEvictedSince(schemaName, evictionEpoch)) {
|
|
876
|
-
throw new Error(`Schema "${schemaName}" was evicted during validation`);
|
|
877
|
-
}
|
|
878
|
-
return Schema.create(schemaName, { waitForReady: false, hold: false });
|
|
879
|
-
};
|
|
880
|
-
const _validate = async () => {
|
|
881
|
-
try {
|
|
882
|
-
const msg = `[validateEntity] Starting validation for ${entityInput.entityType}`;
|
|
883
|
-
logger$4(msg);
|
|
884
|
-
logger$4(`[validateEntity] Entity data:`, entityInput.entityData);
|
|
885
|
-
let result = { isValid: true, errors: [] };
|
|
886
|
-
if (entityInput.entityType === 'model') {
|
|
887
|
-
const structureMsg = `[validateEntity] Validating model structure`;
|
|
888
|
-
logger$4(structureMsg);
|
|
889
|
-
// Use existing Model validation
|
|
890
|
-
const validationServiceMod = await import('./SchemaValidationService-DnGKel4V.js');
|
|
891
|
-
const { SchemaValidationService } = validationServiceMod;
|
|
892
|
-
const validationService = new SchemaValidationService();
|
|
893
|
-
// Validate model structure
|
|
894
|
-
const structureResult = validationService.validateModelStructure(entityInput.entityData);
|
|
895
|
-
const structureResultMsg = `[validateEntity] Structure validation result: isValid=${structureResult.isValid}, errors=${structureResult.errors?.length || 0}`;
|
|
896
|
-
logger$4(structureResultMsg);
|
|
897
|
-
if (!structureResult.isValid) {
|
|
898
|
-
result = {
|
|
899
|
-
isValid: false,
|
|
900
|
-
errors: structureResult.errors,
|
|
901
|
-
};
|
|
902
|
-
}
|
|
903
|
-
else {
|
|
904
|
-
// If schema name provided, validate against schema
|
|
905
|
-
if (entityInput.entityData.schemaName) {
|
|
906
|
-
try {
|
|
907
|
-
logger$4(`[validateEntity] Validating model against schema "${entityInput.entityData.schemaName}"`);
|
|
908
|
-
const schema = await schemaForValidation(entityInput.entityData.schemaName);
|
|
909
|
-
const schemaSnapshot = schema.getService().getSnapshot();
|
|
910
|
-
const schemaStatus = schemaSnapshot.value;
|
|
911
|
-
logger$4(`[validateEntity] Schema status: ${schemaStatus}`);
|
|
912
|
-
if (schemaStatus === 'idle') {
|
|
913
|
-
const schemaContext = schemaSnapshot.context;
|
|
914
|
-
logger$4(`[validateEntity] Running validateModelAgainstSchema`);
|
|
915
|
-
const schemaResult = validationService.validateModelAgainstSchema(schemaContext, entityInput.entityData.modelName, entityInput.entityData);
|
|
916
|
-
logger$4(`[validateEntity] Schema validation result:`, schemaResult);
|
|
917
|
-
if (!schemaResult.isValid) {
|
|
918
|
-
result = {
|
|
919
|
-
isValid: false,
|
|
920
|
-
errors: schemaResult.errors,
|
|
921
|
-
};
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
else {
|
|
925
|
-
logger$4(`[validateEntity] Schema not in idle state (${schemaStatus}), skipping schema validation`);
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
catch (error) {
|
|
929
|
-
logger$4('Error validating model against schema:', error);
|
|
930
|
-
// Continue with structure validation only
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
else {
|
|
934
|
-
logger$4(`[validateEntity] No schemaName provided, skipping schema validation`);
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
else if (entityInput.entityType === 'modelProperty') {
|
|
939
|
-
// Use existing ModelProperty validation
|
|
940
|
-
const validationServiceMod = await import('./SchemaValidationService-DnGKel4V.js');
|
|
941
|
-
const { SchemaValidationService } = validationServiceMod;
|
|
942
|
-
const validationService = new SchemaValidationService();
|
|
943
|
-
// Validate property structure
|
|
944
|
-
const structureResult = validationService.validatePropertyStructure(entityInput.entityData);
|
|
945
|
-
if (!structureResult.isValid) {
|
|
946
|
-
result = {
|
|
947
|
-
isValid: false,
|
|
948
|
-
errors: structureResult.errors,
|
|
949
|
-
};
|
|
950
|
-
}
|
|
951
|
-
else {
|
|
952
|
-
// If schema name and model name provided, validate against schema
|
|
953
|
-
if (entityInput.entityData._schemaName && entityInput.entityData.modelName) {
|
|
954
|
-
try {
|
|
955
|
-
const schema = await schemaForValidation(entityInput.entityData._schemaName);
|
|
956
|
-
const schemaSnapshot = schema.getService().getSnapshot();
|
|
957
|
-
const schemaStatus = schemaSnapshot.value;
|
|
958
|
-
if (schemaStatus === 'idle') {
|
|
959
|
-
const schemaContext = schemaSnapshot.context;
|
|
960
|
-
if (schemaContext.models && Object.keys(schemaContext.models).length > 0) {
|
|
961
|
-
const schemaResult = validationService.validateProperty(schemaContext, entityInput.entityData.modelName, entityInput.entityData.name || '', entityInput.entityData);
|
|
962
|
-
if (!schemaResult.isValid) {
|
|
963
|
-
result = {
|
|
964
|
-
isValid: false,
|
|
965
|
-
errors: schemaResult.errors,
|
|
966
|
-
};
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
catch (error) {
|
|
972
|
-
logger$4('Error validating property against schema:', error);
|
|
973
|
-
// Continue with structure validation only
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
// Return the result - fromPromise will automatically put this in event.output
|
|
979
|
-
const completeMsg = `[validateEntity] Validation complete: isValid=${result.isValid}, errors=${result.errors?.length || 0}`;
|
|
980
|
-
logger$4(completeMsg);
|
|
981
|
-
logger$4(`[validateEntity] Validation complete:`, result);
|
|
982
|
-
return result;
|
|
983
|
-
}
|
|
984
|
-
catch (error) {
|
|
985
|
-
const errorMsg = `[validateEntity] Error in validateEntity: ${error instanceof Error ? error.message : String(error)}`;
|
|
986
|
-
logger$4(errorMsg);
|
|
987
|
-
logger$4('Error in validateEntity:', error);
|
|
988
|
-
// Throw error - fromPromise will automatically create error.platform event
|
|
989
|
-
throw error;
|
|
990
|
-
}
|
|
991
|
-
};
|
|
992
|
-
return await _validate();
|
|
993
|
-
});
|
|
994
|
-
|
|
995
|
-
const logger$3 = debug('seedSdk:write:writeToDatabase');
|
|
996
|
-
// Writes that have started. Stopping a write process's actor can't cancel one, so code that deletes rows
|
|
997
|
-
// under live instances (Schema.destroy, test cleanup) waits for these after evicting the instances.
|
|
998
|
-
const inFlightWrites = new Set();
|
|
999
|
-
/**
|
|
1000
|
-
* Resolves once every database write started so far has finished (or after timeoutMs). Evict/stop the
|
|
1001
|
-
* instances first so no new writes start while waiting.
|
|
1002
|
-
*/
|
|
1003
|
-
const waitForInFlightWrites = async (timeoutMs = 5000) => {
|
|
1004
|
-
const deadline = Date.now() + timeoutMs;
|
|
1005
|
-
while (inFlightWrites.size > 0 && Date.now() < deadline) {
|
|
1006
|
-
await Promise.race([
|
|
1007
|
-
Promise.allSettled([...inFlightWrites]),
|
|
1008
|
-
new Promise((resolve) => setTimeout(resolve, deadline - Date.now())),
|
|
1009
|
-
]);
|
|
1010
|
-
}
|
|
1011
|
-
};
|
|
1012
|
-
const writeToDatabase = fromCallback(({ sendBack, input }) => {
|
|
1013
|
-
const _write = async () => {
|
|
1014
|
-
try {
|
|
1015
|
-
const db = BaseDb.getAppDb();
|
|
1016
|
-
if (!db) {
|
|
1017
|
-
throw new Error('Database not available');
|
|
1018
|
-
}
|
|
1019
|
-
let output = input.entityData;
|
|
1020
|
-
if (input.entityType === 'model') {
|
|
1021
|
-
const writeMsg = `Writing model to database: ${input.entityData.modelName} (schemaId: ${input.entityData.schemaId})`;
|
|
1022
|
-
logger$3(writeMsg);
|
|
1023
|
-
logger$3(`Writing model to database:`, {
|
|
1024
|
-
modelFileId: input.entityId,
|
|
1025
|
-
modelName: input.entityData.modelName,
|
|
1026
|
-
schemaId: input.entityData.schemaId,
|
|
1027
|
-
});
|
|
1028
|
-
await writeModelToDb(input.entityId, input.entityData);
|
|
1029
|
-
// Get _dbId from database after write
|
|
1030
|
-
const db = BaseDb.getAppDb();
|
|
1031
|
-
if (db) {
|
|
1032
|
-
const modelRecords = await db
|
|
1033
|
-
.select({ id: models.id })
|
|
1034
|
-
.from(models)
|
|
1035
|
-
.where(eq(models.schemaFileId, input.entityId)) // entityId is now the schemaFileId (string)
|
|
1036
|
-
.limit(1);
|
|
1037
|
-
if (modelRecords.length > 0 && modelRecords[0].id) {
|
|
1038
|
-
output = { ...input.entityData, _dbId: modelRecords[0].id }; // Store as _dbId (database integer ID)
|
|
1039
|
-
}
|
|
1040
|
-
else {
|
|
1041
|
-
output = input.entityData;
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
else {
|
|
1045
|
-
output = input.entityData;
|
|
1046
|
-
}
|
|
1047
|
-
const successMsg = `Successfully wrote model "${input.entityData.modelName}" to database`;
|
|
1048
|
-
logger$3(successMsg);
|
|
1049
|
-
}
|
|
1050
|
-
else if (input.entityType === 'modelProperty') {
|
|
1051
|
-
// Use current ModelProperty context when available so a user rename before the
|
|
1052
|
-
// initial write completes is not overwritten by the stale requestWrite payload.
|
|
1053
|
-
let dataToWrite = input.entityData;
|
|
1054
|
-
try {
|
|
1055
|
-
const mod = await import('./ModelProperty-i5flwzQG.js');
|
|
1056
|
-
const ModelProperty = mod?.ModelProperty ?? mod?.default;
|
|
1057
|
-
if (ModelProperty && typeof ModelProperty.getById === 'function') {
|
|
1058
|
-
const instance = ModelProperty.getById(input.entityId);
|
|
1059
|
-
if (instance && typeof instance._getSnapshotContext === 'function') {
|
|
1060
|
-
const ctx = instance._getSnapshotContext();
|
|
1061
|
-
const ctxName = ctx.name ?? input.entityData.name;
|
|
1062
|
-
dataToWrite = {
|
|
1063
|
-
...input.entityData,
|
|
1064
|
-
name: ctxName,
|
|
1065
|
-
dataType: ctx.dataType ?? input.entityData.dataType,
|
|
1066
|
-
refModelId: ctx.refModelId ?? input.entityData.refModelId,
|
|
1067
|
-
refValueType: ctx.refValueType ?? input.entityData.refValueType,
|
|
1068
|
-
refModelName: ctx.refModelName ?? input.entityData.refModelName,
|
|
1069
|
-
storageType: ctx.storageType ?? input.entityData.storageType,
|
|
1070
|
-
localStorageDir: ctx.localStorageDir ?? input.entityData.localStorageDir,
|
|
1071
|
-
filenameSuffix: ctx.filenameSuffix ?? input.entityData.filenameSuffix,
|
|
1072
|
-
required: ctx.required ?? input.entityData.required,
|
|
1073
|
-
};
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
catch (_) {
|
|
1078
|
-
// Fall back to input.entityData if instance not available
|
|
1079
|
-
}
|
|
1080
|
-
await writePropertyToDb(input.entityId, dataToWrite);
|
|
1081
|
-
output = input.entityData;
|
|
1082
|
-
}
|
|
1083
|
-
else {
|
|
1084
|
-
throw new Error(`Unknown entity type: ${input.entityType}`);
|
|
1085
|
-
}
|
|
1086
|
-
// Send success event explicitly - callback actors don't trigger onDone
|
|
1087
|
-
sendBack({
|
|
1088
|
-
type: 'writeSuccess',
|
|
1089
|
-
output,
|
|
1090
|
-
});
|
|
1091
|
-
}
|
|
1092
|
-
catch (error) {
|
|
1093
|
-
logger$3('Error in writeToDatabase:', error);
|
|
1094
|
-
sendBack({
|
|
1095
|
-
type: 'writeError',
|
|
1096
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
1097
|
-
});
|
|
1098
|
-
}
|
|
1099
|
-
};
|
|
1100
|
-
const write = _write().catch((error) => {
|
|
1101
|
-
logger$3('Error in writeToDatabase promise:', error);
|
|
1102
|
-
sendBack({
|
|
1103
|
-
type: 'writeError',
|
|
1104
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
1105
|
-
});
|
|
1106
|
-
});
|
|
1107
|
-
inFlightWrites.add(write);
|
|
1108
|
-
write.finally(() => inFlightWrites.delete(write));
|
|
1109
|
-
return () => {
|
|
1110
|
-
// Cleanup function (optional)
|
|
1111
|
-
};
|
|
1112
|
-
});
|
|
1113
|
-
|
|
1114
|
-
var writeToDatabase$1 = /*#__PURE__*/Object.freeze({
|
|
1115
|
-
__proto__: null,
|
|
1116
|
-
waitForInFlightWrites: waitForInFlightWrites,
|
|
1117
|
-
writeToDatabase: writeToDatabase
|
|
1118
|
-
});
|
|
1119
|
-
|
|
1120
|
-
const logger$2 = debug('seedSdk:write:writeProcessMachine');
|
|
1121
|
-
const writeProcessMachine = setup({
|
|
1122
|
-
types: {
|
|
1123
|
-
context: {},
|
|
1124
|
-
input: {},
|
|
1125
|
-
events: {},
|
|
1126
|
-
},
|
|
1127
|
-
actors: {
|
|
1128
|
-
validateEntity,
|
|
1129
|
-
writeToDatabase,
|
|
1130
|
-
},
|
|
1131
|
-
}).createMachine({
|
|
1132
|
-
id: 'writeProcess',
|
|
1133
|
-
initial: 'idle',
|
|
1134
|
-
context: ({ input }) => ({
|
|
1135
|
-
entityType: input.entityType,
|
|
1136
|
-
entityId: input.entityId,
|
|
1137
|
-
entityData: input.entityData || {},
|
|
1138
|
-
validationErrors: [],
|
|
1139
|
-
error: null,
|
|
1140
|
-
pendingWrite: null,
|
|
1141
|
-
}),
|
|
1142
|
-
states: {
|
|
1143
|
-
idle: {
|
|
1144
|
-
on: {
|
|
1145
|
-
startWrite: {
|
|
1146
|
-
target: 'validating',
|
|
1147
|
-
actions: [
|
|
1148
|
-
assign({
|
|
1149
|
-
pendingWrite: ({ event }) => ({
|
|
1150
|
-
data: event.data,
|
|
1151
|
-
timestamp: Date.now(),
|
|
1152
|
-
}),
|
|
1153
|
-
}),
|
|
1154
|
-
({ event, context }) => {
|
|
1155
|
-
logger$2(`[startWrite] Received startWrite for ${context.entityType} "${context.entityId}"`);
|
|
1156
|
-
logger$2(`[startWrite] Data:`, event.data);
|
|
1157
|
-
},
|
|
1158
|
-
],
|
|
1159
|
-
},
|
|
1160
|
-
},
|
|
1161
|
-
},
|
|
1162
|
-
validating: {
|
|
1163
|
-
entry: ({ context }) => {
|
|
1164
|
-
const msg = `[validating] Entering validating state for ${context.entityType} "${context.entityId}"`;
|
|
1165
|
-
logger$2(msg);
|
|
1166
|
-
logger$2(`[validating] Entity data:`, context.pendingWrite?.data || context.entityData);
|
|
1167
|
-
},
|
|
1168
|
-
invoke: {
|
|
1169
|
-
src: 'validateEntity',
|
|
1170
|
-
input: ({ context }) => {
|
|
1171
|
-
const entityData = context.pendingWrite?.data || context.entityData;
|
|
1172
|
-
logger$2(`[validating] Invoking validateEntity with:`, { entityType: context.entityType, entityData });
|
|
1173
|
-
return {
|
|
1174
|
-
entityType: context.entityType,
|
|
1175
|
-
entityData,
|
|
1176
|
-
};
|
|
1177
|
-
},
|
|
1178
|
-
onDone: [
|
|
1179
|
-
{
|
|
1180
|
-
target: 'writing',
|
|
1181
|
-
guard: ({ event }) => {
|
|
1182
|
-
// The output might be the result directly, or it might be wrapped in an event
|
|
1183
|
-
let result;
|
|
1184
|
-
if (event.output && typeof event.output === 'object' && 'isValid' in event.output) {
|
|
1185
|
-
result = event.output;
|
|
1186
|
-
}
|
|
1187
|
-
else if (event.output && typeof event.output === 'object' && 'type' in event.output) {
|
|
1188
|
-
// If it's wrapped in an event, extract the result
|
|
1189
|
-
result = event.output;
|
|
1190
|
-
}
|
|
1191
|
-
else {
|
|
1192
|
-
console.error(`[validating] Unexpected event.output format! Event:`, event);
|
|
1193
|
-
return false;
|
|
1194
|
-
}
|
|
1195
|
-
if (!result || typeof result.isValid !== 'boolean') {
|
|
1196
|
-
console.error(`[validating] Invalid result format! Result:`, result);
|
|
1197
|
-
return false;
|
|
1198
|
-
}
|
|
1199
|
-
const resultMsg = `[validating] Validation result: isValid=${result.isValid}, errors=${result.errors?.length || 0}`;
|
|
1200
|
-
logger$2(resultMsg);
|
|
1201
|
-
logger$2(`[validating] Validation result:`, result);
|
|
1202
|
-
return result.isValid;
|
|
1203
|
-
},
|
|
1204
|
-
actions: [
|
|
1205
|
-
assign({
|
|
1206
|
-
validationErrors: [],
|
|
1207
|
-
}),
|
|
1208
|
-
({ context }) => {
|
|
1209
|
-
const msg = `[validating] Validation passed, transitioning to writing for ${context.entityType} "${context.entityId}"`;
|
|
1210
|
-
logger$2(msg);
|
|
1211
|
-
},
|
|
1212
|
-
],
|
|
1213
|
-
},
|
|
1214
|
-
{
|
|
1215
|
-
target: 'error',
|
|
1216
|
-
actions: [
|
|
1217
|
-
assign({
|
|
1218
|
-
validationErrors: ({ event }) => {
|
|
1219
|
-
const result = event.output;
|
|
1220
|
-
return result.errors || [];
|
|
1221
|
-
},
|
|
1222
|
-
error: ({ event }) => {
|
|
1223
|
-
const result = event.output;
|
|
1224
|
-
return new Error(`Validation failed: ${result.errors.map(e => e.message).join(', ')}`);
|
|
1225
|
-
},
|
|
1226
|
-
}),
|
|
1227
|
-
({ context, event }) => {
|
|
1228
|
-
const result = event.output;
|
|
1229
|
-
logger$2(`[validating] Validation failed for ${context.entityType} "${context.entityId}":`, result.errors);
|
|
1230
|
-
},
|
|
1231
|
-
],
|
|
1232
|
-
},
|
|
1233
|
-
],
|
|
1234
|
-
onError: {
|
|
1235
|
-
target: 'error',
|
|
1236
|
-
actions: [
|
|
1237
|
-
assign({
|
|
1238
|
-
error: ({ event }) => event.error instanceof Error ? event.error : new Error(String(event.error)),
|
|
1239
|
-
}),
|
|
1240
|
-
({ context, event }) => {
|
|
1241
|
-
const errorMsg = `[validating] Validation error for ${context.entityType} "${context.entityId}": ${event.error}`;
|
|
1242
|
-
logger$2(errorMsg);
|
|
1243
|
-
console.error(errorMsg, event.error); // Always log to console
|
|
1244
|
-
},
|
|
1245
|
-
],
|
|
1246
|
-
},
|
|
1247
|
-
},
|
|
1248
|
-
},
|
|
1249
|
-
writing: {
|
|
1250
|
-
invoke: {
|
|
1251
|
-
src: 'writeToDatabase',
|
|
1252
|
-
input: ({ context }) => {
|
|
1253
|
-
const entityData = context.pendingWrite?.data || context.entityData;
|
|
1254
|
-
logger$2(`[writing] Calling writeToDatabase for ${context.entityType} "${context.entityId}"`);
|
|
1255
|
-
logger$2(`[writing] Entity data:`, entityData);
|
|
1256
|
-
return {
|
|
1257
|
-
entityType: context.entityType,
|
|
1258
|
-
entityId: context.entityId,
|
|
1259
|
-
entityData,
|
|
1260
|
-
};
|
|
1261
|
-
},
|
|
1262
|
-
},
|
|
1263
|
-
on: {
|
|
1264
|
-
writeSuccess: {
|
|
1265
|
-
target: 'success',
|
|
1266
|
-
actions: [
|
|
1267
|
-
assign({
|
|
1268
|
-
pendingWrite: null,
|
|
1269
|
-
entityData: ({ context, event }) => {
|
|
1270
|
-
// Update entityData with any returned data from write
|
|
1271
|
-
return event.output || context.entityData;
|
|
1272
|
-
},
|
|
1273
|
-
}),
|
|
1274
|
-
({ context }) => {
|
|
1275
|
-
logger$2(`[writing] Write successful for ${context.entityType} "${context.entityId}"`);
|
|
1276
|
-
},
|
|
1277
|
-
],
|
|
1278
|
-
},
|
|
1279
|
-
writeError: {
|
|
1280
|
-
target: 'error',
|
|
1281
|
-
actions: [
|
|
1282
|
-
assign({
|
|
1283
|
-
error: ({ event }) => event.error instanceof Error ? event.error : new Error(String(event.error)),
|
|
1284
|
-
}),
|
|
1285
|
-
({ context, event }) => {
|
|
1286
|
-
const errorMsg = `[writing] Write error for ${context.entityType} "${context.entityId}": ${event.error}`;
|
|
1287
|
-
logger$2(errorMsg);
|
|
1288
|
-
// Nothing else reports a failed persist to the caller, so always log to console
|
|
1289
|
-
console.error(errorMsg, event.error);
|
|
1290
|
-
},
|
|
1291
|
-
],
|
|
1292
|
-
},
|
|
1293
|
-
},
|
|
1294
|
-
},
|
|
1295
|
-
success: {
|
|
1296
|
-
after: {
|
|
1297
|
-
2000: { target: 'idle' }, // Auto-reset after 2s
|
|
1298
|
-
},
|
|
1299
|
-
},
|
|
1300
|
-
error: {},
|
|
1301
|
-
},
|
|
1302
|
-
});
|
|
1303
|
-
|
|
1304
|
-
const logger$1 = debug('seedSdk:model:modelMachine');
|
|
1305
|
-
const modelMachine = setup({
|
|
1306
|
-
types: {
|
|
1307
|
-
context: {},
|
|
1308
|
-
input: {},
|
|
1309
|
-
events: {},
|
|
1310
|
-
},
|
|
1311
|
-
actors: {
|
|
1312
|
-
loadOrCreateModel,
|
|
1313
|
-
validateModel,
|
|
1314
|
-
createModelProperties,
|
|
1315
|
-
writeProcessMachine,
|
|
1316
|
-
},
|
|
1317
|
-
guards: {
|
|
1318
|
-
isModelValid: ({ context }) => {
|
|
1319
|
-
return !context._validationErrors || context._validationErrors.length === 0;
|
|
1320
|
-
},
|
|
1321
|
-
},
|
|
1322
|
-
actions: {
|
|
1323
|
-
assignValidationErrors: assign(({ context, event }) => {
|
|
1324
|
-
if (event.type === 'validationError') {
|
|
1325
|
-
return {
|
|
1326
|
-
...context,
|
|
1327
|
-
_validationErrors: event.errors,
|
|
1328
|
-
};
|
|
1329
|
-
}
|
|
1330
|
-
return context;
|
|
1331
|
-
}),
|
|
1332
|
-
},
|
|
1333
|
-
}).createMachine({
|
|
1334
|
-
id: 'model',
|
|
1335
|
-
initial: 'loading',
|
|
1336
|
-
context: ({ input }) => ({
|
|
1337
|
-
id: input.id,
|
|
1338
|
-
_dbId: undefined,
|
|
1339
|
-
modelName: input.modelName,
|
|
1340
|
-
schemaName: input.schemaName,
|
|
1341
|
-
_pendingPropertyDefinitions: input._pendingPropertyDefinitions,
|
|
1342
|
-
_idFromSchema: input._idFromSchema,
|
|
1343
|
-
_isDraft: false,
|
|
1344
|
-
_editedProperties: new Set(),
|
|
1345
|
-
_validationErrors: undefined,
|
|
1346
|
-
writeProcess: undefined,
|
|
1347
|
-
_liveQueryPropertyIds: [], // Initialize empty array
|
|
1348
|
-
}),
|
|
1349
|
-
on: {
|
|
1350
|
-
updateContext: [
|
|
1351
|
-
{
|
|
1352
|
-
// Always update context first (no guard, always runs)
|
|
1353
|
-
actions: [
|
|
1354
|
-
assign(({ context, event }) => {
|
|
1355
|
-
const newContext = { ...context };
|
|
1356
|
-
// Check if this is only updating internal fields
|
|
1357
|
-
const onlyInternalFields = Object.keys(event).every((key) => {
|
|
1358
|
-
return key === 'type' || key.startsWith('_');
|
|
1359
|
-
});
|
|
1360
|
-
// Update the context with new values
|
|
1361
|
-
for (let i = 0; i < Object.keys(event).length; i++) {
|
|
1362
|
-
const key = Object.keys(event)[i];
|
|
1363
|
-
if (key === 'type') {
|
|
1364
|
-
continue;
|
|
1365
|
-
}
|
|
1366
|
-
const eventValue = event[key];
|
|
1367
|
-
// Ensure arrays are properly copied (create new reference)
|
|
1368
|
-
newContext[key] = Array.isArray(eventValue) ? [...eventValue] : eventValue;
|
|
1369
|
-
}
|
|
1370
|
-
// Compare with original values and set _isEdited flag (only for non-internal updates)
|
|
1371
|
-
// Note: properties are not in context - they're computed from ModelProperty instances
|
|
1372
|
-
// Property changes are tracked via ModelProperty._isEdited flags
|
|
1373
|
-
if (!onlyInternalFields && context._originalValues) {
|
|
1374
|
-
const hasChanges = Object.keys(event).some((key) => {
|
|
1375
|
-
if (key === 'type' || key.startsWith('_') || key === 'properties')
|
|
1376
|
-
return false;
|
|
1377
|
-
const newValue = newContext[key];
|
|
1378
|
-
const oldValue = context._originalValues?.[key];
|
|
1379
|
-
// Deep comparison for objects/arrays
|
|
1380
|
-
if (typeof newValue === 'object' && typeof oldValue === 'object') {
|
|
1381
|
-
return JSON.stringify(newValue) !== JSON.stringify(oldValue);
|
|
1382
|
-
}
|
|
1383
|
-
return newValue !== oldValue;
|
|
1384
|
-
});
|
|
1385
|
-
newContext._isEdited = hasChanges;
|
|
1386
|
-
}
|
|
1387
|
-
// Clear validation errors on context update (will be re-validated if needed)
|
|
1388
|
-
newContext._validationErrors = undefined;
|
|
1389
|
-
return newContext;
|
|
1390
|
-
}),
|
|
1391
|
-
// Verify the context was updated and create property instances if needed
|
|
1392
|
-
({ event, self }) => {
|
|
1393
|
-
if (event._liveQueryPropertyIds !== undefined) {
|
|
1394
|
-
const newPropertyIds = event._liveQueryPropertyIds;
|
|
1395
|
-
// The model can be evicted with its schema (Schema.destroy, test cleanup) before the
|
|
1396
|
-
// timer and import below run: then create nothing, and drop lookups an eviction overtakes.
|
|
1397
|
-
const evictionEpoch = currentEvictionEpoch();
|
|
1398
|
-
// Use setTimeout to check after assign has been applied
|
|
1399
|
-
setTimeout(() => {
|
|
1400
|
-
const snapshot = self.getSnapshot();
|
|
1401
|
-
if (snapshot.status === 'stopped')
|
|
1402
|
-
return;
|
|
1403
|
-
// Create ModelProperty instances for any new property IDs
|
|
1404
|
-
if (Array.isArray(newPropertyIds) && newPropertyIds.length > 0) {
|
|
1405
|
-
// Import and create instances asynchronously (fire-and-forget)
|
|
1406
|
-
import('./ModelProperty-i5flwzQG.js').then(({ ModelProperty }) => {
|
|
1407
|
-
if (self.getSnapshot().status === 'stopped')
|
|
1408
|
-
return;
|
|
1409
|
-
const createPromises = newPropertyIds.map(async (propertyFileId) => {
|
|
1410
|
-
try {
|
|
1411
|
-
const property = await ModelProperty.createById(propertyFileId, { evictionEpoch });
|
|
1412
|
-
if (property) {
|
|
1413
|
-
logger$1(`[modelMachine] Created/cached ModelProperty instance for propertyFileId "${propertyFileId}" after _liveQueryPropertyIds update`);
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
catch (error) {
|
|
1417
|
-
logger$1(`[modelMachine] Error creating ModelProperty instance for propertyFileId "${propertyFileId}": ${error}`);
|
|
1418
|
-
}
|
|
1419
|
-
});
|
|
1420
|
-
Promise.all(createPromises).catch((error) => {
|
|
1421
|
-
logger$1(`[modelMachine] Error creating property instances: ${error}`);
|
|
1422
|
-
});
|
|
1423
|
-
}).catch((error) => {
|
|
1424
|
-
logger$1(`[modelMachine] Error importing ModelProperty: ${error}`);
|
|
1425
|
-
});
|
|
1426
|
-
}
|
|
1427
|
-
}, 0);
|
|
1428
|
-
}
|
|
1429
|
-
},
|
|
1430
|
-
],
|
|
1431
|
-
// Don't transition - stay in current state, context will be updated
|
|
1432
|
-
},
|
|
1433
|
-
{
|
|
1434
|
-
// Conditionally trigger validation for non-internal updates
|
|
1435
|
-
guard: ({ event }) => {
|
|
1436
|
-
return !Object.keys(event).every((key) => {
|
|
1437
|
-
return key === 'type' || key.startsWith('_');
|
|
1438
|
-
});
|
|
1439
|
-
},
|
|
1440
|
-
target: '.validating',
|
|
1441
|
-
},
|
|
1442
|
-
],
|
|
1443
|
-
validateModel: {
|
|
1444
|
-
target: '.validating',
|
|
1445
|
-
},
|
|
1446
|
-
validationSuccess: {
|
|
1447
|
-
actions: assign(({ context }) => ({
|
|
1448
|
-
...context,
|
|
1449
|
-
_validationErrors: [],
|
|
1450
|
-
})),
|
|
1451
|
-
},
|
|
1452
|
-
validationError: {
|
|
1453
|
-
actions: 'assignValidationErrors',
|
|
1454
|
-
},
|
|
1455
|
-
initializeOriginalValues: {
|
|
1456
|
-
actions: assign(({ context, event }) => ({
|
|
1457
|
-
...context,
|
|
1458
|
-
_originalValues: event.originalValues,
|
|
1459
|
-
_isEdited: event.isEdited ?? false,
|
|
1460
|
-
_validationErrors: undefined,
|
|
1461
|
-
})),
|
|
1462
|
-
},
|
|
1463
|
-
markAsDraft: {
|
|
1464
|
-
actions: assign(({ context, event }) => ({
|
|
1465
|
-
...context,
|
|
1466
|
-
_isEdited: true,
|
|
1467
|
-
_editedProperties: new Set([
|
|
1468
|
-
...(context._editedProperties || []),
|
|
1469
|
-
event.propertyKey,
|
|
1470
|
-
]),
|
|
1471
|
-
})),
|
|
1472
|
-
},
|
|
1473
|
-
clearDraft: {
|
|
1474
|
-
actions: assign(({ context }) => ({
|
|
1475
|
-
...context,
|
|
1476
|
-
_isEdited: false,
|
|
1477
|
-
_editedProperties: new Set(),
|
|
1478
|
-
_originalValues: {
|
|
1479
|
-
// Properties are not stored in context - they're computed from ModelProperty instances
|
|
1480
|
-
// Properties serialization happens separately when initializing original values
|
|
1481
|
-
},
|
|
1482
|
-
})),
|
|
1483
|
-
},
|
|
1484
|
-
destroyStarted: {
|
|
1485
|
-
actions: assign({ _destroyError: null }),
|
|
1486
|
-
},
|
|
1487
|
-
destroyError: {
|
|
1488
|
-
actions: assign(({ event }) => ({
|
|
1489
|
-
_destroyError: event.error instanceof Error
|
|
1490
|
-
? { message: event.error.message, name: event.error.name }
|
|
1491
|
-
: { message: String(event.error) },
|
|
1492
|
-
})),
|
|
1493
|
-
},
|
|
1494
|
-
},
|
|
1495
|
-
states: {
|
|
1496
|
-
loading: {
|
|
1497
|
-
on: {
|
|
1498
|
-
loadOrCreateModelSuccess: {
|
|
1499
|
-
target: 'idle',
|
|
1500
|
-
actions: assign(({ context, event }) => {
|
|
1501
|
-
const hasPendingProps = !!(context._pendingPropertyDefinitions && Object.keys(context._pendingPropertyDefinitions).length > 0);
|
|
1502
|
-
logger$1(`[loadOrCreateModelSuccess] Preserving _pendingPropertyDefinitions: ${hasPendingProps} (${Object.keys(context._pendingPropertyDefinitions || {}).length} properties)`);
|
|
1503
|
-
// Check if there's a pending model name update (set before this event to avoid validation trigger)
|
|
1504
|
-
const finalModelName = context._pendingModelName || context.modelName;
|
|
1505
|
-
if (context._pendingModelName) {
|
|
1506
|
-
logger$1(`[loadOrCreateModelSuccess] Applying pending model name: "${context.modelName}" -> "${finalModelName}"`);
|
|
1507
|
-
}
|
|
1508
|
-
return {
|
|
1509
|
-
...context,
|
|
1510
|
-
id: event.model.id, // schemaFileId (string)
|
|
1511
|
-
_dbId: event.model._dbId, // Database integer ID
|
|
1512
|
-
_isEdited: event.model._isEdited ?? false, // Load isEdited from database
|
|
1513
|
-
_editedProperties: new Set(),
|
|
1514
|
-
_validationErrors: undefined,
|
|
1515
|
-
// Use final model name (may have been set via _pendingModelName to avoid validation trigger)
|
|
1516
|
-
modelName: finalModelName,
|
|
1517
|
-
// Clear the temporary field
|
|
1518
|
-
_pendingModelName: undefined,
|
|
1519
|
-
// Preserve _pendingPropertyDefinitions if it exists
|
|
1520
|
-
_pendingPropertyDefinitions: context._pendingPropertyDefinitions,
|
|
1521
|
-
// Preserve _liveQueryPropertyIds if provided (set by loadOrCreateModel when loading from DB)
|
|
1522
|
-
_liveQueryPropertyIds: event.model._liveQueryPropertyIds ?? context._liveQueryPropertyIds ?? [],
|
|
1523
|
-
// Preserve conflict detection metadata if provided
|
|
1524
|
-
_loadedAt: event.model._loadedAt,
|
|
1525
|
-
_dbVersion: event.model._dbVersion,
|
|
1526
|
-
_dbUpdatedAt: event.model._dbUpdatedAt,
|
|
1527
|
-
_originalValues: {
|
|
1528
|
-
// Properties are not stored in context - they're computed from ModelProperty instances
|
|
1529
|
-
// Properties will be loaded via liveQuery after model creation
|
|
1530
|
-
},
|
|
1531
|
-
};
|
|
1532
|
-
}),
|
|
1533
|
-
},
|
|
1534
|
-
loadOrCreateModelError: {
|
|
1535
|
-
target: 'error',
|
|
1536
|
-
},
|
|
1537
|
-
},
|
|
1538
|
-
invoke: {
|
|
1539
|
-
src: 'loadOrCreateModel',
|
|
1540
|
-
input: ({ context }) => ({ context }),
|
|
1541
|
-
},
|
|
1542
|
-
},
|
|
1543
|
-
idle: {
|
|
1544
|
-
entry: assign({
|
|
1545
|
-
writeProcess: ({ context, spawn }) => {
|
|
1546
|
-
if (!context.writeProcess && context.id) {
|
|
1547
|
-
logger$1(`[idle entry] Spawning writeProcess for model "${context.modelName}" (${context.id})`);
|
|
1548
|
-
return spawn(writeProcessMachine, {
|
|
1549
|
-
input: {
|
|
1550
|
-
entityType: 'model',
|
|
1551
|
-
entityId: context.id,
|
|
1552
|
-
entityData: {
|
|
1553
|
-
modelName: context.modelName,
|
|
1554
|
-
schemaName: context.schemaName,
|
|
1555
|
-
// Properties are not stored in context - they'll be provided via requestWrite event
|
|
1556
|
-
},
|
|
1557
|
-
},
|
|
1558
|
-
});
|
|
1559
|
-
}
|
|
1560
|
-
return context.writeProcess;
|
|
1561
|
-
},
|
|
1562
|
-
}),
|
|
1563
|
-
on: {
|
|
1564
|
-
refreshProperties: {
|
|
1565
|
-
actions: () => {
|
|
1566
|
-
// This event will be handled by the Model instance's subscription
|
|
1567
|
-
// The Model will call _refreshPropertiesFromDb when it receives this event
|
|
1568
|
-
logger$1(`[idle] refreshProperties event received, will be handled by Model instance`);
|
|
1569
|
-
},
|
|
1570
|
-
},
|
|
1571
|
-
validateModel: {
|
|
1572
|
-
target: 'validating',
|
|
1573
|
-
},
|
|
1574
|
-
reloadFromDb: {
|
|
1575
|
-
target: 'loading',
|
|
1576
|
-
},
|
|
1577
|
-
requestWrite: {
|
|
1578
|
-
actions: ({ context, event }) => {
|
|
1579
|
-
if (context.writeProcess) {
|
|
1580
|
-
logger$1(`[requestWrite] Sending startWrite to writeProcess with data:`, event.data);
|
|
1581
|
-
context.writeProcess.send({
|
|
1582
|
-
type: 'startWrite',
|
|
1583
|
-
data: event.data,
|
|
1584
|
-
});
|
|
1585
|
-
}
|
|
1586
|
-
else {
|
|
1587
|
-
logger$1(`[requestWrite] ERROR: writeProcess not available for model "${context.modelName}" (${context.id})`);
|
|
1588
|
-
}
|
|
1589
|
-
},
|
|
1590
|
-
},
|
|
1591
|
-
writeSuccess: [
|
|
1592
|
-
{
|
|
1593
|
-
guard: ({ context }) => {
|
|
1594
|
-
// If we have pending property definitions, transition to creatingProperties
|
|
1595
|
-
const hasPending = !!(context._pendingPropertyDefinitions && Object.keys(context._pendingPropertyDefinitions).length > 0);
|
|
1596
|
-
logger$1(`[writeSuccess guard] hasPending: ${hasPending}, _pendingPropertyDefinitions keys: ${context._pendingPropertyDefinitions ? Object.keys(context._pendingPropertyDefinitions).length : 0}`);
|
|
1597
|
-
return hasPending;
|
|
1598
|
-
},
|
|
1599
|
-
target: 'creatingProperties',
|
|
1600
|
-
actions: assign(({ context, event }) => {
|
|
1601
|
-
logger$1(`[writeSuccess] Transitioning to creatingProperties for model "${context.modelName}"`);
|
|
1602
|
-
// Update _dbId from write output if available (writeToDatabase returns _dbId, not id)
|
|
1603
|
-
const newContext = { ...context };
|
|
1604
|
-
const dbId = event.output?._dbId ?? event.output?.id;
|
|
1605
|
-
if (dbId != null) {
|
|
1606
|
-
newContext._dbId = dbId;
|
|
1607
|
-
logger$1(`[writeSuccess] Updated _dbId to ${dbId}`);
|
|
1608
|
-
}
|
|
1609
|
-
return newContext;
|
|
1610
|
-
}),
|
|
1611
|
-
},
|
|
1612
|
-
{
|
|
1613
|
-
target: 'idle',
|
|
1614
|
-
actions: assign(({ context, event }) => {
|
|
1615
|
-
// Update _dbId from write output if available (writeToDatabase returns _dbId, not id)
|
|
1616
|
-
const newContext = { ...context };
|
|
1617
|
-
const dbId = event.output?._dbId ?? event.output?.id;
|
|
1618
|
-
if (dbId != null) {
|
|
1619
|
-
newContext._dbId = dbId;
|
|
1620
|
-
}
|
|
1621
|
-
return newContext;
|
|
1622
|
-
}),
|
|
1623
|
-
},
|
|
1624
|
-
],
|
|
1625
|
-
},
|
|
1626
|
-
// No `always: hasValidationErrors -> validating` here: validation is re-run when the context changes
|
|
1627
|
-
// (updateContext) or on request (validateModel). Re-entering while errors exist would loop forever on a
|
|
1628
|
-
// model that is still invalid.
|
|
1629
|
-
},
|
|
1630
|
-
validating: {
|
|
1631
|
-
on: {
|
|
1632
|
-
validationSuccess: {
|
|
1633
|
-
target: 'idle',
|
|
1634
|
-
actions: assign(({ context }) => ({
|
|
1635
|
-
...context,
|
|
1636
|
-
_validationErrors: [],
|
|
1637
|
-
})),
|
|
1638
|
-
},
|
|
1639
|
-
validationError: {
|
|
1640
|
-
target: 'idle',
|
|
1641
|
-
actions: 'assignValidationErrors',
|
|
1642
|
-
},
|
|
1643
|
-
},
|
|
1644
|
-
invoke: {
|
|
1645
|
-
src: 'validateModel',
|
|
1646
|
-
input: ({ context }) => ({ context }),
|
|
1647
|
-
},
|
|
1648
|
-
},
|
|
1649
|
-
creatingProperties: {
|
|
1650
|
-
entry: ({ context }) => {
|
|
1651
|
-
logger$1(`[creatingProperties entry] Starting property creation for model "${context.modelName}" with ${Object.keys(context._pendingPropertyDefinitions || {}).length} properties`);
|
|
1652
|
-
},
|
|
1653
|
-
invoke: {
|
|
1654
|
-
src: 'createModelProperties',
|
|
1655
|
-
input: ({ context }) => {
|
|
1656
|
-
logger$1(`[creatingProperties invoke] Invoking createModelProperties with ${Object.keys(context._pendingPropertyDefinitions || {}).length} property definitions`);
|
|
1657
|
-
return {
|
|
1658
|
-
context,
|
|
1659
|
-
propertyDefinitions: context._pendingPropertyDefinitions || {},
|
|
1660
|
-
};
|
|
1661
|
-
},
|
|
1662
|
-
},
|
|
1663
|
-
on: {
|
|
1664
|
-
createModelPropertiesSuccess: {
|
|
1665
|
-
target: 'idle',
|
|
1666
|
-
actions: [
|
|
1667
|
-
assign(({ context }) => {
|
|
1668
|
-
// Only clear _pendingPropertyDefinitions, preserve everything else including _liveQueryPropertyIds
|
|
1669
|
-
return {
|
|
1670
|
-
...context,
|
|
1671
|
-
_pendingPropertyDefinitions: undefined,
|
|
1672
|
-
};
|
|
1673
|
-
}),
|
|
1674
|
-
({ self }) => {
|
|
1675
|
-
// Trigger property refresh after properties are created
|
|
1676
|
-
// This ensures _liveQueryPropertyIds is updated in Node.js where liveQuery isn't available
|
|
1677
|
-
setTimeout(() => {
|
|
1678
|
-
if (self.getSnapshot().status === 'stopped')
|
|
1679
|
-
return;
|
|
1680
|
-
self.send({ type: 'refreshProperties' });
|
|
1681
|
-
}, 100); // Small delay to ensure properties are written to DB
|
|
1682
|
-
},
|
|
1683
|
-
],
|
|
1684
|
-
},
|
|
1685
|
-
createModelPropertiesError: {
|
|
1686
|
-
target: 'error',
|
|
1687
|
-
actions: ({ event }) => {
|
|
1688
|
-
logger$1('Error creating model properties:', event.error);
|
|
1689
|
-
},
|
|
1690
|
-
},
|
|
1691
|
-
},
|
|
1692
|
-
},
|
|
1693
|
-
error: {},
|
|
1694
|
-
},
|
|
1695
|
-
});
|
|
1696
|
-
|
|
1697
|
-
/**
|
|
1698
|
-
* Error thrown when a conflict is detected between actor context and database
|
|
1699
|
-
*/
|
|
1700
|
-
class ConflictError extends Error {
|
|
1701
|
-
constructor(message, conflict) {
|
|
1702
|
-
super(message);
|
|
1703
|
-
this.name = 'ConflictError';
|
|
1704
|
-
this.conflict = conflict;
|
|
1705
|
-
Object.setPrototypeOf(this, ConflictError.prototype);
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
/**
|
|
1710
|
-
* Set up liveQuery subscription for an entity
|
|
1711
|
-
* This enables cross-instance synchronization (e.g., changes in other tabs/windows)
|
|
1712
|
-
*
|
|
1713
|
-
* @param instance - Entity instance
|
|
1714
|
-
* @param config - LiveQuery configuration
|
|
1715
|
-
*/
|
|
1716
|
-
function setupEntityLiveQuery(instance, config) {
|
|
1717
|
-
const isBrowser = typeof window !== 'undefined';
|
|
1718
|
-
const logger = debug(config.loggerName);
|
|
1719
|
-
// Use a closure variable to track setup state per instance
|
|
1720
|
-
const setupState = { subscriptionSetUp: false };
|
|
1721
|
-
const setupLiveQuery = async (entityId) => {
|
|
1722
|
-
if (setupState.subscriptionSetUp) {
|
|
1723
|
-
return;
|
|
1724
|
-
}
|
|
1725
|
-
setupState.subscriptionSetUp = true;
|
|
1726
|
-
logger(`Setting up liveQuery for entity ID: ${entityId}`);
|
|
1727
|
-
try {
|
|
1728
|
-
// Query initial data if provided (for Node.js compatibility)
|
|
1729
|
-
if (config.queryInitialData) {
|
|
1730
|
-
const initialRows = await config.queryInitialData(entityId);
|
|
1731
|
-
const initialIds = config.extractEntityIds(initialRows);
|
|
1732
|
-
if (initialIds.length > 0) {
|
|
1733
|
-
logger(`Initial query returned ${initialIds.length} entities`);
|
|
1734
|
-
// Create child instances if provided (not for a stopped entity: an unloaded or evicted
|
|
1735
|
-
// parent must not re-cache its children)
|
|
1736
|
-
if (isActorStopped(instance.getService()))
|
|
1737
|
-
return;
|
|
1738
|
-
if (config.createChildInstances) {
|
|
1739
|
-
await config.createChildInstances(initialIds);
|
|
1740
|
-
}
|
|
1741
|
-
// Update context with initial IDs (the queries above can outlive the instance)
|
|
1742
|
-
if (isActorStopped(instance.getService()))
|
|
1743
|
-
return;
|
|
1744
|
-
config.updateContext(instance, initialIds);
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
// Only set up liveQuery subscription in browser environment
|
|
1748
|
-
if (isBrowser) {
|
|
1749
|
-
// buildQuery can be async to allow dynamic imports
|
|
1750
|
-
const queryResult = config.buildQuery(entityId);
|
|
1751
|
-
const query$ = queryResult instanceof Promise ? await queryResult : queryResult;
|
|
1752
|
-
const instanceState = config.instanceState.get(instance);
|
|
1753
|
-
if (!instanceState) {
|
|
1754
|
-
logger('Instance state not found');
|
|
1755
|
-
return;
|
|
1756
|
-
}
|
|
1757
|
-
// Unloaded or destroyed while setup awaited: subscribing now would leak the query.
|
|
1758
|
-
if (isActorStopped(instance.getService()))
|
|
1759
|
-
return;
|
|
1760
|
-
// Subscribe to liveQuery updates
|
|
1761
|
-
const subscription = query$.subscribe({
|
|
1762
|
-
next: async (rows) => {
|
|
1763
|
-
// Check if instance state still exists (hasn't been cleaned up)
|
|
1764
|
-
const currentInstanceState = config.instanceState.get(instance);
|
|
1765
|
-
if (!currentInstanceState) {
|
|
1766
|
-
logger('Instance state was cleaned up, skipping update');
|
|
1767
|
-
return;
|
|
1768
|
-
}
|
|
1769
|
-
logger(`Query returned ${rows.length} rows`);
|
|
1770
|
-
const ids = config.extractEntityIds(rows);
|
|
1771
|
-
// Create child instances if provided (before updating context; not for a stopped entity)
|
|
1772
|
-
if (isActorStopped(instance.getService()))
|
|
1773
|
-
return;
|
|
1774
|
-
if (config.createChildInstances && ids.length > 0) {
|
|
1775
|
-
// Nothing awaits this handler, so a child that fails to load (its row deleted while it
|
|
1776
|
-
// loads, say) would be an unhandled rejection. Skip this result; a later one tries again.
|
|
1777
|
-
try {
|
|
1778
|
-
await config.createChildInstances(ids);
|
|
1779
|
-
}
|
|
1780
|
-
catch (error) {
|
|
1781
|
-
logger(`createChildInstances failed: ${error}`);
|
|
1782
|
-
return;
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
// Update context with new IDs
|
|
1786
|
-
if (isActorStopped(instance.getService()))
|
|
1787
|
-
return;
|
|
1788
|
-
config.updateContext(instance, ids);
|
|
1789
|
-
},
|
|
1790
|
-
error: (error) => {
|
|
1791
|
-
logger(`LiveQuery error: ${error}`);
|
|
1792
|
-
},
|
|
1793
|
-
});
|
|
1794
|
-
instanceState.liveQuerySubscription = subscription;
|
|
1795
|
-
logger(`LiveQuery subscription set up for entity ID: ${entityId}`);
|
|
1796
|
-
}
|
|
1797
|
-
else {
|
|
1798
|
-
logger('Skipping liveQuery subscription in Node.js environment');
|
|
1799
|
-
}
|
|
1800
|
-
}
|
|
1801
|
-
catch (error) {
|
|
1802
|
-
logger(`Error setting up subscription: ${error}`);
|
|
1803
|
-
setupState.subscriptionSetUp = false; // Reset on error so we can retry
|
|
1804
|
-
}
|
|
1805
|
-
};
|
|
1806
|
-
// The entity's DB row can be written after the entity goes idle (e.g. a schema import writes its
|
|
1807
|
-
// models' rows after the Model instances loaded from the schema file). Nothing changes the
|
|
1808
|
-
// entity's snapshot then, so an idle entity's id is also retried on a timer until found or the
|
|
1809
|
-
// actor stops. Only while idle and not writing (canRetryEntityId): a busy entity's own snapshots
|
|
1810
|
-
// drive the lookup, and finding its row mid-write handed a Model its _dbId before its write (and
|
|
1811
|
-
// its properties' rows) had finished.
|
|
1812
|
-
let retryIndex = 0;
|
|
1813
|
-
let retryTimer;
|
|
1814
|
-
const scheduleRetry = () => {
|
|
1815
|
-
if (retryTimer !== undefined || retryIndex >= ENTITY_ID_RETRY_DELAYS_MS.length)
|
|
1816
|
-
return;
|
|
1817
|
-
retryTimer = setTimeout(() => {
|
|
1818
|
-
retryTimer = undefined;
|
|
1819
|
-
// Busy: its next snapshot retries
|
|
1820
|
-
if (instance.getService().getSnapshot().value !== 'idle')
|
|
1821
|
-
return;
|
|
1822
|
-
if (config.canRetryEntityId && !config.canRetryEntityId(instance))
|
|
1823
|
-
return;
|
|
1824
|
-
retryIndex++;
|
|
1825
|
-
void trySetup();
|
|
1826
|
-
}, ENTITY_ID_RETRY_DELAYS_MS[retryIndex]);
|
|
1827
|
-
};
|
|
1828
|
-
const trySetup = async () => {
|
|
1829
|
-
if (setupState.subscriptionSetUp || isActorStopped(instance.getService()))
|
|
1830
|
-
return;
|
|
1831
|
-
if (config.isReady && !config.isReady(instance)) {
|
|
1832
|
-
if (config.waitForReady) {
|
|
1833
|
-
await config.waitForReady(instance);
|
|
1834
|
-
}
|
|
1835
|
-
else {
|
|
1836
|
-
return; // Not ready yet, will retry on next snapshot
|
|
1837
|
-
}
|
|
1838
|
-
}
|
|
1839
|
-
let entityId;
|
|
1840
|
-
try {
|
|
1841
|
-
entityId = await config.getEntityId(instance);
|
|
1842
|
-
}
|
|
1843
|
-
catch (error) {
|
|
1844
|
-
logger(`Error getting entity ID: ${error}`);
|
|
1845
|
-
}
|
|
1846
|
-
if (!entityId) {
|
|
1847
|
-
scheduleRetry();
|
|
1848
|
-
return;
|
|
1849
|
-
}
|
|
1850
|
-
// Once we have entity ID, set up the liveQuery subscription (only once)
|
|
1851
|
-
if (!setupState.subscriptionSetUp) {
|
|
1852
|
-
await setupLiveQuery(entityId);
|
|
1853
|
-
}
|
|
1854
|
-
if (setupState.subscriptionSetUp) {
|
|
1855
|
-
setupSubscription.unsubscribe();
|
|
1856
|
-
if (retryTimer !== undefined)
|
|
1857
|
-
clearTimeout(retryTimer);
|
|
1858
|
-
}
|
|
1859
|
-
};
|
|
1860
|
-
// Set up liveQuery subscription as soon as we have entity ID
|
|
1861
|
-
const setupSubscription = instance.getService().subscribe({
|
|
1862
|
-
next: () => {
|
|
1863
|
-
void trySetup();
|
|
1864
|
-
},
|
|
1865
|
-
complete: () => {
|
|
1866
|
-
if (retryTimer !== undefined)
|
|
1867
|
-
clearTimeout(retryTimer);
|
|
1868
|
-
},
|
|
1869
|
-
});
|
|
1870
|
-
// Also check current state immediately in case entity ID is already available
|
|
1871
|
-
void trySetup();
|
|
1872
|
-
}
|
|
1873
|
-
/** Backoff for retrying an entity's id when its DB row isn't there yet (~60s in total). */
|
|
1874
|
-
const ENTITY_ID_RETRY_DELAYS_MS = [50, 100, 200, 400, 800, 1600, ...Array(28).fill(2000)];
|
|
1875
|
-
|
|
1876
|
-
/**
|
|
1877
|
-
* Returns model rows (id, name, schemaFileId, isEdited) for building Model instances.
|
|
1878
|
-
* When schemaIdentifier is provided, only returns models for that schema (join models -> model_schemas -> schemas).
|
|
1879
|
-
* schemaIdentifier can be either schema name or schema file ID.
|
|
1880
|
-
*/
|
|
1881
|
-
const getModelsData = async (schemaIdentifier) => {
|
|
1882
|
-
const appDb = BaseDb.getAppDb();
|
|
1883
|
-
if (!appDb) {
|
|
1884
|
-
return [];
|
|
1885
|
-
}
|
|
1886
|
-
if (schemaIdentifier !== undefined && schemaIdentifier !== '') {
|
|
1887
|
-
const runQuery = () => appDb
|
|
1888
|
-
.select({
|
|
1889
|
-
id: models.id,
|
|
1890
|
-
name: models.name,
|
|
1891
|
-
schemaFileId: models.schemaFileId,
|
|
1892
|
-
isEdited: models.isEdited,
|
|
1893
|
-
})
|
|
1894
|
-
.from(models)
|
|
1895
|
-
.innerJoin(modelSchemas, eq(models.id, modelSchemas.modelId))
|
|
1896
|
-
.innerJoin(schemas, eq(modelSchemas.schemaId, schemas.id))
|
|
1897
|
-
.where(or(eq(schemas.name, schemaIdentifier), eq(schemas.schemaFileId, schemaIdentifier)));
|
|
1898
|
-
let rows = await runQuery();
|
|
1899
|
-
// Retry when we get 0 rows (read-after-write consistency; SQLocal/async may not see the write immediately).
|
|
1900
|
-
for (let i = 0; i < 4 && rows.length === 0; i++) {
|
|
1901
|
-
await new Promise((r) => setTimeout(r, 150));
|
|
1902
|
-
rows = await runQuery();
|
|
1903
|
-
}
|
|
1904
|
-
return rows;
|
|
1905
|
-
}
|
|
1906
|
-
const rows = await appDb
|
|
1907
|
-
.select({
|
|
1908
|
-
id: models.id,
|
|
1909
|
-
name: models.name,
|
|
1910
|
-
schemaFileId: models.schemaFileId,
|
|
1911
|
-
isEdited: models.isEdited,
|
|
1912
|
-
})
|
|
1913
|
-
.from(models);
|
|
1914
|
-
return rows;
|
|
1915
|
-
};
|
|
1916
|
-
|
|
1917
|
-
/**
|
|
1918
|
-
* Errors from draft saves that assignment starts in the background (schema/model renames and
|
|
1919
|
-
* tracked-property edits). Nothing awaits those saves, so the error is kept for `lastSaveError`
|
|
1920
|
-
* instead of only being logged. Keyed by the instance's service, which the instance and its
|
|
1921
|
-
* proxy share.
|
|
1922
|
-
*/
|
|
1923
|
-
const draftSaveErrors = new WeakMap();
|
|
1924
|
-
function getDraftSaveError(service) {
|
|
1925
|
-
return draftSaveErrors.get(service);
|
|
1926
|
-
}
|
|
1927
|
-
/** Run a background draft save, recording its failure (or clearing an earlier one on success). */
|
|
1928
|
-
function trackDraftSave(service, save, label) {
|
|
1929
|
-
save.then(() => {
|
|
1930
|
-
draftSaveErrors.delete(service);
|
|
1931
|
-
}, (error) => {
|
|
1932
|
-
const recorded = error instanceof Error ? error : new Error(String(error));
|
|
1933
|
-
draftSaveErrors.set(service, recorded);
|
|
1934
|
-
console.warn(`[seed] ${label}: draft save failed (see lastSaveError):`, recorded.message);
|
|
1935
|
-
});
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
const logger = debug('seedSdk:model:Model');
|
|
1939
|
-
const saveDraftLogger = debug('seedSdk:model:saveDraftToDb');
|
|
1940
|
-
// Lazy import cache for ModelProperty to avoid circular dependency
|
|
1941
|
-
// Eagerly start loading to minimize delay on first access
|
|
1942
|
-
let ModelPropertyClass = null;
|
|
1943
|
-
const modelPropertyImportPromise = import('./ModelProperty-i5flwzQG.js')
|
|
1944
|
-
.then(module => {
|
|
1945
|
-
ModelPropertyClass = module.ModelProperty;
|
|
1946
|
-
return ModelPropertyClass;
|
|
1947
|
-
})
|
|
1948
|
-
.catch(() => {
|
|
1949
|
-
// If import fails, ModelPropertyClass remains null
|
|
1950
|
-
return null;
|
|
1951
|
-
});
|
|
1952
|
-
function getModelProperty() {
|
|
1953
|
-
// Return cached class if available (synchronous access)
|
|
1954
|
-
// Note: On first access before import completes, this will return null
|
|
1955
|
-
// and the getter will return empty array, which matches the original behavior
|
|
1956
|
-
return ModelPropertyClass;
|
|
1957
|
-
}
|
|
1958
|
-
// Mutable state per Model instance
|
|
1959
|
-
const modelInstanceState = new ActorKeyedState();
|
|
1960
|
-
// Settles once the properties passed to Model.create exist, or rejects when they can't be created.
|
|
1961
|
-
// The model reaches idle before its write, so idle alone doesn't mean they exist yet. Keyed by the
|
|
1962
|
-
// actor because callers hold the proxy, not the instance.
|
|
1963
|
-
const pendingPropertyCreation = new WeakMap();
|
|
1964
|
-
// Define tracked properties for the Proxy
|
|
1965
|
-
// These properties will be read from/written to the actor context
|
|
1966
|
-
const TRACKED_PROPERTIES = [
|
|
1967
|
-
'id', // schemaFileId (string) - public ID
|
|
1968
|
-
'modelName',
|
|
1969
|
-
'schemaName',
|
|
1970
|
-
];
|
|
1971
|
-
class Model {
|
|
1972
|
-
static trackPendingWrite(modelFileId, schemaId) {
|
|
1973
|
-
this.pendingWrites.set(modelFileId, {
|
|
1974
|
-
modelFileId,
|
|
1975
|
-
schemaId,
|
|
1976
|
-
status: 'pending',
|
|
1977
|
-
timestamp: Date.now(),
|
|
1978
|
-
});
|
|
1979
|
-
}
|
|
1980
|
-
static getPendingModelIds(schemaId) {
|
|
1981
|
-
return Array.from(this.pendingWrites.entries())
|
|
1982
|
-
.filter(([_, write]) => write.schemaId === schemaId && write.status !== 'error')
|
|
1983
|
-
.map(([modelFileId]) => modelFileId);
|
|
1984
|
-
}
|
|
1985
|
-
// 'name' is available as an alias for 'modelName' via the proxy
|
|
1986
|
-
constructor(modelName, schemaName, id, // schemaFileId (string) - public ID
|
|
1987
|
-
initialContext, idFromSchema // true when id was provided via options (schema model), not generated
|
|
1988
|
-
) {
|
|
1989
|
-
const serviceInput = {
|
|
1990
|
-
modelName,
|
|
1991
|
-
schemaName,
|
|
1992
|
-
...(id ? { id } : {}),
|
|
1993
|
-
...(initialContext?._pendingPropertyDefinitions ? { _pendingPropertyDefinitions: initialContext._pendingPropertyDefinitions } : {}),
|
|
1994
|
-
_idFromSchema: !!idFromSchema,
|
|
1995
|
-
};
|
|
1996
|
-
this._service = createActor(modelMachine, {
|
|
1997
|
-
input: serviceInput,
|
|
1998
|
-
});
|
|
1999
|
-
this._service.start();
|
|
2000
|
-
// Initialize instance state in WeakMap
|
|
2001
|
-
modelInstanceState.set(this, {
|
|
2002
|
-
liveQuerySubscription: null,
|
|
2003
|
-
});
|
|
2004
|
-
// Set up liveQuery subscription for cross-instance property updates
|
|
2005
|
-
this._setupLiveQuerySubscription();
|
|
2006
|
-
// Subscribe to refresh properties after they're created (for Node.js where liveQuery isn't available)
|
|
2007
|
-
let previousState;
|
|
2008
|
-
this._service.subscribe((snapshot) => {
|
|
2009
|
-
// Check if we just transitioned to idle from creatingProperties state
|
|
2010
|
-
if (snapshot.value === 'idle' && previousState === 'creatingProperties') {
|
|
2011
|
-
// Small delay to ensure properties are written to DB
|
|
2012
|
-
setTimeout(() => {
|
|
2013
|
-
this._refreshPropertiesFromDb().catch((error) => {
|
|
2014
|
-
logger(`[Model constructor] Error refreshing properties: ${error}`);
|
|
2015
|
-
});
|
|
2016
|
-
}, 200);
|
|
2017
|
-
}
|
|
2018
|
-
previousState = snapshot.value;
|
|
2019
|
-
});
|
|
2020
|
-
// Note: Property getters/setters are now handled by the Proxy in create()
|
|
2021
|
-
}
|
|
2022
|
-
/**
|
|
2023
|
-
* Resolves once the model is idle and any properties passed to Model.create exist.
|
|
2024
|
-
* Rejects if it errors, times out, or its properties can't be created.
|
|
2025
|
-
*/
|
|
2026
|
-
static async waitUntilReady(instance, timeout) {
|
|
2027
|
-
const startedAt = Date.now();
|
|
2028
|
-
await waitForEntityIdle(instance, { timeout });
|
|
2029
|
-
const propertyCreation = pendingPropertyCreation.get(instance.getService());
|
|
2030
|
-
if (!propertyCreation)
|
|
2031
|
-
return instance;
|
|
2032
|
-
const remaining = Math.max(0, timeout - (Date.now() - startedAt));
|
|
2033
|
-
let timer;
|
|
2034
|
-
try {
|
|
2035
|
-
await Promise.race([
|
|
2036
|
-
propertyCreation,
|
|
2037
|
-
new Promise((_, reject) => {
|
|
2038
|
-
timer = setTimeout(() => {
|
|
2039
|
-
const modelName = instance.getService().getSnapshot().context.modelName;
|
|
2040
|
-
reject(new Error(`Model "${modelName}" did not finish creating its properties within ${timeout}ms`));
|
|
2041
|
-
}, remaining);
|
|
2042
|
-
}),
|
|
2043
|
-
]);
|
|
2044
|
-
}
|
|
2045
|
-
finally {
|
|
2046
|
-
clearTimeout(timer);
|
|
2047
|
-
}
|
|
2048
|
-
return instance;
|
|
2049
|
-
}
|
|
2050
|
-
/**
|
|
2051
|
-
* Gives an idle, valid model the row someone else wrote for its id, the way its own write would
|
|
2052
|
-
* (writeSuccess with the row's id, which goes on to create its properties from their rows).
|
|
2053
|
-
* Returns false when there is no such row, so the model writes it.
|
|
2054
|
-
*/
|
|
2055
|
-
static async adoptPersistedRow(instance) {
|
|
2056
|
-
const snapshot = instance.getService().getSnapshot();
|
|
2057
|
-
if (snapshot.value !== 'idle' || snapshot.context._validationErrors?.length || !snapshot.context.id) {
|
|
2058
|
-
return false;
|
|
2059
|
-
}
|
|
2060
|
-
const db = BaseDb.getAppDb();
|
|
2061
|
-
if (!db)
|
|
2062
|
-
return false;
|
|
2063
|
-
const [row] = await db
|
|
2064
|
-
.select({ id: models.id })
|
|
2065
|
-
.from(models)
|
|
2066
|
-
.where(eq(models.schemaFileId, snapshot.context.id))
|
|
2067
|
-
.limit(1);
|
|
2068
|
-
if (!row || isActorStopped(instance.getService()))
|
|
2069
|
-
return false;
|
|
2070
|
-
logger(`Model "${snapshot.context.modelName}" adopts model row ${row.id} written by its schema import`);
|
|
2071
|
-
instance.getService().send({ type: 'writeSuccess', output: { _dbId: row.id } });
|
|
2072
|
-
return true;
|
|
2073
|
-
}
|
|
2074
|
-
/**
|
|
2075
|
-
* True when a name-cached instance belongs to a different model than the requested modelFileId.
|
|
2076
|
-
* A schema re-imported under the same name can give its models new ids while the previous
|
|
2077
|
-
* instances are still cached by name; Model.create must not hand those back for the new id.
|
|
2078
|
-
*/
|
|
2079
|
-
static isOtherModelId(cachedId, requestedId) {
|
|
2080
|
-
return !!requestedId && !!cachedId && cachedId !== requestedId;
|
|
2081
|
-
}
|
|
2082
|
-
/**
|
|
2083
|
-
* Find a unique model name by checking for duplicates (case-insensitive) in the cache
|
|
2084
|
-
* If duplicates are found, appends an incrementing number to make it unique
|
|
2085
|
-
*
|
|
2086
|
-
* @param modelName - The desired model name
|
|
2087
|
-
* @param schemaName - The schema name
|
|
2088
|
-
* @param skipAllChecks - If true, skip all duplicate checks and return original name (used when creating schema models to preserve original names)
|
|
2089
|
-
* @returns A unique model name
|
|
2090
|
-
*/
|
|
2091
|
-
static findUniqueModelName(modelName, schemaName, skipAllChecks = false) {
|
|
2092
|
-
// If skipAllChecks is true (schema models), preserve original name
|
|
2093
|
-
if (skipAllChecks) {
|
|
2094
|
-
return modelName;
|
|
2095
|
-
}
|
|
2096
|
-
const lowerModelName = modelName.toLowerCase();
|
|
2097
|
-
// Check all cached models for this schema
|
|
2098
|
-
const existingNames = new Set();
|
|
2099
|
-
const existingNumbers = new Set();
|
|
2100
|
-
// Check name-based cache
|
|
2101
|
-
for (const nameKey of this.instanceCacheByName.keys()) {
|
|
2102
|
-
const [cachedSchemaName, cachedModelName] = nameKey.split(':', 2);
|
|
2103
|
-
if (cachedSchemaName === schemaName && cachedModelName) {
|
|
2104
|
-
const lowerCachedName = cachedModelName.toLowerCase();
|
|
2105
|
-
existingNames.add(lowerCachedName);
|
|
2106
|
-
// If it matches the base name (case-insensitive), check if it has a number suffix
|
|
2107
|
-
if (lowerCachedName === lowerModelName) {
|
|
2108
|
-
existingNumbers.add(0); // Base name exists
|
|
2109
|
-
}
|
|
2110
|
-
else if (lowerCachedName.startsWith(lowerModelName + ' ')) {
|
|
2111
|
-
// Check if it's the base name followed by a space and a number
|
|
2112
|
-
const suffix = lowerCachedName.slice(lowerModelName.length + 1);
|
|
2113
|
-
const number = parseInt(suffix, 10);
|
|
2114
|
-
if (!isNaN(number) && suffix === number.toString()) {
|
|
2115
|
-
existingNumbers.add(number);
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
}
|
|
2119
|
-
}
|
|
2120
|
-
// Check legacy cache
|
|
2121
|
-
for (const [nameKey] of this.instanceCache.keys()) {
|
|
2122
|
-
const [cachedSchemaName, cachedModelName] = nameKey.split(':', 2);
|
|
2123
|
-
if (cachedSchemaName === schemaName && cachedModelName) {
|
|
2124
|
-
const lowerCachedName = cachedModelName.toLowerCase();
|
|
2125
|
-
existingNames.add(lowerCachedName);
|
|
2126
|
-
// If it matches the base name (case-insensitive), check if it has a number suffix
|
|
2127
|
-
if (lowerCachedName === lowerModelName) {
|
|
2128
|
-
existingNumbers.add(0); // Base name exists
|
|
2129
|
-
}
|
|
2130
|
-
else if (lowerCachedName.startsWith(lowerModelName + ' ')) {
|
|
2131
|
-
// Check if it's the base name followed by a space and a number
|
|
2132
|
-
const suffix = lowerCachedName.slice(lowerModelName.length + 1);
|
|
2133
|
-
const number = parseInt(suffix, 10);
|
|
2134
|
-
if (!isNaN(number) && suffix === number.toString()) {
|
|
2135
|
-
existingNumbers.add(number);
|
|
2136
|
-
}
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
|
-
}
|
|
2140
|
-
// If no duplicates found (no base name match and no numbered variants), return original name
|
|
2141
|
-
if (existingNumbers.size === 0) {
|
|
2142
|
-
return modelName;
|
|
2143
|
-
}
|
|
2144
|
-
// Find the next available number, starting from 1
|
|
2145
|
-
let nextNumber = 1;
|
|
2146
|
-
while (existingNumbers.has(nextNumber)) {
|
|
2147
|
-
nextNumber++;
|
|
2148
|
-
}
|
|
2149
|
-
// Return the name with the number appended
|
|
2150
|
-
return `${modelName} ${nextNumber}`;
|
|
2151
|
-
}
|
|
2152
|
-
static create(modelName, schemaNameOrSchema, // Schema type - using any to avoid circular dependency
|
|
2153
|
-
options) {
|
|
2154
|
-
if (!modelName) {
|
|
2155
|
-
throw new Error('Model name is required');
|
|
2156
|
-
}
|
|
2157
|
-
const waitForReady = options?.waitForReady !== false;
|
|
2158
|
-
const readyTimeout = options?.readyTimeout ?? DEFAULT_READY_TIMEOUT_MS;
|
|
2159
|
-
const holds = options?.hold === false ? 0 : 1;
|
|
2160
|
-
// Extract schema name and instance
|
|
2161
|
-
let schemaName;
|
|
2162
|
-
let schemaInstance; // Using any to avoid circular dependency with Schema
|
|
2163
|
-
if (typeof schemaNameOrSchema === 'string') {
|
|
2164
|
-
schemaName = schemaNameOrSchema;
|
|
2165
|
-
}
|
|
2166
|
-
else {
|
|
2167
|
-
schemaInstance = schemaNameOrSchema;
|
|
2168
|
-
schemaName = schemaInstance.schemaName;
|
|
2169
|
-
}
|
|
2170
|
-
if (!schemaName) {
|
|
2171
|
-
throw new Error('Schema name is required');
|
|
2172
|
-
}
|
|
2173
|
-
// Support both modelFileId (documented) and id (backward compatibility)
|
|
2174
|
-
const id = options?.modelFileId || options?.id; // schemaFileId (string) - public ID
|
|
2175
|
-
const registerWithSchema = options?.registerWithSchema !== false && schemaInstance !== undefined;
|
|
2176
|
-
// Step 1: Check ID-based cache first (if ID provided)
|
|
2177
|
-
const cachedById = id ? this.instanceCacheById.get(id) : undefined;
|
|
2178
|
-
if (cachedById) {
|
|
2179
|
-
cachedById.refCount += holds;
|
|
2180
|
-
const { instance } = cachedById;
|
|
2181
|
-
logger(`Model.create: Found instance in ID cache for "${modelName}" (ID: ${id})`);
|
|
2182
|
-
if (!waitForReady)
|
|
2183
|
-
return instance;
|
|
2184
|
-
return this.waitUntilReady(instance, readyTimeout);
|
|
2185
|
-
}
|
|
2186
|
-
// Step 2: Check for duplicate names and generate unique name if needed (before checking cache)
|
|
2187
|
-
// This allows us to create new models with renamed versions when duplicates exist
|
|
2188
|
-
// Skip all duplicate checks if modelFileId is provided (schema models should preserve original names)
|
|
2189
|
-
const skipAllChecks = !!id; // If id (modelFileId) is provided, this is a schema model - skip schema check to preserve name
|
|
2190
|
-
const uniqueModelName = this.findUniqueModelName(modelName, schemaName, skipAllChecks);
|
|
2191
|
-
// Step 3: Check name-based index with ORIGINAL name first (before using unique name)
|
|
2192
|
-
// Only return existing instance if the unique name matches the original (no rename needed)
|
|
2193
|
-
// If a rename is needed, we'll create a new model with the unique name
|
|
2194
|
-
let resolvedId = id;
|
|
2195
|
-
const originalNameKey = `${schemaName}:${modelName}`;
|
|
2196
|
-
// Step 3: When we have no id (runtime create), do not return from name cache. Each create('My Model')
|
|
2197
|
-
// must get a new instance with an incremented name (My Model, My Model 1, ...). If the only cached
|
|
2198
|
-
// instance with this name is the schema model (_idFromSchema), ensure we fall through to create new.
|
|
2199
|
-
if (!resolvedId) {
|
|
2200
|
-
const cachedId = this.instanceCacheByName.get(originalNameKey);
|
|
2201
|
-
if (cachedId && this.instanceCacheById.has(cachedId)) {
|
|
2202
|
-
const { instance } = this.instanceCacheById.get(cachedId);
|
|
2203
|
-
const ctx = instance._getSnapshotContext();
|
|
2204
|
-
if (ctx._idFromSchema) {
|
|
2205
|
-
resolvedId = undefined;
|
|
2206
|
-
}
|
|
2207
|
-
// Never return by name when !id (runtime create); fall through to create new with unique name
|
|
2208
|
-
}
|
|
2209
|
-
}
|
|
2210
|
-
// Step 4: Check legacy cache with original name. Skip when !id (runtime create) so duplicate
|
|
2211
|
-
// create('My Model') calls get new instances with incremented names.
|
|
2212
|
-
if (id && uniqueModelName === modelName) {
|
|
2213
|
-
const legacyKey = `${schemaName}:${modelName}`;
|
|
2214
|
-
const legacyEntry = this.instanceCache.get(legacyKey);
|
|
2215
|
-
if (legacyEntry) {
|
|
2216
|
-
const { instance } = legacyEntry;
|
|
2217
|
-
const ctx = instance._getSnapshotContext();
|
|
2218
|
-
if (!ctx._idFromSchema && !this.isOtherModelId(ctx.id, id)) {
|
|
2219
|
-
legacyEntry.refCount += holds;
|
|
2220
|
-
logger(`Model.create: Found instance in legacy cache for "${modelName}"`);
|
|
2221
|
-
const existingId = instance._getSnapshotContext().id;
|
|
2222
|
-
if (existingId) {
|
|
2223
|
-
this.instanceCacheById.set(existingId, legacyEntry);
|
|
2224
|
-
this.instanceCacheByName.set(legacyKey, existingId);
|
|
2225
|
-
}
|
|
2226
|
-
if (!waitForReady)
|
|
2227
|
-
return instance;
|
|
2228
|
-
return this.waitUntilReady(instance, readyTimeout);
|
|
2229
|
-
}
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
// Step 5: When a rename is needed (uniqueModelName !== modelName), do NOT return a cached
|
|
2233
|
-
// instance. The only cached instance with the original name at this point is the
|
|
2234
|
-
// schema-defined model (created with modelFileId); returning it would give the user the
|
|
2235
|
-
// schema model instead of a new runtime model with the unique name (e.g. "New model 1").
|
|
2236
|
-
if (uniqueModelName !== modelName) {
|
|
2237
|
-
logger(`Model.create: Found duplicate name "${modelName}" in schema "${schemaName}", using unique name "${uniqueModelName}"`);
|
|
2238
|
-
}
|
|
2239
|
-
// Step 6a: Generate ID now when needed so we can use a placeholder cache key for runtime creates
|
|
2240
|
-
if (!resolvedId) {
|
|
2241
|
-
resolvedId = generateId();
|
|
2242
|
-
logger(`Model.create: Generated new id (schemaFileId) "${resolvedId}" for model "${modelName}"`);
|
|
2243
|
-
}
|
|
2244
|
-
// Create name-based cache key. When no id (runtime create), use a placeholder so we don't
|
|
2245
|
-
// overwrite the schema model's entry (e.g. "New model"); loadOrCreateModel will replace it with the final name.
|
|
2246
|
-
const nameKey = id
|
|
2247
|
-
? `${schemaName}:${uniqueModelName}`
|
|
2248
|
-
: `${schemaName}:__pending__${resolvedId}`;
|
|
2249
|
-
// Step 6: Check legacy cache with unique name (backward compatibility during migration)
|
|
2250
|
-
// This is a fallback in case an instance was cached with a unique name
|
|
2251
|
-
if (this.instanceCache.has(nameKey) &&
|
|
2252
|
-
!this.isOtherModelId(this.instanceCache.get(nameKey).instance._getSnapshotContext().id, id)) {
|
|
2253
|
-
const legacyEntry = this.instanceCache.get(nameKey);
|
|
2254
|
-
legacyEntry.refCount += holds;
|
|
2255
|
-
const { instance } = legacyEntry;
|
|
2256
|
-
logger(`Model.create: Found instance in legacy cache for "${modelName}"`);
|
|
2257
|
-
// Migrate to new cache structure
|
|
2258
|
-
const context = instance._getSnapshotContext();
|
|
2259
|
-
const existingId = context.id; // id is now the schemaFileId (string)
|
|
2260
|
-
if (existingId) {
|
|
2261
|
-
this.instanceCacheById.set(existingId, legacyEntry);
|
|
2262
|
-
this.instanceCacheByName.set(nameKey, existingId);
|
|
2263
|
-
}
|
|
2264
|
-
if (!waitForReady)
|
|
2265
|
-
return instance;
|
|
2266
|
-
return this.waitUntilReady(instance, readyTimeout);
|
|
2267
|
-
}
|
|
2268
|
-
// Step 7: Create new instance with id in initial context
|
|
2269
|
-
// This ensures loadOrCreateModel sees the ID immediately
|
|
2270
|
-
// Pass _pendingPropertyDefinitions in initial context to avoid race condition with loadOrCreateModel
|
|
2271
|
-
// This is the proper XState way - include it in the initial input so it's available from the start
|
|
2272
|
-
const initialContext = options?.properties
|
|
2273
|
-
? { _pendingPropertyDefinitions: JSON.parse(JSON.stringify(options.properties)) }
|
|
2274
|
-
: undefined;
|
|
2275
|
-
// Use the unique model name for the instance (idFromSchema: true when id was provided, so schema model is not renamed)
|
|
2276
|
-
const newInstance = new this(uniqueModelName, schemaName, resolvedId, initialContext, !!id);
|
|
2277
|
-
if (options?.properties) {
|
|
2278
|
-
logger(`[Model.create] Created instance with _pendingPropertyDefinitions in initial context (${Object.keys(options.properties).length} properties) for model "${uniqueModelName}"`);
|
|
2279
|
-
}
|
|
2280
|
-
// Called by the write trigger below when the properties will never be created
|
|
2281
|
-
let failPropertyCreation = () => { };
|
|
2282
|
-
if (options?.properties && Object.keys(options.properties).length > 0) {
|
|
2283
|
-
const service = newInstance.getService();
|
|
2284
|
-
const propertyCreation = new Promise((resolve, reject) => {
|
|
2285
|
-
const subscription = service.subscribe({
|
|
2286
|
-
next: (snapshot) => {
|
|
2287
|
-
if (snapshot.value === 'idle' && !snapshot.context._pendingPropertyDefinitions) {
|
|
2288
|
-
subscription.unsubscribe();
|
|
2289
|
-
resolve();
|
|
2290
|
-
}
|
|
2291
|
-
else if (snapshot.value === 'error') {
|
|
2292
|
-
failPropertyCreation('the model is in an error state');
|
|
2293
|
-
}
|
|
2294
|
-
},
|
|
2295
|
-
complete: () => failPropertyCreation('the model was unloaded'),
|
|
2296
|
-
});
|
|
2297
|
-
failPropertyCreation = (reason) => {
|
|
2298
|
-
subscription.unsubscribe();
|
|
2299
|
-
reject(new Error(`Model "${service.getSnapshot().context.modelName}" could not create its properties: ${reason}`));
|
|
2300
|
-
};
|
|
2301
|
-
});
|
|
2302
|
-
// Only waitUntilReady reports this; without waitForReady nobody awaits it
|
|
2303
|
-
propertyCreation.catch(() => { });
|
|
2304
|
-
pendingPropertyCreation.set(service, propertyCreation);
|
|
2305
|
-
}
|
|
2306
|
-
// Step 7: Set initial model data if provided
|
|
2307
|
-
// Note: description is not supported - JSON files can have it but we ignore it at runtime
|
|
2308
|
-
if (options) {
|
|
2309
|
-
const updates = {};
|
|
2310
|
-
if (Object.keys(updates).length > 0) {
|
|
2311
|
-
newInstance._service.send({
|
|
2312
|
-
type: 'updateContext',
|
|
2313
|
-
...updates,
|
|
2314
|
-
});
|
|
2315
|
-
// Initialize original values
|
|
2316
|
-
// Note: properties are not stored in context, they're computed from ModelProperty instances
|
|
2317
|
-
newInstance._service.send({
|
|
2318
|
-
type: 'initializeOriginalValues',
|
|
2319
|
-
originalValues: {},
|
|
2320
|
-
isEdited: false,
|
|
2321
|
-
});
|
|
2322
|
-
}
|
|
2323
|
-
}
|
|
2324
|
-
// Wrap instance in Proxy for reactive property access. `name` is an alias for `modelName`.
|
|
2325
|
-
const sendUpdate = (target, prop, value) => {
|
|
2326
|
-
if (prop === 'modelName') {
|
|
2327
|
-
// Model name change - need to update Schema and database
|
|
2328
|
-
const context = target._getSnapshotContext();
|
|
2329
|
-
const oldName = context.modelName;
|
|
2330
|
-
const newName = value;
|
|
2331
|
-
if (oldName === newName) {
|
|
2332
|
-
logger(`Model name unchanged: "${oldName}"`);
|
|
2333
|
-
return;
|
|
2334
|
-
}
|
|
2335
|
-
logger(`Updating model name from "${oldName}" to "${newName}"`);
|
|
2336
|
-
// Update Model instance cache using new ID-based structure
|
|
2337
|
-
const modelFileId = context.id; // id is now the schemaFileId (string)
|
|
2338
|
-
if (modelFileId) {
|
|
2339
|
-
Model.updateNameIndex(oldName, newName, context.schemaName, modelFileId);
|
|
2340
|
-
}
|
|
2341
|
-
else {
|
|
2342
|
-
// Fallback to legacy cache if no ID (shouldn't happen, but safety)
|
|
2343
|
-
const oldCacheKey = `${context.schemaName}:${oldName}`;
|
|
2344
|
-
const newCacheKey = `${context.schemaName}:${newName}`;
|
|
2345
|
-
if (Model.instanceCache.has(oldCacheKey)) {
|
|
2346
|
-
const entry = Model.instanceCache.get(oldCacheKey);
|
|
2347
|
-
Model.instanceCache.delete(oldCacheKey);
|
|
2348
|
-
Model.instanceCache.set(newCacheKey, entry);
|
|
2349
|
-
}
|
|
2350
|
-
}
|
|
2351
|
-
// Update Model context
|
|
2352
|
-
target._service.send({
|
|
2353
|
-
type: 'updateContext',
|
|
2354
|
-
modelName: newName,
|
|
2355
|
-
});
|
|
2356
|
-
// Mark model as draft
|
|
2357
|
-
target._service.send({
|
|
2358
|
-
type: 'markAsDraft',
|
|
2359
|
-
propertyKey: 'modelName',
|
|
2360
|
-
});
|
|
2361
|
-
// Save draft to database immediately so changes persist
|
|
2362
|
-
trackDraftSave(target._service, target._saveDraftToDb(oldName, newName), `Model "${newName}"`);
|
|
2363
|
-
// NOTE: Schema is not notified of model name changes during edits.
|
|
2364
|
-
// Schema will read from Model instances when persisting/saving.
|
|
2365
|
-
}
|
|
2366
|
-
else {
|
|
2367
|
-
// Standard property update
|
|
2368
|
-
target._service.send({
|
|
2369
|
-
type: 'updateContext',
|
|
2370
|
-
[prop]: value,
|
|
2371
|
-
});
|
|
2372
|
-
}
|
|
2373
|
-
};
|
|
2374
|
-
const proxiedInstance = createReactiveProxy({
|
|
2375
|
-
instance: newInstance,
|
|
2376
|
-
service: newInstance._service,
|
|
2377
|
-
trackedProperties: TRACKED_PROPERTIES,
|
|
2378
|
-
getContext: (target) => target._getSnapshotContext(),
|
|
2379
|
-
sendUpdate,
|
|
2380
|
-
resolve: (target, prop) => {
|
|
2381
|
-
if (prop === 'name') {
|
|
2382
|
-
return {
|
|
2383
|
-
get: () => target._getSnapshotContext().modelName,
|
|
2384
|
-
set: (value) => sendUpdate(target, 'modelName', value),
|
|
2385
|
-
};
|
|
2386
|
-
}
|
|
2387
|
-
return undefined;
|
|
2388
|
-
},
|
|
2389
|
-
virtualKeys: () => ['name'],
|
|
2390
|
-
onSet: (_target, prop) => {
|
|
2391
|
-
if (prop === 'properties') {
|
|
2392
|
-
// Properties are read-only computed values from ModelProperty instances
|
|
2393
|
-
throw new Error('Cannot set model.properties directly. Properties are computed from ModelProperty instances.');
|
|
2394
|
-
}
|
|
2395
|
-
if (prop === 'description') {
|
|
2396
|
-
// Description is not supported - JSON files can have it but we ignore it at runtime
|
|
2397
|
-
throw new Error('Cannot set model.description. Description is not supported in runtime Model instances.');
|
|
2398
|
-
}
|
|
2399
|
-
},
|
|
2400
|
-
});
|
|
2401
|
-
// Step 8: Store in new ID-based cache
|
|
2402
|
-
// resolvedId is guaranteed to be defined at this point (generated if not provided).
|
|
2403
|
-
// The ID and legacy caches share one entry: one hold count for the instance.
|
|
2404
|
-
const entry = { instance: proxiedInstance, refCount: holds };
|
|
2405
|
-
this.instanceCacheById.set(resolvedId, entry);
|
|
2406
|
-
// Step 9: Store in name-based index (placeholder key when !id; loadOrCreateModel will replace with final name)
|
|
2407
|
-
this.instanceCacheByName.set(nameKey, resolvedId);
|
|
2408
|
-
// Step 9b: When created with id (schema model) and unique name, also register original name so create(modelName) returns same instance
|
|
2409
|
-
if (id && uniqueModelName !== modelName) {
|
|
2410
|
-
this.instanceCacheByName.set(originalNameKey, resolvedId);
|
|
2411
|
-
}
|
|
2412
|
-
// Step 10: Also store in legacy cache (for backward compatibility during migration)
|
|
2413
|
-
this.instanceCache.set(nameKey, entry);
|
|
2414
|
-
logger(`Model.create: Created new instance for "${uniqueModelName}" (ID: ${id})`);
|
|
2415
|
-
// Model instance is now cached and accessible via Model.getById() and Model.getByName()
|
|
2416
|
-
// No wrapper or store registration needed
|
|
2417
|
-
// Step 11: Register with schema if requested, OR trigger write if properties are provided
|
|
2418
|
-
// If properties are provided, we need to write the model to get modelId for property creation
|
|
2419
|
-
// If schema provided, trigger write process instead of registration
|
|
2420
|
-
// When schemaName is passed as string (e.g. useCreateModel), we must trigger write so runtime-created
|
|
2421
|
-
// models get persisted to DB and show up in useModels (which queries the database).
|
|
2422
|
-
const hasProperties = !!options?.properties && Object.keys(options.properties).length > 0;
|
|
2423
|
-
const shouldTriggerWrite = (registerWithSchema && schemaInstance) ||
|
|
2424
|
-
hasProperties ||
|
|
2425
|
-
typeof schemaNameOrSchema === 'string';
|
|
2426
|
-
// Every DB load (createById, getByNameAsync, ...) also passes a string schema and no properties.
|
|
2427
|
-
// Such a model adopts its existing row: writing it again rewrote each property row and cleared is_edited.
|
|
2428
|
-
const adoptExistingRow = !!options?.persistedBy || (typeof schemaNameOrSchema === 'string' && !hasProperties);
|
|
2429
|
-
if (shouldTriggerWrite) {
|
|
2430
|
-
queueMicrotask(async () => {
|
|
2431
|
-
try {
|
|
2432
|
-
// Wait for model to reach idle state (validation complete)
|
|
2433
|
-
await new Promise((resolve) => {
|
|
2434
|
-
// Cleared once the model settles: a pending timer kept Node scripts alive for 10 s.
|
|
2435
|
-
const timer = setTimeout(() => {
|
|
2436
|
-
subscription.unsubscribe();
|
|
2437
|
-
resolve();
|
|
2438
|
-
}, 10000);
|
|
2439
|
-
const subscription = proxiedInstance._service.subscribe((snapshot) => {
|
|
2440
|
-
// Wait for idle state (validation complete) or error state
|
|
2441
|
-
if (snapshot.value === 'idle' || snapshot.value === 'error') {
|
|
2442
|
-
clearTimeout(timer);
|
|
2443
|
-
subscription.unsubscribe();
|
|
2444
|
-
resolve();
|
|
2445
|
-
}
|
|
2446
|
-
});
|
|
2447
|
-
});
|
|
2448
|
-
// The caller writes this model's rows; wait for that so two writers don't write them at once
|
|
2449
|
-
if (options?.persistedBy) {
|
|
2450
|
-
await options.persistedBy.catch(() => { });
|
|
2451
|
-
}
|
|
2452
|
-
// Unloaded or evicted (e.g. Schema.destroy) while it loaded: a stopped model never writes
|
|
2453
|
-
if (isActorStopped(proxiedInstance._service)) {
|
|
2454
|
-
failPropertyCreation('the model was unloaded');
|
|
2455
|
-
return;
|
|
2456
|
-
}
|
|
2457
|
-
if (adoptExistingRow && (await Model.adoptPersistedRow(proxiedInstance))) {
|
|
2458
|
-
return;
|
|
2459
|
-
}
|
|
2460
|
-
// Only write if validation passed (model is in idle state, not error)
|
|
2461
|
-
const finalSnapshot = proxiedInstance._service.getSnapshot();
|
|
2462
|
-
if (finalSnapshot.value === 'idle' && (!finalSnapshot.context._validationErrors || finalSnapshot.context._validationErrors.length === 0)) {
|
|
2463
|
-
logger(`Model validation passed, triggering write process`);
|
|
2464
|
-
// Get schema ID from schema instance or look it up by schema name
|
|
2465
|
-
let schemaId;
|
|
2466
|
-
if (schemaInstance) {
|
|
2467
|
-
const schemaContext = schemaInstance._getSnapshotContext();
|
|
2468
|
-
// First, try to use _dbId directly from schema context (most reliable)
|
|
2469
|
-
// But verify it exists in database before using it
|
|
2470
|
-
if (schemaContext._dbId && typeof schemaContext._dbId === 'number' && schemaContext._dbId > 0) {
|
|
2471
|
-
try {
|
|
2472
|
-
const db = BaseDb.getAppDb();
|
|
2473
|
-
if (db) {
|
|
2474
|
-
const verifySchema = await db
|
|
2475
|
-
.select({ id: schemas.id })
|
|
2476
|
-
.from(schemas)
|
|
2477
|
-
.where(eq(schemas.id, schemaContext._dbId))
|
|
2478
|
-
.limit(1);
|
|
2479
|
-
if (verifySchema.length > 0) {
|
|
2480
|
-
schemaId = schemaContext._dbId;
|
|
2481
|
-
logger(`Using schema _dbId directly from Schema context: ${schemaId} (verified in database)`);
|
|
2482
|
-
}
|
|
2483
|
-
else {
|
|
2484
|
-
logger(`WARNING: Schema _dbId ${schemaContext._dbId} from context does not exist in database. Falling back to schemaFileId lookup.`);
|
|
2485
|
-
}
|
|
2486
|
-
}
|
|
2487
|
-
}
|
|
2488
|
-
catch (error) {
|
|
2489
|
-
logger(`WARNING: Could not verify schema _dbId: ${error}. Falling back to schemaFileId lookup.`);
|
|
2490
|
-
}
|
|
2491
|
-
}
|
|
2492
|
-
// Fall back to database lookup by schemaFileId if _dbId wasn't available or invalid
|
|
2493
|
-
if (!schemaId) {
|
|
2494
|
-
// Fall back to database lookup by schemaFileId
|
|
2495
|
-
const schemaFileId = schemaContext.id; // id is the schemaFileId (string) in SchemaMachineContext
|
|
2496
|
-
if (schemaFileId) {
|
|
2497
|
-
try {
|
|
2498
|
-
const db = BaseDb.getAppDb();
|
|
2499
|
-
if (db) {
|
|
2500
|
-
const schemaRecords = await db
|
|
2501
|
-
.select()
|
|
2502
|
-
.from(schemas)
|
|
2503
|
-
.where(eq(schemas.schemaFileId, schemaFileId))
|
|
2504
|
-
.limit(1);
|
|
2505
|
-
if (schemaRecords.length > 0) {
|
|
2506
|
-
schemaId = schemaRecords[0].id;
|
|
2507
|
-
logger(`Found schemaId ${schemaId} by schemaFileId lookup: ${schemaFileId}`);
|
|
2508
|
-
}
|
|
2509
|
-
else {
|
|
2510
|
-
logger(`WARNING: Schema not found in database by schemaFileId: ${schemaFileId}. Schema may not be fully loaded yet.`);
|
|
2511
|
-
}
|
|
2512
|
-
}
|
|
2513
|
-
}
|
|
2514
|
-
catch (error) {
|
|
2515
|
-
logger(`Error looking up schemaId: ${error}`);
|
|
2516
|
-
}
|
|
2517
|
-
}
|
|
2518
|
-
else {
|
|
2519
|
-
logger(`WARNING: Schema instance has no schemaFileId (id) in context`);
|
|
2520
|
-
}
|
|
2521
|
-
}
|
|
2522
|
-
}
|
|
2523
|
-
else if (schemaName) {
|
|
2524
|
-
// If no schema instance but we have schema name, look it up by name
|
|
2525
|
-
try {
|
|
2526
|
-
const db = BaseDb.getAppDb();
|
|
2527
|
-
if (db) {
|
|
2528
|
-
const schemaRecords = await db
|
|
2529
|
-
.select()
|
|
2530
|
-
.from(schemas)
|
|
2531
|
-
.where(eq(schemas.name, schemaName))
|
|
2532
|
-
.limit(1);
|
|
2533
|
-
if (schemaRecords.length > 0) {
|
|
2534
|
-
schemaId = schemaRecords[0].id;
|
|
2535
|
-
logger(`Found schemaId ${schemaId} by schema name lookup: ${schemaName}`);
|
|
2536
|
-
}
|
|
2537
|
-
else {
|
|
2538
|
-
logger(`WARNING: Schema not found in database by name: ${schemaName}`);
|
|
2539
|
-
}
|
|
2540
|
-
}
|
|
2541
|
-
}
|
|
2542
|
-
catch (error) {
|
|
2543
|
-
logger(`Error looking up schemaId by name: ${error}`);
|
|
2544
|
-
}
|
|
2545
|
-
}
|
|
2546
|
-
// Validate schemaId before proceeding
|
|
2547
|
-
if (!schemaId || !Number.isInteger(schemaId) || schemaId <= 0) {
|
|
2548
|
-
logger(`ERROR: Invalid schemaId (${schemaId}) for model "${finalSnapshot.context.modelName}". Cannot trigger write process.`);
|
|
2549
|
-
logger(`Schema context:`, {
|
|
2550
|
-
schemaFileId: schemaInstance ? schemaInstance._getSnapshotContext().id : 'N/A',
|
|
2551
|
-
_dbId: schemaInstance ? schemaInstance._getSnapshotContext()._dbId : 'N/A',
|
|
2552
|
-
schemaName: schemaName || 'N/A',
|
|
2553
|
-
});
|
|
2554
|
-
failPropertyCreation(`its schema "${schemaName}" was not found in the database`);
|
|
2555
|
-
}
|
|
2556
|
-
else if (finalSnapshot.context.id) {
|
|
2557
|
-
// Verify schema exists in database before proceeding (additional safety check)
|
|
2558
|
-
try {
|
|
2559
|
-
const db = BaseDb.getAppDb();
|
|
2560
|
-
if (db) {
|
|
2561
|
-
const schemaCheck = await db
|
|
2562
|
-
.select({ id: schemas.id })
|
|
2563
|
-
.from(schemas)
|
|
2564
|
-
.where(eq(schemas.id, schemaId))
|
|
2565
|
-
.limit(1);
|
|
2566
|
-
if (schemaCheck.length === 0) {
|
|
2567
|
-
const errorMsg = `Schema with id ${schemaId} does not exist in database. Cannot create model "${finalSnapshot.context.modelName}".`;
|
|
2568
|
-
logger(`ERROR: ${errorMsg}`);
|
|
2569
|
-
throw new Error(errorMsg);
|
|
2570
|
-
}
|
|
2571
|
-
logger(`Verified schema ${schemaId} exists in database before creating model "${finalSnapshot.context.modelName}"`);
|
|
2572
|
-
}
|
|
2573
|
-
else {
|
|
2574
|
-
logger(`WARNING: Database not available for schema verification. Proceeding anyway.`);
|
|
2575
|
-
}
|
|
2576
|
-
}
|
|
2577
|
-
catch (error) {
|
|
2578
|
-
// If it's our validation error, re-throw it
|
|
2579
|
-
if (error instanceof Error && error.message.includes('does not exist in database')) {
|
|
2580
|
-
throw error;
|
|
2581
|
-
}
|
|
2582
|
-
logger(`WARNING: Could not verify schema exists in database: ${error}. Proceeding anyway.`);
|
|
2583
|
-
}
|
|
2584
|
-
const pendingModelFileId = finalSnapshot.context.id;
|
|
2585
|
-
const pendingSchemaId = schemaId;
|
|
2586
|
-
// Wait for writeProcess to be spawned (it's spawned in idle state entry action)
|
|
2587
|
-
// Retry a few times if writeProcess isn't available yet
|
|
2588
|
-
let retries = 0;
|
|
2589
|
-
const maxRetries = 10;
|
|
2590
|
-
const checkAndSend = async () => {
|
|
2591
|
-
// Unloaded or evicted (e.g. Schema.destroy) before the write started: nothing to write to
|
|
2592
|
-
if (isActorStopped(proxiedInstance._service)) {
|
|
2593
|
-
failPropertyCreation('the model was unloaded');
|
|
2594
|
-
return;
|
|
2595
|
-
}
|
|
2596
|
-
const currentSnapshot = proxiedInstance._service.getSnapshot();
|
|
2597
|
-
if (currentSnapshot.context.writeProcess) {
|
|
2598
|
-
// Track pending write (only once it really starts: a stopped model leaves none behind)
|
|
2599
|
-
Model.trackPendingWrite(pendingModelFileId, pendingSchemaId); // id is now the schemaFileId (string)
|
|
2600
|
-
logger(`Triggering write process for model "${finalSnapshot.context.modelName}" (schemaId: ${schemaId})`);
|
|
2601
|
-
// Use pending property definitions if available, otherwise convert from ModelProperty instances
|
|
2602
|
-
let propertiesObject = {};
|
|
2603
|
-
if (currentSnapshot.context._pendingPropertyDefinitions) {
|
|
2604
|
-
// Use the original property definitions for the write
|
|
2605
|
-
// Convert dataType to type for validation compatibility
|
|
2606
|
-
propertiesObject = JSON.parse(JSON.stringify(currentSnapshot.context._pendingPropertyDefinitions));
|
|
2607
|
-
for (const [propName, propData] of Object.entries(propertiesObject)) {
|
|
2608
|
-
if (propData.dataType && !propData.type) {
|
|
2609
|
-
propertiesObject[propName] = {
|
|
2610
|
-
...propData,
|
|
2611
|
-
type: propData.dataType,
|
|
2612
|
-
};
|
|
2613
|
-
}
|
|
2614
|
-
}
|
|
2615
|
-
}
|
|
2616
|
-
else {
|
|
2617
|
-
// Fallback: convert ModelProperty instances to object format
|
|
2618
|
-
const properties = proxiedInstance.properties || [];
|
|
2619
|
-
propertiesObject = modelPropertiesToObject(properties);
|
|
2620
|
-
}
|
|
2621
|
-
// Set up subscription to writeProcess to detect write success BEFORE sending request
|
|
2622
|
-
const writeProcess = currentSnapshot.context.writeProcess;
|
|
2623
|
-
// Check current state - if already in success, handle immediately
|
|
2624
|
-
const currentWriteState = writeProcess.getSnapshot();
|
|
2625
|
-
if (currentWriteState.value === 'success') {
|
|
2626
|
-
proxiedInstance._service.send({
|
|
2627
|
-
type: 'writeSuccess',
|
|
2628
|
-
output: currentWriteState.context.entityData,
|
|
2629
|
-
});
|
|
2630
|
-
}
|
|
2631
|
-
else {
|
|
2632
|
-
// Set up subscription to catch future state changes
|
|
2633
|
-
const writeSubscription = writeProcess.subscribe((writeSnapshot) => {
|
|
2634
|
-
if (writeSnapshot.value === 'success') {
|
|
2635
|
-
writeSubscription.unsubscribe();
|
|
2636
|
-
const currentContext = proxiedInstance._service.getSnapshot().context;
|
|
2637
|
-
logger(`[writeProcess subscription] Write succeeded, sending writeSuccess event. Has _pendingPropertyDefinitions: ${!!(currentContext._pendingPropertyDefinitions && Object.keys(currentContext._pendingPropertyDefinitions).length > 0)}`);
|
|
2638
|
-
// Send writeSuccess event to model machine
|
|
2639
|
-
proxiedInstance._service.send({
|
|
2640
|
-
type: 'writeSuccess',
|
|
2641
|
-
output: writeSnapshot.context.entityData,
|
|
2642
|
-
});
|
|
2643
|
-
}
|
|
2644
|
-
else if (writeSnapshot.value === 'error') {
|
|
2645
|
-
writeSubscription.unsubscribe();
|
|
2646
|
-
const errorContext = writeSnapshot.context;
|
|
2647
|
-
logger(`Write process failed for model "${finalSnapshot.context.modelName}": ${errorContext.error?.message || 'Unknown error'}`);
|
|
2648
|
-
logger(`Write process error details:`, errorContext.error);
|
|
2649
|
-
logger(`Validation errors:`, errorContext.validationErrors);
|
|
2650
|
-
failPropertyCreation(`writing the model failed: ${errorContext.error?.message || 'Unknown error'}`);
|
|
2651
|
-
}
|
|
2652
|
-
});
|
|
2653
|
-
}
|
|
2654
|
-
proxiedInstance._service.send({
|
|
2655
|
-
type: 'requestWrite',
|
|
2656
|
-
data: {
|
|
2657
|
-
modelFileId: finalSnapshot.context.id, // id is now the schemaFileId (string)
|
|
2658
|
-
modelName: finalSnapshot.context.modelName,
|
|
2659
|
-
schemaName: finalSnapshot.context.schemaName,
|
|
2660
|
-
schemaId,
|
|
2661
|
-
properties: propertiesObject,
|
|
2662
|
-
},
|
|
2663
|
-
});
|
|
2664
|
-
}
|
|
2665
|
-
else if (retries < maxRetries) {
|
|
2666
|
-
retries++;
|
|
2667
|
-
setTimeout(checkAndSend, 50); // Retry after 50ms
|
|
2668
|
-
}
|
|
2669
|
-
else {
|
|
2670
|
-
logger(`ERROR: writeProcess not available after ${maxRetries} retries for model "${finalSnapshot.context.modelName}"`);
|
|
2671
|
-
failPropertyCreation('its write process never started');
|
|
2672
|
-
}
|
|
2673
|
-
};
|
|
2674
|
-
// Start checking immediately, but also allow time for entry action to complete
|
|
2675
|
-
setTimeout(checkAndSend, 0);
|
|
2676
|
-
}
|
|
2677
|
-
else {
|
|
2678
|
-
logger(`Cannot trigger write process: missing schemaId (${schemaId}) or id (${finalSnapshot.context.id})`);
|
|
2679
|
-
failPropertyCreation('it has no id');
|
|
2680
|
-
}
|
|
2681
|
-
}
|
|
2682
|
-
else {
|
|
2683
|
-
logger(`Model validation failed or model in error state, skipping write process`);
|
|
2684
|
-
failPropertyCreation('the model failed validation');
|
|
2685
|
-
}
|
|
2686
|
-
}
|
|
2687
|
-
catch (error) {
|
|
2688
|
-
logger(`Failed to trigger write process: ${error instanceof Error ? error.message : String(error)}`);
|
|
2689
|
-
failPropertyCreation(error instanceof Error ? error.message : String(error));
|
|
2690
|
-
}
|
|
2691
|
-
});
|
|
2692
|
-
}
|
|
2693
|
-
if (!waitForReady)
|
|
2694
|
-
return proxiedInstance;
|
|
2695
|
-
return this.waitUntilReady(proxiedInstance, readyTimeout);
|
|
2696
|
-
}
|
|
2697
|
-
/**
|
|
2698
|
-
* Get Model instance by modelFileId (O(1) lookup). Takes a hold on the instance; release it with
|
|
2699
|
-
* `unload()`.
|
|
2700
|
-
*/
|
|
2701
|
-
static getById(modelFileId) {
|
|
2702
|
-
const entry = this.instanceCacheById.get(modelFileId);
|
|
2703
|
-
if (!entry)
|
|
2704
|
-
return undefined;
|
|
2705
|
-
entry.refCount += 1;
|
|
2706
|
-
return entry.instance;
|
|
2707
|
-
}
|
|
2708
|
-
/**
|
|
2709
|
-
* @internal For a Schema loading its models: true when this load should take the schema's hold
|
|
2710
|
-
* on the model (pass no `hold` option), false when the schema already holds it (pass
|
|
2711
|
-
* `hold: false`). After a true, call `dropSchemaHold` if the load returns no model.
|
|
2712
|
-
*/
|
|
2713
|
-
static takeSchemaHold(modelFileId) {
|
|
2714
|
-
if (!modelFileId || this.schemaHeldModelIds.has(modelFileId))
|
|
2715
|
-
return false;
|
|
2716
|
-
this.schemaHeldModelIds.add(modelFileId);
|
|
2717
|
-
return true;
|
|
2718
|
-
}
|
|
2719
|
-
/** @internal The load `takeSchemaHold` approved found no model: there is no hold to release. */
|
|
2720
|
-
static dropSchemaHold(modelFileId) {
|
|
2721
|
-
this.schemaHeldModelIds.delete(modelFileId);
|
|
2722
|
-
}
|
|
2723
|
-
/** @internal A Schema's last unload: releases its hold on each of its models. */
|
|
2724
|
-
static releaseSchemaHolds(schemaName) {
|
|
2725
|
-
for (const modelFileId of [...this.schemaHeldModelIds]) {
|
|
2726
|
-
const model = this.peek(modelFileId);
|
|
2727
|
-
if (!model) {
|
|
2728
|
-
this.schemaHeldModelIds.delete(modelFileId);
|
|
2729
|
-
continue;
|
|
2730
|
-
}
|
|
2731
|
-
if (model._getSnapshotContext().schemaName !== schemaName)
|
|
2732
|
-
continue;
|
|
2733
|
-
this.schemaHeldModelIds.delete(modelFileId);
|
|
2734
|
-
model.unload();
|
|
2735
|
-
}
|
|
2736
|
-
}
|
|
2737
|
-
/**
|
|
2738
|
-
* The cached Model with this modelFileId, without taking a hold. For SDK code that reads the
|
|
2739
|
-
* model and doesn't hand it to a caller; callers that keep it use `getById`, `find` or `create`.
|
|
2740
|
-
*/
|
|
2741
|
-
static peek(modelFileId) {
|
|
2742
|
-
return this.instanceCacheById.get(modelFileId)?.instance;
|
|
2743
|
-
}
|
|
2744
|
-
/**
|
|
2745
|
-
* Get Model instance by name (O(1) lookup via name→ID mapping). Takes a hold on the instance;
|
|
2746
|
-
* release it with `unload()`.
|
|
2747
|
-
*
|
|
2748
|
-
* @param modelName - The name of the model
|
|
2749
|
-
* @param schemaName - The schema name (optional, will query DB if not provided)
|
|
2750
|
-
* @returns The Model instance if found, undefined otherwise
|
|
2751
|
-
*/
|
|
2752
|
-
static getByName(modelName, schemaName) {
|
|
2753
|
-
const model = this.peekByName(modelName, schemaName);
|
|
2754
|
-
return model?.id ? this.getById(model.id) : undefined;
|
|
2755
|
-
}
|
|
2756
|
-
/**
|
|
2757
|
-
* `getByName` without taking a hold. Without `schemaName` it searches every cached schema and
|
|
2758
|
-
* throws AmbiguousModelError when models from several schemas share the name; callers that know
|
|
2759
|
-
* the schema or model id should use Model.resolve() instead.
|
|
2760
|
-
*/
|
|
2761
|
-
static peekByName(modelName, schemaName) {
|
|
2762
|
-
if (schemaName) {
|
|
2763
|
-
const id = this.instanceCacheByName.get(`${schemaName}:${modelName}`);
|
|
2764
|
-
return id ? this.peek(id) : undefined;
|
|
2765
|
-
}
|
|
2766
|
-
return this._getUniqueCachedModel(modelName, (cachedModelName) => cachedModelName === modelName);
|
|
2767
|
-
}
|
|
2768
|
-
/**
|
|
2769
|
-
* The one cached Model whose name matches (no hold taken), or AmbiguousModelError if models from
|
|
2770
|
-
* several schemas do.
|
|
2771
|
-
*/
|
|
2772
|
-
static _getUniqueCachedModel(label, matches) {
|
|
2773
|
-
const matchesById = new Map(); // modelFileId -> schemaName
|
|
2774
|
-
for (const [nameKey, id] of this.instanceCacheByName.entries()) {
|
|
2775
|
-
const separator = nameKey.lastIndexOf(':');
|
|
2776
|
-
const cachedModelName = nameKey.slice(separator + 1);
|
|
2777
|
-
if (id && cachedModelName && matches(cachedModelName) && this.instanceCacheById.has(id)) {
|
|
2778
|
-
matchesById.set(id, nameKey.slice(0, separator));
|
|
2779
|
-
}
|
|
2780
|
-
}
|
|
2781
|
-
if (matchesById.size > 1) {
|
|
2782
|
-
throw new AmbiguousModelError(label, [...matchesById.values()]);
|
|
2783
|
-
}
|
|
2784
|
-
const [matchId] = matchesById.keys();
|
|
2785
|
-
return matchId ? this.peek(matchId) : undefined;
|
|
2786
|
-
}
|
|
2787
|
-
/**
|
|
2788
|
-
* Resolve a cached Model from the most specific scope available: modelFileId (Model.id), then
|
|
2789
|
-
* schemaName + modelName, then modelName alone. Takes no hold.
|
|
2790
|
-
*/
|
|
2791
|
-
static resolve(modelName, scope = {}) {
|
|
2792
|
-
if (scope.modelFileId) {
|
|
2793
|
-
const byId = this.peek(scope.modelFileId);
|
|
2794
|
-
if (byId)
|
|
2795
|
-
return byId;
|
|
2796
|
-
}
|
|
2797
|
-
if (!modelName)
|
|
2798
|
-
return undefined;
|
|
2799
|
-
if (scope.schemaName) {
|
|
2800
|
-
const bySchema = this.peekByName(modelName, scope.schemaName);
|
|
2801
|
-
if (bySchema)
|
|
2802
|
-
return bySchema;
|
|
2803
|
-
}
|
|
2804
|
-
return this.peekByName(modelName);
|
|
2805
|
-
}
|
|
2806
|
-
/**
|
|
2807
|
-
* Find Model by modelType (an EAS model name, as seeds.type stores it). Matched as ADR 0008
|
|
2808
|
-
* matches EAS names: `new_model` finds "New model", and `htmlpage` (an older SDK's name) finds
|
|
2809
|
-
* "HTMLPage", unless HTMLPage's nameMatch (ADR 0009) is `exact`. Only the tightest matching tier
|
|
2810
|
-
* counts; matches in several schemas are ambiguous. Takes no hold.
|
|
2811
|
-
*/
|
|
2812
|
-
static findByModelType(modelType) {
|
|
2813
|
-
if (!modelType)
|
|
2814
|
-
return undefined;
|
|
2815
|
-
const cachedNames = [...this.instanceCacheByName.keys()].map((key) => key.slice(key.lastIndexOf(':') + 1));
|
|
2816
|
-
// Each name at its nameMatch level (ADR 0009), as last loaded.
|
|
2817
|
-
const levels = currentNameMatchLevels();
|
|
2818
|
-
const best = localNamesInBestMatchTier(modelType, cachedNames, { level: (name) => levels.model(name) });
|
|
2819
|
-
return this._getUniqueCachedModel(modelType, (cachedModelName) => best.has(cachedModelName));
|
|
2820
|
-
}
|
|
2821
|
-
/**
|
|
2822
|
-
* Get all Model instances for a schema from cache only (synchronous). Takes no holds.
|
|
2823
|
-
* Includes models created at runtime via Model.create() that may not yet be in schema context.
|
|
2824
|
-
*/
|
|
2825
|
-
static getCachedInstancesForSchema(schemaName) {
|
|
2826
|
-
const instances = [];
|
|
2827
|
-
const seen = new Set();
|
|
2828
|
-
for (const [nameKey, id] of this.instanceCacheByName.entries()) {
|
|
2829
|
-
const [cachedSchemaName] = nameKey.split(':');
|
|
2830
|
-
if (cachedSchemaName === schemaName && id && !seen.has(id)) {
|
|
2831
|
-
const instance = this.peek(id);
|
|
2832
|
-
if (instance) {
|
|
2833
|
-
instances.push(instance);
|
|
2834
|
-
seen.add(id);
|
|
2835
|
-
}
|
|
2836
|
-
}
|
|
2837
|
-
}
|
|
2838
|
-
for (const [nameKey, entry] of this.instanceCache.entries()) {
|
|
2839
|
-
const [cachedSchemaName] = nameKey.split(':');
|
|
2840
|
-
if (cachedSchemaName === schemaName) {
|
|
2841
|
-
const context = entry.instance._getSnapshotContext();
|
|
2842
|
-
const id = context.id;
|
|
2843
|
-
if (id && !seen.has(id)) {
|
|
2844
|
-
instances.push(entry.instance);
|
|
2845
|
-
seen.add(id);
|
|
2846
|
-
}
|
|
2847
|
-
}
|
|
2848
|
-
}
|
|
2849
|
-
return instances;
|
|
2850
|
-
}
|
|
2851
|
-
/**
|
|
2852
|
-
* Async Model.resolve(): exact cache hit by modelFileId or schemaName+modelName, else the DB row for
|
|
2853
|
-
* that scope (instantiating the Model), and only then the name-only fallback. Takes no hold.
|
|
2854
|
-
*/
|
|
2855
|
-
static async resolveAsync(modelName, scope = {}) {
|
|
2856
|
-
if (scope.modelFileId) {
|
|
2857
|
-
const byId = this.peek(scope.modelFileId);
|
|
2858
|
-
if (byId)
|
|
2859
|
-
return byId;
|
|
2860
|
-
}
|
|
2861
|
-
if (modelName && scope.schemaName) {
|
|
2862
|
-
const bySchema = this.peekByName(modelName, scope.schemaName);
|
|
2863
|
-
if (bySchema)
|
|
2864
|
-
return bySchema;
|
|
2865
|
-
}
|
|
2866
|
-
if (scope.modelFileId || scope.schemaName) {
|
|
2867
|
-
const db = BaseDb.getAppDb();
|
|
2868
|
-
if (db) {
|
|
2869
|
-
const evictionEpoch = currentEvictionEpoch();
|
|
2870
|
-
try {
|
|
2871
|
-
const rows = await db
|
|
2872
|
-
.select({
|
|
2873
|
-
modelFileId: models.schemaFileId,
|
|
2874
|
-
modelName: models.name,
|
|
2875
|
-
schemaName: schemas.name,
|
|
2876
|
-
})
|
|
2877
|
-
.from(models)
|
|
2878
|
-
.innerJoin(modelSchemas, eq(models.id, modelSchemas.modelId))
|
|
2879
|
-
.innerJoin(schemas, eq(modelSchemas.schemaId, schemas.id))
|
|
2880
|
-
.where(scope.modelFileId
|
|
2881
|
-
? eq(models.schemaFileId, scope.modelFileId)
|
|
2882
|
-
: and(eq(models.name, modelName ?? ''), eq(schemas.name, scope.schemaName)))
|
|
2883
|
-
.limit(1);
|
|
2884
|
-
const record = rows[0];
|
|
2885
|
-
if (record?.modelName && record.schemaName && record.modelFileId) {
|
|
2886
|
-
// Its schema was evicted, or it was destroyed, while we read the row: don't bring it back.
|
|
2887
|
-
if (schemaEvictedSince(record.schemaName, evictionEpoch))
|
|
2888
|
-
return undefined;
|
|
2889
|
-
if (modelDestroyedSince(record.modelFileId, evictionEpoch))
|
|
2890
|
-
return undefined;
|
|
2891
|
-
return this.create(record.modelName, record.schemaName, { id: record.modelFileId, hold: false });
|
|
2892
|
-
}
|
|
2893
|
-
}
|
|
2894
|
-
catch (error) {
|
|
2895
|
-
logger(`Model.resolveAsync: scoped lookup failed for "${modelName}": ${error instanceof Error ? error.message : String(error)}`);
|
|
2896
|
-
}
|
|
2897
|
-
}
|
|
2898
|
-
}
|
|
2899
|
-
return modelName ? this.getByNameAsync(modelName, undefined, { hold: false }) : undefined;
|
|
2900
|
-
}
|
|
2901
|
-
/**
|
|
2902
|
-
* Get Model instance by name, querying database if not in cache
|
|
2903
|
-
* This is an async version that can query the database when schemaName is not provided
|
|
2904
|
-
*
|
|
2905
|
-
* @param modelName - The name of the model
|
|
2906
|
-
* @param schemaName - Optional schema name (will query DB if not provided)
|
|
2907
|
-
* @returns The Model instance if found, undefined otherwise. Takes a hold unless `hold: false`.
|
|
2908
|
-
*/
|
|
2909
|
-
static async getByNameAsync(modelName, schemaName, options) {
|
|
2910
|
-
// First try cache
|
|
2911
|
-
const cached = options?.hold === false ? this.peekByName(modelName, schemaName) : this.getByName(modelName, schemaName);
|
|
2912
|
-
if (cached) {
|
|
2913
|
-
return cached;
|
|
2914
|
-
}
|
|
2915
|
-
// If not in cache and schemaName not provided, query database
|
|
2916
|
-
if (!schemaName) {
|
|
2917
|
-
const db = BaseDb.getAppDb();
|
|
2918
|
-
if (!db) {
|
|
2919
|
-
return undefined;
|
|
2920
|
-
}
|
|
2921
|
-
const evictionEpoch = options?.evictionEpoch ?? currentEvictionEpoch();
|
|
2922
|
-
try {
|
|
2923
|
-
// Query model by name (throws AmbiguousModelError when several schemas define it)
|
|
2924
|
-
const _mod_10 = await import('./index-Dx_GTdyQ.js');
|
|
2925
|
-
const _ns_10 = _mod_10.be;
|
|
2926
|
-
const { resolveModelRecord } = _ns_10;
|
|
2927
|
-
const resolved = await resolveModelRecord(modelName, {}, db);
|
|
2928
|
-
if (!resolved?.schemaFileId) {
|
|
2929
|
-
return undefined;
|
|
2930
|
-
}
|
|
2931
|
-
const modelRecords = await db
|
|
2932
|
-
.select({
|
|
2933
|
-
modelFileId: models.schemaFileId,
|
|
2934
|
-
modelName: models.name,
|
|
2935
|
-
schemaName: schemas.name,
|
|
2936
|
-
})
|
|
2937
|
-
.from(models)
|
|
2938
|
-
.innerJoin(modelSchemas, eq(models.id, modelSchemas.modelId))
|
|
2939
|
-
.innerJoin(schemas, eq(modelSchemas.schemaId, schemas.id))
|
|
2940
|
-
.where(eq(models.id, resolved.id))
|
|
2941
|
-
.limit(1);
|
|
2942
|
-
if (modelRecords.length === 0) {
|
|
2943
|
-
return undefined;
|
|
2944
|
-
}
|
|
2945
|
-
const record = modelRecords[0];
|
|
2946
|
-
if (!record.modelName || !record.schemaName || !record.modelFileId) {
|
|
2947
|
-
return undefined;
|
|
2948
|
-
}
|
|
2949
|
-
// Its schema was evicted, or it was destroyed, while we read the row: don't bring it back.
|
|
2950
|
-
if (schemaEvictedSince(record.schemaName, evictionEpoch)) {
|
|
2951
|
-
return undefined;
|
|
2952
|
-
}
|
|
2953
|
-
if (modelDestroyedSince(record.modelFileId, evictionEpoch)) {
|
|
2954
|
-
return undefined;
|
|
2955
|
-
}
|
|
2956
|
-
// Create model instance (will be cached)
|
|
2957
|
-
return this.create(record.modelName, record.schemaName, {
|
|
2958
|
-
id: record.modelFileId, // id is now the schemaFileId (string)
|
|
2959
|
-
...(options?.hold === false && { hold: false }),
|
|
2960
|
-
});
|
|
2961
|
-
}
|
|
2962
|
-
catch (error) {
|
|
2963
|
-
if (error instanceof AmbiguousModelError)
|
|
2964
|
-
throw error;
|
|
2965
|
-
logger(`Model.getByNameAsync: Error looking up model by name "${modelName}": ${error instanceof Error ? error.message : String(error)}`);
|
|
2966
|
-
return undefined;
|
|
2967
|
-
}
|
|
2968
|
-
}
|
|
2969
|
-
return undefined;
|
|
2970
|
-
}
|
|
2971
|
-
/**
|
|
2972
|
-
* Create or get Model instance by modelFileId
|
|
2973
|
-
* Checks cache first, then database if not found
|
|
2974
|
-
*
|
|
2975
|
-
* @param modelFileId - The model file ID to look up
|
|
2976
|
-
* @returns The Model instance if found, undefined otherwise
|
|
2977
|
-
*/
|
|
2978
|
-
static async createById(modelFileId, options) {
|
|
2979
|
-
if (!modelFileId) {
|
|
2980
|
-
return undefined;
|
|
2981
|
-
}
|
|
2982
|
-
// Step 1: Check cache first (fast path)
|
|
2983
|
-
const cached = options?.hold === false ? this.peek(modelFileId) : this.getById(modelFileId);
|
|
2984
|
-
if (cached) {
|
|
2985
|
-
logger(`Model.createById: Found instance in cache for ID "${modelFileId}"`);
|
|
2986
|
-
return cached;
|
|
2987
|
-
}
|
|
2988
|
-
// Step 2: Query database to get modelName and schemaName
|
|
2989
|
-
const db = BaseDb.getAppDb();
|
|
2990
|
-
if (!db) {
|
|
2991
|
-
logger(`Model.createById: Database not available for ID "${modelFileId}"`);
|
|
2992
|
-
return undefined;
|
|
2993
|
-
}
|
|
2994
|
-
const evictionEpoch = options?.evictionEpoch ?? currentEvictionEpoch();
|
|
2995
|
-
try {
|
|
2996
|
-
// Query model by schemaFileId and join to get schema name
|
|
2997
|
-
const modelRecords = await db
|
|
2998
|
-
.select({
|
|
2999
|
-
modelId: models.id,
|
|
3000
|
-
modelName: models.name,
|
|
3001
|
-
schemaName: schemas.name,
|
|
3002
|
-
})
|
|
3003
|
-
.from(models)
|
|
3004
|
-
.innerJoin(modelSchemas, eq(models.id, modelSchemas.modelId))
|
|
3005
|
-
.innerJoin(schemas, eq(modelSchemas.schemaId, schemas.id))
|
|
3006
|
-
.where(eq(models.schemaFileId, modelFileId))
|
|
3007
|
-
.limit(1);
|
|
3008
|
-
if (modelRecords.length === 0) {
|
|
3009
|
-
logger(`Model.createById: Model not found in database for ID "${modelFileId}"`);
|
|
3010
|
-
return undefined;
|
|
3011
|
-
}
|
|
3012
|
-
const { modelName, schemaName } = modelRecords[0];
|
|
3013
|
-
if (!modelName || !schemaName) {
|
|
3014
|
-
logger(`Model.createById: Missing modelName or schemaName for ID "${modelFileId}"`);
|
|
3015
|
-
return undefined;
|
|
3016
|
-
}
|
|
3017
|
-
// Its schema was evicted while we read the row (e.g. a Schema live query in flight during
|
|
3018
|
-
// Schema.destroy or test cleanup): don't bring the model back.
|
|
3019
|
-
if (schemaEvictedSince(schemaName, evictionEpoch)) {
|
|
3020
|
-
logger(`Model.createById: schema "${schemaName}" was evicted during the lookup of "${modelFileId}"`);
|
|
3021
|
-
return undefined;
|
|
3022
|
-
}
|
|
3023
|
-
// Destroyed while we read the row: destroy() clears the caches before it deletes the rows
|
|
3024
|
-
if (modelDestroyedSince(modelFileId, evictionEpoch)) {
|
|
3025
|
-
logger(`Model.createById: model "${modelFileId}" was destroyed during the lookup`);
|
|
3026
|
-
return undefined;
|
|
3027
|
-
}
|
|
3028
|
-
// Step 3: Create model using existing create method (which will handle caching)
|
|
3029
|
-
logger(`Model.createById: Creating model "${modelName}" in schema "${schemaName}" from ID "${modelFileId}"`);
|
|
3030
|
-
return this.create(modelName, schemaName, {
|
|
3031
|
-
id: modelFileId, // id is now the schemaFileId (string)
|
|
3032
|
-
...(options?.hold === false && { hold: false }),
|
|
3033
|
-
});
|
|
3034
|
-
}
|
|
3035
|
-
catch (error) {
|
|
3036
|
-
logger(`Model.createById: Error looking up model by ID "${modelFileId}": ${error instanceof Error ? error.message : String(error)}`);
|
|
3037
|
-
return undefined;
|
|
3038
|
-
}
|
|
3039
|
-
}
|
|
3040
|
-
/**
|
|
3041
|
-
* Find Model instance by modelFileId, modelName/schemaName, or both
|
|
3042
|
-
* Waits for the model to be fully loaded (idle state) by default
|
|
3043
|
-
* @param options - Find options including lookup parameters and wait configuration
|
|
3044
|
-
* @returns Model instance if found, undefined otherwise
|
|
3045
|
-
*/
|
|
3046
|
-
static async find({ modelFileId, modelName, schemaName, waitForReady = true, readyTimeout = DEFAULT_READY_TIMEOUT_MS, }) {
|
|
3047
|
-
return await findEntity({
|
|
3048
|
-
getById: (id) => Model.getById(id),
|
|
3049
|
-
createById: (id) => Model.createById(id),
|
|
3050
|
-
getByName: (name, ...args) => {
|
|
3051
|
-
const schemaName = args[0];
|
|
3052
|
-
return Model.getByName(name, schemaName);
|
|
3053
|
-
},
|
|
3054
|
-
createByName: async (name, ...args) => {
|
|
3055
|
-
const schemaName = args[0];
|
|
3056
|
-
return Model.getByNameAsync(name, schemaName);
|
|
3057
|
-
},
|
|
3058
|
-
}, {
|
|
3059
|
-
id: modelFileId,
|
|
3060
|
-
name: modelName,
|
|
3061
|
-
schemaName,
|
|
3062
|
-
}, {
|
|
3063
|
-
waitForReady,
|
|
3064
|
-
readyTimeout,
|
|
3065
|
-
});
|
|
3066
|
-
}
|
|
3067
|
-
/**
|
|
3068
|
-
* Get all Model instances for a schema by schemaFileId or schemaName
|
|
3069
|
-
* Queries database for all models with the given schemaId/schemaName and returns Model instances
|
|
3070
|
-
*
|
|
3071
|
-
* @param schemaIdentifier - The schema file ID or schema name to get models for
|
|
3072
|
-
* @returns Array of Model instances for the schema
|
|
3073
|
-
*/
|
|
3074
|
-
static async createBySchemaId(schemaIdentifier) {
|
|
3075
|
-
if (!schemaIdentifier) {
|
|
3076
|
-
return [];
|
|
3077
|
-
}
|
|
3078
|
-
const db = BaseDb.getAppDb();
|
|
3079
|
-
if (!db) {
|
|
3080
|
-
logger(`Model.createBySchemaId: Database not available for schema "${schemaIdentifier}"`);
|
|
3081
|
-
return [];
|
|
3082
|
-
}
|
|
3083
|
-
const evictionEpoch = currentEvictionEpoch();
|
|
3084
|
-
try {
|
|
3085
|
-
// Query all models for this schema - support both schemaFileId and schema name
|
|
3086
|
-
const modelRecords = await db
|
|
3087
|
-
.select({
|
|
3088
|
-
modelFileId: models.schemaFileId,
|
|
3089
|
-
modelName: models.name,
|
|
3090
|
-
schemaName: schemas.name,
|
|
3091
|
-
})
|
|
3092
|
-
.from(schemas)
|
|
3093
|
-
.innerJoin(modelSchemas, eq(schemas.id, modelSchemas.schemaId))
|
|
3094
|
-
.innerJoin(models, eq(modelSchemas.modelId, models.id))
|
|
3095
|
-
.where(or(eq(schemas.schemaFileId, schemaIdentifier), eq(schemas.name, schemaIdentifier)));
|
|
3096
|
-
if (modelRecords.length === 0) {
|
|
3097
|
-
logger(`Model.createBySchemaId: No models found in database for schema "${schemaIdentifier}"`);
|
|
3098
|
-
return [];
|
|
3099
|
-
}
|
|
3100
|
-
// Create Model instances for each model (create() handles caching)
|
|
3101
|
-
const modelInstances = [];
|
|
3102
|
-
for (const record of modelRecords) {
|
|
3103
|
-
if (!record.modelName || !record.schemaName || !record.modelFileId) {
|
|
3104
|
-
continue;
|
|
3105
|
-
}
|
|
3106
|
-
// Its schema was evicted, or the model destroyed, while we read the rows: don't bring it back.
|
|
3107
|
-
if (schemaEvictedSince(record.schemaName, evictionEpoch)) {
|
|
3108
|
-
continue;
|
|
3109
|
-
}
|
|
3110
|
-
if (modelDestroyedSince(record.modelFileId, evictionEpoch)) {
|
|
3111
|
-
continue;
|
|
3112
|
-
}
|
|
3113
|
-
// Check cache first
|
|
3114
|
-
const cached = this.getById(record.modelFileId);
|
|
3115
|
-
if (cached) {
|
|
3116
|
-
modelInstances.push(cached);
|
|
3117
|
-
}
|
|
3118
|
-
else {
|
|
3119
|
-
// Create model instance (will be cached)
|
|
3120
|
-
const model = this.create(record.modelName, record.schemaName, {
|
|
3121
|
-
id: record.modelFileId, // id is now the schemaFileId (string)
|
|
3122
|
-
waitForReady: false,
|
|
3123
|
-
});
|
|
3124
|
-
modelInstances.push(model);
|
|
3125
|
-
}
|
|
3126
|
-
}
|
|
3127
|
-
logger(`Model.createBySchemaId: Found ${modelInstances.length} models for schema "${schemaIdentifier}"`);
|
|
3128
|
-
return modelInstances;
|
|
3129
|
-
}
|
|
3130
|
-
catch (error) {
|
|
3131
|
-
logger(`Model.createBySchemaId: Error looking up models for schema "${schemaIdentifier}": ${error instanceof Error ? error.message : String(error)}`);
|
|
3132
|
-
return [];
|
|
3133
|
-
}
|
|
3134
|
-
}
|
|
3135
|
-
/**
|
|
3136
|
-
* Get all Model instances, optionally filtered by schema.
|
|
3137
|
-
* When DB is available, loads from DB via getModelsData; otherwise returns from cache.
|
|
3138
|
-
* With waitForReady, waits for each model to be ready (idle, with any properties from Model.create
|
|
3139
|
-
* created) and leaves out models that error or time out. Takes no holds: a caller that keeps a
|
|
3140
|
-
* model uses `Model.find` or `Model.create`.
|
|
3141
|
-
*/
|
|
3142
|
-
static async all(schemaName, options) {
|
|
3143
|
-
const { waitForReady = false, readyTimeout = DEFAULT_READY_TIMEOUT_MS } = options ?? {};
|
|
3144
|
-
const appDb = BaseDb.getAppDb();
|
|
3145
|
-
if (!appDb) {
|
|
3146
|
-
const instances = [];
|
|
3147
|
-
const seen = new Set();
|
|
3148
|
-
if (schemaName !== undefined && schemaName !== '') {
|
|
3149
|
-
for (const [nameKey, id] of this.instanceCacheByName.entries()) {
|
|
3150
|
-
const [cachedSchemaName] = nameKey.split(':');
|
|
3151
|
-
if (cachedSchemaName === schemaName) {
|
|
3152
|
-
const instance = this.peek(id);
|
|
3153
|
-
if (instance && !seen.has(id)) {
|
|
3154
|
-
instances.push(instance);
|
|
3155
|
-
seen.add(id);
|
|
3156
|
-
}
|
|
3157
|
-
}
|
|
3158
|
-
}
|
|
3159
|
-
for (const [nameKey, entry] of this.instanceCache.entries()) {
|
|
3160
|
-
const [cachedSchemaName] = nameKey.split(':');
|
|
3161
|
-
if (cachedSchemaName === schemaName) {
|
|
3162
|
-
const context = entry.instance._getSnapshotContext();
|
|
3163
|
-
const id = context.id;
|
|
3164
|
-
if (id && !seen.has(id)) {
|
|
3165
|
-
instances.push(entry.instance);
|
|
3166
|
-
seen.add(id);
|
|
3167
|
-
}
|
|
3168
|
-
}
|
|
3169
|
-
}
|
|
3170
|
-
}
|
|
3171
|
-
else {
|
|
3172
|
-
for (const [id, entry] of this.instanceCacheById.entries()) {
|
|
3173
|
-
if (!seen.has(id)) {
|
|
3174
|
-
instances.push(entry.instance);
|
|
3175
|
-
seen.add(id);
|
|
3176
|
-
}
|
|
3177
|
-
}
|
|
3178
|
-
for (const entry of this.instanceCache.values()) {
|
|
3179
|
-
const context = entry.instance._getSnapshotContext();
|
|
3180
|
-
const id = context.id;
|
|
3181
|
-
if (id && !seen.has(id)) {
|
|
3182
|
-
instances.push(entry.instance);
|
|
3183
|
-
seen.add(id);
|
|
3184
|
-
}
|
|
3185
|
-
}
|
|
3186
|
-
}
|
|
3187
|
-
return instances;
|
|
3188
|
-
}
|
|
3189
|
-
const rows = await getModelsData(schemaName);
|
|
3190
|
-
const instances = [];
|
|
3191
|
-
const seen = new Set();
|
|
3192
|
-
for (const row of rows) {
|
|
3193
|
-
// A model can appear more than once (e.g. duplicate model_schemas links); return it once.
|
|
3194
|
-
if (row.schemaFileId && !seen.has(row.schemaFileId)) {
|
|
3195
|
-
const instance = await this.createById(row.schemaFileId, { hold: false });
|
|
3196
|
-
if (instance && !seen.has(instance.id ?? row.schemaFileId)) {
|
|
3197
|
-
instances.push(instance);
|
|
3198
|
-
seen.add(instance.id ?? row.schemaFileId);
|
|
3199
|
-
}
|
|
3200
|
-
}
|
|
3201
|
-
}
|
|
3202
|
-
// Merge in any instances from cache for this schema (e.g. just-created model not yet visible in DB read).
|
|
3203
|
-
if (schemaName !== undefined && schemaName !== '') {
|
|
3204
|
-
for (const [nameKey, id] of this.instanceCacheByName.entries()) {
|
|
3205
|
-
const [cachedSchemaName] = nameKey.split(':');
|
|
3206
|
-
if (cachedSchemaName === schemaName && id && !seen.has(id)) {
|
|
3207
|
-
const instance = this.peek(id);
|
|
3208
|
-
if (instance) {
|
|
3209
|
-
instances.push(instance);
|
|
3210
|
-
seen.add(id);
|
|
3211
|
-
}
|
|
3212
|
-
}
|
|
3213
|
-
}
|
|
3214
|
-
for (const [nameKey, entry] of this.instanceCache.entries()) {
|
|
3215
|
-
const [cachedSchemaName] = nameKey.split(':');
|
|
3216
|
-
if (cachedSchemaName === schemaName) {
|
|
3217
|
-
const context = entry.instance._getSnapshotContext();
|
|
3218
|
-
const id = context.id;
|
|
3219
|
-
if (id && !seen.has(id)) {
|
|
3220
|
-
instances.push(entry.instance);
|
|
3221
|
-
seen.add(id);
|
|
3222
|
-
}
|
|
3223
|
-
}
|
|
3224
|
-
}
|
|
3225
|
-
}
|
|
3226
|
-
if (waitForReady && instances.length > 0) {
|
|
3227
|
-
// Leave out models that errored or didn't get ready in time. Don't re-read their state afterwards:
|
|
3228
|
-
// a ready model can already be validating again, and it still belongs in the result.
|
|
3229
|
-
const ready = await Promise.all(instances.map((m) => this.waitUntilReady(m, readyTimeout).then(() => true, (error) => {
|
|
3230
|
-
logger(`Model.all: leaving out a model that isn't ready: ${error instanceof Error ? error.message : String(error)}`);
|
|
3231
|
-
return false;
|
|
3232
|
-
})));
|
|
3233
|
-
return instances.filter((_, i) => ready[i]);
|
|
3234
|
-
}
|
|
3235
|
-
return instances;
|
|
3236
|
-
}
|
|
3237
|
-
/**
|
|
3238
|
-
* Update name index when model name changes
|
|
3239
|
-
*/
|
|
3240
|
-
static updateNameIndex(oldName, newName, schemaName, modelFileId) {
|
|
3241
|
-
const oldKey = `${schemaName}:${oldName}`;
|
|
3242
|
-
const newKey = `${schemaName}:${newName}`;
|
|
3243
|
-
// Update name index
|
|
3244
|
-
if (this.instanceCacheByName.has(oldKey)) {
|
|
3245
|
-
this.instanceCacheByName.delete(oldKey);
|
|
3246
|
-
}
|
|
3247
|
-
this.instanceCacheByName.set(newKey, modelFileId);
|
|
3248
|
-
// Also update legacy cache if it exists
|
|
3249
|
-
if (this.instanceCache.has(oldKey)) {
|
|
3250
|
-
const entry = this.instanceCache.get(oldKey);
|
|
3251
|
-
this.instanceCache.delete(oldKey);
|
|
3252
|
-
this.instanceCache.set(newKey, entry);
|
|
3253
|
-
}
|
|
3254
|
-
logger(`Model.updateNameIndex: Updated name mapping from "${oldName}" to "${newName}" (ID: ${modelFileId})`);
|
|
3255
|
-
}
|
|
3256
|
-
getService() {
|
|
3257
|
-
return this._service;
|
|
3258
|
-
}
|
|
3259
|
-
_getSnapshot() {
|
|
3260
|
-
return this._service.getSnapshot();
|
|
3261
|
-
}
|
|
3262
|
-
_getSnapshotContext() {
|
|
3263
|
-
return this._getSnapshot().context;
|
|
3264
|
-
}
|
|
3265
|
-
/**
|
|
3266
|
-
* Check for conflicts between actor context and database
|
|
3267
|
-
* @returns ConflictResult indicating if a conflict exists
|
|
3268
|
-
*/
|
|
3269
|
-
async _checkForConflicts() {
|
|
3270
|
-
const context = this._getSnapshotContext();
|
|
3271
|
-
// If we don't have load metadata, can't check for conflicts
|
|
3272
|
-
if (!context._dbUpdatedAt || !context.id) {
|
|
3273
|
-
return { hasConflict: false };
|
|
3274
|
-
}
|
|
3275
|
-
try {
|
|
3276
|
-
const db = BaseDb.getAppDb();
|
|
3277
|
-
if (!db) {
|
|
3278
|
-
return { hasConflict: false }; // Can't check without DB
|
|
3279
|
-
}
|
|
3280
|
-
// Get current DB record by id (schemaFileId) - most reliable lookup
|
|
3281
|
-
const dbModels = await db
|
|
3282
|
-
.select()
|
|
3283
|
-
.from(models)
|
|
3284
|
-
.where(eq(models.schemaFileId, context.id)) // id is now the schemaFileId (string)
|
|
3285
|
-
.limit(1);
|
|
3286
|
-
if (dbModels.length === 0) {
|
|
3287
|
-
return { hasConflict: false }; // No DB record, no conflict
|
|
3288
|
-
}
|
|
3289
|
-
// Check if DB was updated after we loaded
|
|
3290
|
-
// Note: models table doesn't have updatedAt, so we can't do timestamp comparison
|
|
3291
|
-
// For now, we'll just check if the record exists and matches
|
|
3292
|
-
// In the future, if models table gets updatedAt, we can compare timestamps
|
|
3293
|
-
return { hasConflict: false };
|
|
3294
|
-
}
|
|
3295
|
-
catch (error) {
|
|
3296
|
-
logger(`Error checking for conflicts: ${error instanceof Error ? error.message : String(error)}`);
|
|
3297
|
-
return { hasConflict: false }; // On error, assume no conflict to allow save
|
|
3298
|
-
}
|
|
3299
|
-
}
|
|
3300
|
-
/**
|
|
3301
|
-
* Saves model name changes to the database immediately (draft save)
|
|
3302
|
-
* This is called when modelName is changed to persist the change immediately
|
|
3303
|
-
* @param oldName - The old model name (if name changed)
|
|
3304
|
-
* @param newName - The new model name
|
|
3305
|
-
*/
|
|
3306
|
-
async _saveDraftToDb(oldName, newName) {
|
|
3307
|
-
saveDraftLogger(`_saveDraftToDb called for model (oldName: "${oldName}", newName: "${newName}")`);
|
|
3308
|
-
// Don't save during initialization - models are being loaded from files, not created as drafts
|
|
3309
|
-
// Check this FIRST before doing any expensive work like _getSnapshotContext()
|
|
3310
|
-
if (typeof window !== 'undefined') {
|
|
3311
|
-
const now = Date.now();
|
|
3312
|
-
// Use cached check to avoid expensive operations
|
|
3313
|
-
// Always check if cache is stale or if we previously got false (to allow recovery)
|
|
3314
|
-
const cacheIsStale = Model.cachedClientInitialized === null || (now - Model.clientCheckTime) > Model.CLIENT_CHECK_CACHE_MS;
|
|
3315
|
-
const shouldRecheck = cacheIsStale || Model.cachedClientInitialized === false;
|
|
3316
|
-
saveDraftLogger(`Client check: cacheIsStale=${cacheIsStale}, shouldRecheck=${shouldRecheck}, cachedValue=${Model.cachedClientInitialized}, timeSinceCheck=${now - Model.clientCheckTime}ms`);
|
|
3317
|
-
if (shouldRecheck) {
|
|
3318
|
-
try {
|
|
3319
|
-
// Use dynamic import for browser compatibility (require() doesn't work in browsers)
|
|
3320
|
-
const client = getClient();
|
|
3321
|
-
const clientSnapshot = client.getService().getSnapshot();
|
|
3322
|
-
// Check if state is IDLE (primary check) - isInitialized is set in entry action so should be true
|
|
3323
|
-
// But we check it as a secondary safeguard
|
|
3324
|
-
const isIdle = clientSnapshot.value === ClientManagerState.IDLE;
|
|
3325
|
-
const isInitialized = clientSnapshot.context.isInitialized;
|
|
3326
|
-
// If state is IDLE, trust it even if isInitialized isn't set yet (entry action should set it)
|
|
3327
|
-
// This aligns with useIsClientReady which only checks the state value
|
|
3328
|
-
Model.cachedClientInitialized = isIdle && (isInitialized !== false);
|
|
3329
|
-
Model.clientCheckTime = now;
|
|
3330
|
-
saveDraftLogger(`Client state checked: state=${clientSnapshot.value}, isIdle=${isIdle}, isInitialized=${isInitialized}, result=${Model.cachedClientInitialized}`);
|
|
3331
|
-
}
|
|
3332
|
-
catch (error) {
|
|
3333
|
-
// If we can't check client state, assume not initialized to be safe
|
|
3334
|
-
// But only cache for a short time to allow recovery
|
|
3335
|
-
Model.cachedClientInitialized = false;
|
|
3336
|
-
Model.clientCheckTime = now;
|
|
3337
|
-
saveDraftLogger(`Error checking client state: ${error instanceof Error ? error.message : String(error)}, setting cachedClientInitialized=false`);
|
|
3338
|
-
}
|
|
3339
|
-
}
|
|
3340
|
-
else {
|
|
3341
|
-
saveDraftLogger(`Using cached client state: cachedClientInitialized=${Model.cachedClientInitialized}`);
|
|
3342
|
-
}
|
|
3343
|
-
if (!Model.cachedClientInitialized) {
|
|
3344
|
-
saveDraftLogger(`Client not initialized, skipping save (oldName: "${oldName}", newName: "${newName}")`);
|
|
3345
|
-
// Skip silently during initialization to avoid log spam and reduce overhead
|
|
3346
|
-
return;
|
|
3347
|
-
}
|
|
3348
|
-
}
|
|
3349
|
-
saveDraftLogger(`Client is initialized, proceeding with save (oldName: "${oldName}", newName: "${newName}")`);
|
|
3350
|
-
// Check for conflicts before saving
|
|
3351
|
-
const conflictCheck = await this._checkForConflicts();
|
|
3352
|
-
if (conflictCheck.hasConflict) {
|
|
3353
|
-
const errorMessage = conflictCheck.message || 'Database was updated externally. Please reload the model and try again.';
|
|
3354
|
-
saveDraftLogger(`CONFLICT DETECTED: ${errorMessage}`);
|
|
3355
|
-
throw new ConflictError(errorMessage, conflictCheck);
|
|
3356
|
-
}
|
|
3357
|
-
const context = this._getSnapshotContext();
|
|
3358
|
-
const modelFileId = context.id || ''; // id is now the schemaFileId (string)
|
|
3359
|
-
const modelName = newName || context.modelName || oldName || '';
|
|
3360
|
-
// Use modelFileId as the key if available, otherwise use schemaName:modelName
|
|
3361
|
-
const saveKey = modelFileId || `${context.schemaName}:${modelName}`;
|
|
3362
|
-
// Prevent concurrent saves for the same model
|
|
3363
|
-
if (Model.savingModels.has(saveKey)) {
|
|
3364
|
-
saveDraftLogger(`Model ${modelName} (key: ${saveKey}) is already being saved, skipping concurrent save`);
|
|
3365
|
-
return;
|
|
3366
|
-
}
|
|
3367
|
-
Model.savingModels.add(saveKey);
|
|
3368
|
-
saveDraftLogger(`Starting save for model ${modelName} (key: ${saveKey}, id: ${modelFileId})`);
|
|
3369
|
-
try {
|
|
3370
|
-
const db = BaseDb.getAppDb();
|
|
3371
|
-
if (!db) {
|
|
3372
|
-
throw new Error('Database not found');
|
|
3373
|
-
}
|
|
3374
|
-
// If name changed, use renameModelInDb
|
|
3375
|
-
if (oldName && newName && oldName !== newName) {
|
|
3376
|
-
saveDraftLogger(`Renaming model from "${oldName}" to "${newName}"`);
|
|
3377
|
-
await renameModelInDb(oldName, newName, context.schemaName);
|
|
3378
|
-
saveDraftLogger(`Successfully renamed model from "${oldName}" to "${newName}"`);
|
|
3379
|
-
}
|
|
3380
|
-
else {
|
|
3381
|
-
// For other tracked properties, we would save here
|
|
3382
|
-
// Currently only modelName is saved immediately, so this is a no-op
|
|
3383
|
-
saveDraftLogger(`No name change detected, skipping save (modelName: "${modelName}")`);
|
|
3384
|
-
}
|
|
3385
|
-
// Update conflict detection metadata after successful save
|
|
3386
|
-
// Note: models table doesn't have updatedAt, so we use current timestamp
|
|
3387
|
-
const now = Date.now();
|
|
3388
|
-
this._service.send({
|
|
3389
|
-
type: 'updateContext',
|
|
3390
|
-
_dbUpdatedAt: now,
|
|
3391
|
-
});
|
|
3392
|
-
}
|
|
3393
|
-
catch (error) {
|
|
3394
|
-
logger(`Failed to save draft to database: ${error instanceof Error ? error.message : String(error)}`);
|
|
3395
|
-
// Don't throw - allow operation to continue
|
|
3396
|
-
}
|
|
3397
|
-
finally {
|
|
3398
|
-
// Always remove from saving set
|
|
3399
|
-
Model.savingModels.delete(saveKey);
|
|
3400
|
-
saveDraftLogger(`Finished save for model ${modelName} (key: ${saveKey})`);
|
|
3401
|
-
}
|
|
3402
|
-
}
|
|
3403
|
-
get status() {
|
|
3404
|
-
return this._getSnapshot().value;
|
|
3405
|
-
}
|
|
3406
|
-
/** The error from the last draft save a rename started, or undefined once a later one succeeds. */
|
|
3407
|
-
get lastSaveError() {
|
|
3408
|
-
return getDraftSaveError(this._service);
|
|
3409
|
-
}
|
|
3410
|
-
get validationErrors() {
|
|
3411
|
-
return this._getSnapshotContext()._validationErrors || [];
|
|
3412
|
-
}
|
|
3413
|
-
get isValid() {
|
|
3414
|
-
const errors = this.validationErrors;
|
|
3415
|
-
return errors.length === 0;
|
|
3416
|
-
}
|
|
3417
|
-
get isEdited() {
|
|
3418
|
-
const context = this._getSnapshotContext();
|
|
3419
|
-
// First check in-memory state
|
|
3420
|
-
if (context._isEdited !== undefined) {
|
|
3421
|
-
return context._isEdited;
|
|
3422
|
-
}
|
|
3423
|
-
// Fall back to reading from database if we have _dbId
|
|
3424
|
-
if (context._dbId) {
|
|
3425
|
-
// Load from database asynchronously (fire-and-forget for now)
|
|
3426
|
-
// For synchronous getter, we'll need to cache it or make it async
|
|
3427
|
-
// For now, return false if not in context
|
|
3428
|
-
return false;
|
|
3429
|
-
}
|
|
3430
|
-
return false;
|
|
3431
|
-
}
|
|
3432
|
-
get id() {
|
|
3433
|
-
return this._getSnapshotContext().id; // id is now the schemaFileId (string)
|
|
3434
|
-
}
|
|
3435
|
-
get name() {
|
|
3436
|
-
return this._getSnapshotContext().modelName;
|
|
3437
|
-
}
|
|
3438
|
-
/**
|
|
3439
|
-
* Returns ModelProperty instances for this model
|
|
3440
|
-
* This is a computed property that reads from the service context
|
|
3441
|
-
* Note: This is NOT reactive - use useModelProperties() hook for reactivity
|
|
3442
|
-
*/
|
|
3443
|
-
get properties() {
|
|
3444
|
-
const snapshot = this._service.getSnapshot();
|
|
3445
|
-
const context = snapshot.context;
|
|
3446
|
-
// Get property IDs from liveQuery (stored in context, not instanceState)
|
|
3447
|
-
const liveQueryIds = context._liveQueryPropertyIds || [];
|
|
3448
|
-
// Get _dbId from context for pending writes lookup
|
|
3449
|
-
const dbId = context._dbId; // _dbId is the database integer ID
|
|
3450
|
-
// Get pending property IDs (synchronous - uses static Map)
|
|
3451
|
-
// Lazy import ModelProperty to avoid circular dependency
|
|
3452
|
-
const ModelProperty = getModelProperty();
|
|
3453
|
-
if (!ModelProperty) {
|
|
3454
|
-
// If ModelProperty is not yet loaded, return empty array
|
|
3455
|
-
// It will be available on subsequent accesses after the async import completes
|
|
3456
|
-
return [];
|
|
3457
|
-
}
|
|
3458
|
-
const pendingIds = dbId ? ModelProperty.getPendingPropertyIds(dbId) : [];
|
|
3459
|
-
// Combine and deduplicate
|
|
3460
|
-
const allPropertyIds = [...new Set([...liveQueryIds, ...pendingIds])];
|
|
3461
|
-
// Get ModelProperty instances from static cache (synchronous)
|
|
3462
|
-
const propertyInstances = [];
|
|
3463
|
-
for (const propertyFileId of allPropertyIds) {
|
|
3464
|
-
const property = ModelProperty.getById(propertyFileId);
|
|
3465
|
-
if (property) {
|
|
3466
|
-
propertyInstances.push(property);
|
|
3467
|
-
}
|
|
3468
|
-
// Note: Cannot create properties asynchronously in this synchronous getter
|
|
3469
|
-
// Properties will be created elsewhere when needed
|
|
3470
|
-
}
|
|
3471
|
-
// Return a new array reference (snapshot at time of access)
|
|
3472
|
-
return [...propertyInstances];
|
|
3473
|
-
}
|
|
3474
|
-
/**
|
|
3475
|
-
* Validate the model
|
|
3476
|
-
* @returns Validation result
|
|
3477
|
-
*/
|
|
3478
|
-
async validate() {
|
|
3479
|
-
return new Promise((resolve) => {
|
|
3480
|
-
let resolved = false;
|
|
3481
|
-
const subscription = this._service.subscribe((snapshot) => {
|
|
3482
|
-
if (snapshot.value === 'idle' || snapshot.value === 'error') {
|
|
3483
|
-
if (!resolved) {
|
|
3484
|
-
resolved = true;
|
|
3485
|
-
subscription.unsubscribe();
|
|
3486
|
-
const errors = snapshot.context._validationErrors || [];
|
|
3487
|
-
resolve({
|
|
3488
|
-
isValid: errors.length === 0,
|
|
3489
|
-
errors,
|
|
3490
|
-
});
|
|
3491
|
-
}
|
|
3492
|
-
}
|
|
3493
|
-
});
|
|
3494
|
-
this._service.send({ type: 'validateModel' });
|
|
3495
|
-
// Timeout fallback to ensure we always resolve (15 seconds - longer than actor timeout)
|
|
3496
|
-
setTimeout(() => {
|
|
3497
|
-
if (!resolved) {
|
|
3498
|
-
resolved = true;
|
|
3499
|
-
subscription.unsubscribe();
|
|
3500
|
-
const errors = this._getSnapshotContext()._validationErrors || [];
|
|
3501
|
-
resolve({
|
|
3502
|
-
isValid: errors.length === 0,
|
|
3503
|
-
errors,
|
|
3504
|
-
});
|
|
3505
|
-
}
|
|
3506
|
-
}, 15000);
|
|
3507
|
-
});
|
|
3508
|
-
}
|
|
3509
|
-
/**
|
|
3510
|
-
* Create a new item instance from this model
|
|
3511
|
-
* Automatically injects the model name, so you don't need to pass it explicitly.
|
|
3512
|
-
*
|
|
3513
|
-
* @example
|
|
3514
|
-
* const Post = Model.create('Post', schema)
|
|
3515
|
-
* const post = await Post.create({ title: 'My Post', content: '...' })
|
|
3516
|
-
*
|
|
3517
|
-
* @param values - Item property values (modelName is automatically injected)
|
|
3518
|
-
* @returns The created item instance
|
|
3519
|
-
*/
|
|
3520
|
-
async create(values) {
|
|
3521
|
-
if (!this.modelName) {
|
|
3522
|
-
throw new Error('Cannot create item: model name is not set on this Model instance');
|
|
3523
|
-
}
|
|
3524
|
-
const item = await Item.create({
|
|
3525
|
-
modelName: this.modelName,
|
|
3526
|
-
schemaName: this.schemaName,
|
|
3527
|
-
modelInstance: this,
|
|
3528
|
-
...values,
|
|
3529
|
-
});
|
|
3530
|
-
return item;
|
|
3531
|
-
}
|
|
3532
|
-
/**
|
|
3533
|
-
* Reload model from database
|
|
3534
|
-
* This refreshes the actor context with the latest data from the database
|
|
3535
|
-
*/
|
|
3536
|
-
async reload() {
|
|
3537
|
-
logger(`Reloading model ${this.modelName} from database`);
|
|
3538
|
-
// Send reload event to machine (which will trigger loadOrCreateModel)
|
|
3539
|
-
this._service.send({ type: 'reloadFromDb' });
|
|
3540
|
-
// Wait for reload to complete
|
|
3541
|
-
return new Promise((resolve, reject) => {
|
|
3542
|
-
// Cleared once the reload settles: a pending timer kept Node scripts alive for 10 s.
|
|
3543
|
-
const timer = setTimeout(() => {
|
|
3544
|
-
subscription.unsubscribe();
|
|
3545
|
-
reject(new Error('Timeout waiting for model reload'));
|
|
3546
|
-
}, 10000);
|
|
3547
|
-
const subscription = this._service.subscribe((snapshot) => {
|
|
3548
|
-
if (snapshot.value === 'idle') {
|
|
3549
|
-
clearTimeout(timer);
|
|
3550
|
-
subscription.unsubscribe();
|
|
3551
|
-
resolve();
|
|
3552
|
-
}
|
|
3553
|
-
else if (snapshot.value === 'error') {
|
|
3554
|
-
clearTimeout(timer);
|
|
3555
|
-
subscription.unsubscribe();
|
|
3556
|
-
reject(new Error('Failed to reload model from database'));
|
|
3557
|
-
}
|
|
3558
|
-
});
|
|
3559
|
-
});
|
|
3560
|
-
}
|
|
3561
|
-
/**
|
|
3562
|
-
* Force-evict every cached Model belonging to a schema, ignoring refCounts, and stop their services.
|
|
3563
|
-
* Used by Schema.destroy(): once the DB rows are gone, cached instances would keep a dangling _dbId
|
|
3564
|
-
* and be handed back by Model.create/Model.all after the schema is re-imported.
|
|
3565
|
-
* @returns names of the evicted models (so callers can evict their ModelProperty instances)
|
|
3566
|
-
*/
|
|
3567
|
-
static evictForSchema(schemaName) {
|
|
3568
|
-
// Work already running for this schema's models must not re-cache them when it finishes.
|
|
3569
|
-
recordSchemaEviction(schemaName);
|
|
3570
|
-
const instances = new Set();
|
|
3571
|
-
for (const { instance } of this.instanceCacheById.values())
|
|
3572
|
-
instances.add(instance);
|
|
3573
|
-
for (const { instance } of this.instanceCache.values())
|
|
3574
|
-
instances.add(instance);
|
|
3575
|
-
const evictedNames = [];
|
|
3576
|
-
for (const instance of instances) {
|
|
3577
|
-
let context;
|
|
3578
|
-
try {
|
|
3579
|
-
context = instance._getSnapshotContext();
|
|
3580
|
-
}
|
|
3581
|
-
catch {
|
|
3582
|
-
continue;
|
|
3583
|
-
}
|
|
3584
|
-
if (context.schemaName !== schemaName)
|
|
3585
|
-
continue;
|
|
3586
|
-
const nameKey = `${context.schemaName}:${context.modelName}`;
|
|
3587
|
-
if (context.id)
|
|
3588
|
-
this.schemaHeldModelIds.delete(context.id);
|
|
3589
|
-
clearDestroySubscriptions(instance, { instanceState: modelInstanceState });
|
|
3590
|
-
forceRemoveFromCaches(instance, {
|
|
3591
|
-
getCacheKeys: () => (context.id ? [context.id, nameKey] : [nameKey]),
|
|
3592
|
-
caches: [
|
|
3593
|
-
Model.instanceCacheById,
|
|
3594
|
-
Model.instanceCacheByName,
|
|
3595
|
-
Model.instanceCache,
|
|
3596
|
-
],
|
|
3597
|
-
});
|
|
3598
|
-
try {
|
|
3599
|
-
instance._service.stop();
|
|
3600
|
-
}
|
|
3601
|
-
catch {
|
|
3602
|
-
// Service might already be stopped
|
|
3603
|
-
}
|
|
3604
|
-
if (context.modelName)
|
|
3605
|
-
evictedNames.push(context.modelName);
|
|
3606
|
-
}
|
|
3607
|
-
return evictedNames;
|
|
3608
|
-
}
|
|
3609
|
-
/**
|
|
3610
|
-
* Unload the model instance and clean up resources
|
|
3611
|
-
*/
|
|
3612
|
-
unload() {
|
|
3613
|
-
try {
|
|
3614
|
-
const context = this._getSnapshotContext();
|
|
3615
|
-
const modelFileId = context.id; // id is now the schemaFileId (string)
|
|
3616
|
-
const nameKey = `${context.schemaName}:${context.modelName}`;
|
|
3617
|
-
const cacheKeys = [];
|
|
3618
|
-
if (modelFileId) {
|
|
3619
|
-
cacheKeys.push(modelFileId);
|
|
3620
|
-
}
|
|
3621
|
-
cacheKeys.push(nameKey);
|
|
3622
|
-
unloadEntity(this, {
|
|
3623
|
-
getCacheKeys: () => cacheKeys,
|
|
3624
|
-
caches: [Model.instanceCacheById, Model.instanceCache],
|
|
3625
|
-
secondaryCaches: [Model.instanceCacheByName],
|
|
3626
|
-
instanceState: modelInstanceState,
|
|
3627
|
-
getService: (instance) => instance._service,
|
|
3628
|
-
});
|
|
3629
|
-
}
|
|
3630
|
-
catch (error) {
|
|
3631
|
-
logger(`Error during unload: ${error instanceof Error ? error.message : String(error)}`);
|
|
3632
|
-
// Still try to stop service
|
|
3633
|
-
try {
|
|
3634
|
-
this._service.stop();
|
|
3635
|
-
}
|
|
3636
|
-
catch {
|
|
3637
|
-
// Service might already be stopped
|
|
3638
|
-
}
|
|
3639
|
-
}
|
|
3640
|
-
}
|
|
3641
|
-
/**
|
|
3642
|
-
* Destroy the model instance completely: remove from caches, delete from database,
|
|
3643
|
-
* update Schema context, stop service. Uses shared destroy helpers.
|
|
3644
|
-
*/
|
|
3645
|
-
async destroy() {
|
|
3646
|
-
const context = this._getSnapshotContext();
|
|
3647
|
-
const modelFileId = context.id;
|
|
3648
|
-
const modelName = context.modelName;
|
|
3649
|
-
const schemaName = context.schemaName;
|
|
3650
|
-
const nameKey = schemaName && modelName ? `${schemaName}:${modelName}` : '';
|
|
3651
|
-
clearDestroySubscriptions(this, { instanceState: modelInstanceState });
|
|
3652
|
-
// Before the caches are cleared, so a lookup by id already reading the row doesn't re-cache it
|
|
3653
|
-
if (modelFileId)
|
|
3654
|
-
recordModelDestroy(modelFileId);
|
|
3655
|
-
if (modelFileId)
|
|
3656
|
-
Model.schemaHeldModelIds.delete(modelFileId);
|
|
3657
|
-
const cacheKeys = [];
|
|
3658
|
-
if (modelFileId)
|
|
3659
|
-
cacheKeys.push(modelFileId);
|
|
3660
|
-
if (nameKey)
|
|
3661
|
-
cacheKeys.push(nameKey);
|
|
3662
|
-
forceRemoveFromCaches(this, {
|
|
3663
|
-
getCacheKeys: () => cacheKeys,
|
|
3664
|
-
caches: [
|
|
3665
|
-
Model.instanceCacheById,
|
|
3666
|
-
Model.instanceCacheByName,
|
|
3667
|
-
Model.instanceCache,
|
|
3668
|
-
],
|
|
3669
|
-
});
|
|
3670
|
-
const destroying = runDestroyLifecycle(this, {
|
|
3671
|
-
getService: (instance) => instance._service,
|
|
3672
|
-
doDestroy: async () => {
|
|
3673
|
-
const db = BaseDb.getAppDb();
|
|
3674
|
-
if (db && modelFileId) {
|
|
3675
|
-
const drizzleMod = await import('drizzle-orm');
|
|
3676
|
-
const { eq } = drizzleMod;
|
|
3677
|
-
const modelRecords = await db
|
|
3678
|
-
.select()
|
|
3679
|
-
.from(models)
|
|
3680
|
-
.where(eq(models.schemaFileId, modelFileId))
|
|
3681
|
-
.limit(1);
|
|
3682
|
-
if (modelRecords.length > 0) {
|
|
3683
|
-
const modelId = modelRecords[0].id;
|
|
3684
|
-
const { detachModelRows } = await import('./detachRowsBeforeDelete-DXFYSHRR.js');
|
|
3685
|
-
await detachModelRows(db, [typeof modelId === 'string' ? parseInt(modelId, 10) : modelId]);
|
|
3686
|
-
await db.delete(modelSchemas).where(eq(modelSchemas.modelId, modelId));
|
|
3687
|
-
await db
|
|
3688
|
-
.delete(properties)
|
|
3689
|
-
.where(eq(properties.modelId, typeof modelId === 'string' ? parseInt(modelId, 10) : modelId));
|
|
3690
|
-
await db.delete(models).where(eq(models.id, modelId));
|
|
3691
|
-
}
|
|
3692
|
-
}
|
|
3693
|
-
if (schemaName && modelName) {
|
|
3694
|
-
// Imported here, not at module load: Model and Schema import each other.
|
|
3695
|
-
const _mod_11 = await import('./Schema-PFZwG9Nl.js');
|
|
3696
|
-
const _ns_11 = _mod_11.a;
|
|
3697
|
-
const { Schema } = _ns_11;
|
|
3698
|
-
const schema = Schema.create(schemaName, { waitForReady: false, hold: false });
|
|
3699
|
-
const schemaContext = schema.getService().getSnapshot().context;
|
|
3700
|
-
if (schemaContext.models?.[modelName]) {
|
|
3701
|
-
const updatedModels = { ...schemaContext.models };
|
|
3702
|
-
delete updatedModels[modelName];
|
|
3703
|
-
schema.getService().send({ type: 'updateContext', models: updatedModels });
|
|
3704
|
-
schema.getService().send({ type: 'markAsDraft', propertyKey: `model:${modelName}` });
|
|
3705
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3706
|
-
}
|
|
3707
|
-
}
|
|
3708
|
-
},
|
|
3709
|
-
});
|
|
3710
|
-
try {
|
|
3711
|
-
await destroying;
|
|
3712
|
-
}
|
|
3713
|
-
finally {
|
|
3714
|
-
if (modelFileId)
|
|
3715
|
-
finishModelDestroy(modelFileId);
|
|
3716
|
-
}
|
|
3717
|
-
}
|
|
3718
|
-
/**
|
|
3719
|
-
* Set up liveQuery subscription to watch for property changes in the database
|
|
3720
|
-
* This enables cross-instance synchronization (e.g., changes in other tabs/windows)
|
|
3721
|
-
*/
|
|
3722
|
-
_setupLiveQuerySubscription() {
|
|
3723
|
-
setupEntityLiveQuery(this, {
|
|
3724
|
-
getEntityId: async (model) => {
|
|
3725
|
-
const context = model._getSnapshotContext();
|
|
3726
|
-
let dbId = context._dbId; // _dbId is the database integer ID
|
|
3727
|
-
const modelName = context.modelName;
|
|
3728
|
-
if (!modelName) {
|
|
3729
|
-
return undefined;
|
|
3730
|
-
}
|
|
3731
|
-
// If we don't have _dbId yet, try to find it in the database
|
|
3732
|
-
if (!dbId) {
|
|
3733
|
-
try {
|
|
3734
|
-
const db = BaseDb.getAppDb();
|
|
3735
|
-
if (!db) {
|
|
3736
|
-
return undefined;
|
|
3737
|
-
}
|
|
3738
|
-
// Model names are only unique per schema: find this model's row by its id (schemaFileId).
|
|
3739
|
-
const _mod_12 = await import('./index-Dx_GTdyQ.js');
|
|
3740
|
-
const _ns_12 = _mod_12.be;
|
|
3741
|
-
const { resolveModelRecord } = _ns_12;
|
|
3742
|
-
const modelRecord = await resolveModelRecord(modelName, { modelFileId: context.id, schemaName: context.schemaName }, db);
|
|
3743
|
-
if (!modelRecord?.id) {
|
|
3744
|
-
return undefined;
|
|
3745
|
-
}
|
|
3746
|
-
// Until this model's own row is written, the lookup falls back to the name and can find a
|
|
3747
|
-
// same-named model in another schema. Taking that row would load its properties into this
|
|
3748
|
-
// model; wait for the row with this model's id instead (the caller retries).
|
|
3749
|
-
if (context.id && modelRecord.schemaFileId !== context.id) {
|
|
3750
|
-
return undefined;
|
|
3751
|
-
}
|
|
3752
|
-
dbId = modelRecord.id;
|
|
3753
|
-
// Update context with _dbId (unless the model was stopped while the lookup ran)
|
|
3754
|
-
if (isActorStopped(model._service))
|
|
3755
|
-
return undefined;
|
|
3756
|
-
model._service.send({
|
|
3757
|
-
type: 'updateContext',
|
|
3758
|
-
_dbId: dbId,
|
|
3759
|
-
});
|
|
3760
|
-
}
|
|
3761
|
-
catch (error) {
|
|
3762
|
-
logger(`[Model._setupLiveQuerySubscription] Error finding model: ${error}`);
|
|
3763
|
-
return undefined;
|
|
3764
|
-
}
|
|
3765
|
-
}
|
|
3766
|
-
return dbId;
|
|
3767
|
-
},
|
|
3768
|
-
buildQuery: async (modelId) => {
|
|
3769
|
-
return BaseDb.liveQuery((sql) => sql `
|
|
3770
|
-
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
|
|
3771
|
-
FROM properties
|
|
3772
|
-
WHERE model_id = ${modelId}
|
|
3773
|
-
`);
|
|
3774
|
-
},
|
|
3775
|
-
extractEntityIds: (rows) => rows
|
|
3776
|
-
.map(row => row.schemaFileId)
|
|
3777
|
-
.filter((id) => id !== null && id !== undefined),
|
|
3778
|
-
updateContext: (model, ids) => {
|
|
3779
|
-
model._service.send({
|
|
3780
|
-
type: 'updateContext',
|
|
3781
|
-
_liveQueryPropertyIds: ids, // Store in service context for reactivity
|
|
3782
|
-
_propertiesUpdated: Date.now(), // Internal field for tracking
|
|
3783
|
-
});
|
|
3784
|
-
},
|
|
3785
|
-
createChildInstances: async (ids) => {
|
|
3786
|
-
await modelPropertyImportPromise;
|
|
3787
|
-
const ModelProperty = getModelProperty();
|
|
3788
|
-
if (!ModelProperty) {
|
|
3789
|
-
logger('[Model._setupLiveQuerySubscription] ModelProperty not yet loaded');
|
|
3790
|
-
return;
|
|
3791
|
-
}
|
|
3792
|
-
const createPromises = ids.map(async (propertyFileId) => {
|
|
3793
|
-
try {
|
|
3794
|
-
await ModelProperty.createById(propertyFileId);
|
|
3795
|
-
}
|
|
3796
|
-
catch (error) {
|
|
3797
|
-
logger(`[Model._setupLiveQuerySubscription] Error creating ModelProperty instance for propertyFileId "${propertyFileId}": ${error}`);
|
|
3798
|
-
}
|
|
3799
|
-
});
|
|
3800
|
-
await Promise.all(createPromises);
|
|
3801
|
-
},
|
|
3802
|
-
queryInitialData: async (modelId) => {
|
|
3803
|
-
const db = BaseDb.getAppDb();
|
|
3804
|
-
if (!db) {
|
|
3805
|
-
return [];
|
|
3806
|
-
}
|
|
3807
|
-
const initialProperties = await db
|
|
3808
|
-
.select({ schemaFileId: properties.schemaFileId })
|
|
3809
|
-
.from(properties)
|
|
3810
|
-
.where(eq(properties.modelId, typeof modelId === 'string' ? parseInt(modelId, 10) : modelId));
|
|
3811
|
-
return initialProperties.map((row) => ({
|
|
3812
|
-
schemaFileId: row.schemaFileId,
|
|
3813
|
-
}));
|
|
3814
|
-
},
|
|
3815
|
-
// The model writes from its writeProcess child while itself idle; wait for that write to end.
|
|
3816
|
-
canRetryEntityId: (model) => {
|
|
3817
|
-
const writeProcess = model._getSnapshotContext().writeProcess;
|
|
3818
|
-
const writeState = writeProcess?.getSnapshot().value;
|
|
3819
|
-
return writeState !== 'validating' && writeState !== 'writing';
|
|
3820
|
-
},
|
|
3821
|
-
instanceState: modelInstanceState,
|
|
3822
|
-
loggerName: 'seedSdk:model:liveQuery',
|
|
3823
|
-
});
|
|
3824
|
-
}
|
|
3825
|
-
/**
|
|
3826
|
-
* Refresh property IDs from database (useful in Node.js where liveQuery isn't available)
|
|
3827
|
-
*/
|
|
3828
|
-
async _refreshPropertiesFromDb() {
|
|
3829
|
-
const snapshot = this._service.getSnapshot();
|
|
3830
|
-
const dbId = snapshot.context._dbId; // _dbId is the database integer ID
|
|
3831
|
-
const modelName = snapshot.context.modelName;
|
|
3832
|
-
if (!dbId || !modelName) {
|
|
3833
|
-
return;
|
|
3834
|
-
}
|
|
3835
|
-
try {
|
|
3836
|
-
const db = BaseDb.getAppDb();
|
|
3837
|
-
if (!db) {
|
|
3838
|
-
return;
|
|
3839
|
-
}
|
|
3840
|
-
const propertyRows = await db
|
|
3841
|
-
.select({ schemaFileId: properties.schemaFileId })
|
|
3842
|
-
.from(properties)
|
|
3843
|
-
.where(eq(properties.modelId, dbId)); // Use _dbId (database integer ID)
|
|
3844
|
-
// Stopped while querying: don't re-cache ModelProperty instances for an evicted/unloaded model
|
|
3845
|
-
if (isActorStopped(this._service))
|
|
3846
|
-
return;
|
|
3847
|
-
const propertyIds = propertyRows
|
|
3848
|
-
.map((row) => row.schemaFileId)
|
|
3849
|
-
.filter((id) => id !== null && id !== undefined);
|
|
3850
|
-
// CRITICAL: Create ModelProperty instances BEFORE updating context
|
|
3851
|
-
// This ensures they're in the cache when the properties getter is called
|
|
3852
|
-
if (propertyIds.length > 0) {
|
|
3853
|
-
try {
|
|
3854
|
-
await modelPropertyImportPromise;
|
|
3855
|
-
const ModelProperty = getModelProperty();
|
|
3856
|
-
if (!ModelProperty) {
|
|
3857
|
-
logger('[Model._refreshPropertiesFromDb] ModelProperty not yet loaded');
|
|
3858
|
-
}
|
|
3859
|
-
else {
|
|
3860
|
-
const createPromises = propertyIds.map(async (propertyFileId) => {
|
|
3861
|
-
try {
|
|
3862
|
-
const property = await ModelProperty.createById(propertyFileId);
|
|
3863
|
-
if (property) {
|
|
3864
|
-
logger(`[Model._refreshPropertiesFromDb] Created/cached ModelProperty instance for propertyFileId "${propertyFileId}"`);
|
|
3865
|
-
}
|
|
3866
|
-
else {
|
|
3867
|
-
logger(`[Model._refreshPropertiesFromDb] ModelProperty.createById returned undefined for propertyFileId "${propertyFileId}"`);
|
|
3868
|
-
}
|
|
3869
|
-
}
|
|
3870
|
-
catch (error) {
|
|
3871
|
-
logger(`[Model._refreshPropertiesFromDb] Error creating ModelProperty instance for propertyFileId "${propertyFileId}": ${error}`);
|
|
3872
|
-
}
|
|
3873
|
-
});
|
|
3874
|
-
await Promise.all(createPromises);
|
|
3875
|
-
}
|
|
3876
|
-
}
|
|
3877
|
-
catch (error) {
|
|
3878
|
-
logger(`[Model._refreshPropertiesFromDb] Error importing ModelProperty or creating instances: ${error}`);
|
|
3879
|
-
}
|
|
3880
|
-
}
|
|
3881
|
-
// Update context with refreshed property IDs AFTER creating instances (the awaits above can
|
|
3882
|
-
// outlive the instance)
|
|
3883
|
-
if (isActorStopped(this._service))
|
|
3884
|
-
return;
|
|
3885
|
-
this._service.send({
|
|
3886
|
-
type: 'updateContext',
|
|
3887
|
-
_liveQueryPropertyIds: propertyIds,
|
|
3888
|
-
_propertiesUpdated: Date.now(),
|
|
3889
|
-
});
|
|
3890
|
-
}
|
|
3891
|
-
catch (error) {
|
|
3892
|
-
logger(`[Model._refreshPropertiesFromDb] Error refreshing properties: ${error}`);
|
|
3893
|
-
}
|
|
3894
|
-
}
|
|
3895
|
-
/**
|
|
3896
|
-
* Public method to refresh properties from database
|
|
3897
|
-
*/
|
|
3898
|
-
async refreshProperties() {
|
|
3899
|
-
await this._refreshPropertiesFromDb();
|
|
3900
|
-
}
|
|
3901
|
-
/**
|
|
3902
|
-
* Refresh the property ids of every cached Model whose DB row is `dbId`. Called when a property
|
|
3903
|
-
* write for that model finishes: until then `properties` includes it as a pending write, and in Node
|
|
3904
|
-
* (no reactive liveQuery) nothing else adds it to `_liveQueryPropertyIds`.
|
|
3905
|
-
*/
|
|
3906
|
-
static async refreshPropertiesForDbId(dbId) {
|
|
3907
|
-
const instances = new Set();
|
|
3908
|
-
for (const { instance } of this.instanceCacheById.values())
|
|
3909
|
-
instances.add(instance);
|
|
3910
|
-
for (const { instance } of this.instanceCache.values())
|
|
3911
|
-
instances.add(instance);
|
|
3912
|
-
for (const instance of instances) {
|
|
3913
|
-
let context;
|
|
3914
|
-
try {
|
|
3915
|
-
context = instance._getSnapshotContext();
|
|
3916
|
-
}
|
|
3917
|
-
catch {
|
|
3918
|
-
continue;
|
|
3919
|
-
}
|
|
3920
|
-
if (context._dbId !== dbId)
|
|
3921
|
-
continue;
|
|
3922
|
-
await instance._refreshPropertiesFromDb();
|
|
3923
|
-
}
|
|
3924
|
-
}
|
|
3925
|
-
}
|
|
3926
|
-
// Primary cache: ID-based (never changes on rename)
|
|
3927
|
-
Model.instanceCacheById = new Map();
|
|
3928
|
-
// Secondary index: name lookup (updated on rename)
|
|
3929
|
-
Model.instanceCacheByName = new Map();
|
|
3930
|
-
// Legacy cache: kept temporarily for backward compatibility during migration
|
|
3931
|
-
Model.instanceCache = new Map();
|
|
3932
|
-
// Static tracking for pending writes
|
|
3933
|
-
Model.pendingWrites = new Map();
|
|
3934
|
-
// Track which models are currently being saved to prevent concurrent saves
|
|
3935
|
-
Model.savingModels = new Set();
|
|
3936
|
-
// Cache client initialization state globally to avoid repeated checks
|
|
3937
|
-
Model.cachedClientInitialized = null;
|
|
3938
|
-
Model.clientCheckTime = 0;
|
|
3939
|
-
Model.CLIENT_CHECK_CACHE_MS = 50; // Cache for 50ms to avoid excessive checks
|
|
3940
|
-
/**
|
|
3941
|
-
* Models their schema holds, by modelFileId. A schema's first load of a model takes one hold for
|
|
3942
|
-
* the schema, and the schema's last unload releases it (releaseSchemaHolds), so a user's unload
|
|
3943
|
-
* can't drop a model out of `schema.models` while the schema is loaded.
|
|
3944
|
-
*/
|
|
3945
|
-
Model.schemaHeldModelIds = new Set();
|
|
3946
|
-
|
|
3947
|
-
var Model$1 = /*#__PURE__*/Object.freeze({
|
|
3948
|
-
__proto__: null,
|
|
3949
|
-
Model: Model
|
|
3950
|
-
});
|
|
3951
|
-
|
|
3952
|
-
// Export the Model class
|
|
3953
|
-
const TModelValues = Type.Record(Type.String(), Type.Any());
|
|
3954
|
-
const TModelSchema = Type.Record(Type.String(), TProperty);
|
|
3955
|
-
|
|
3956
|
-
/**
|
|
3957
|
-
* Thrown by Item.create when values don't fit the model: unknown keys, wrong types, or values
|
|
3958
|
-
* that break the property's validation rules. Nothing is left written: most values are checked
|
|
3959
|
-
* before anything is written, and File/Image/Html/ItemStorage values, checked as they're saved,
|
|
3960
|
-
* have their draft removed.
|
|
3961
|
-
*/
|
|
3962
|
-
class ItemCreateValidationError extends Error {
|
|
3963
|
-
constructor(modelName, problems) {
|
|
3964
|
-
super(`Cannot create ${modelName}: ` + problems.map((p) => `${p.property}: ${p.message}`).join('; '));
|
|
3965
|
-
this.name = 'ItemCreateValidationError';
|
|
3966
|
-
this.modelName = modelName;
|
|
3967
|
-
this.problems = problems;
|
|
3968
|
-
Object.setPrototypeOf(this, ItemCreateValidationError.prototype);
|
|
3969
|
-
}
|
|
3970
|
-
}
|
|
3971
|
-
/**
|
|
3972
|
-
* Thrown by Item.create when a File/Image/Html/ItemStorage value couldn't be saved for a reason
|
|
3973
|
-
* other than its rules (those throw ItemCreateValidationError). The draft is removed, as for
|
|
3974
|
-
* ItemCreateValidationError. `cause` holds the save's own error, e.g. an OwnershipError.
|
|
3975
|
-
*/
|
|
3976
|
-
class ItemCreateError extends Error {
|
|
3977
|
-
constructor(message, details) {
|
|
3978
|
-
super(message, details.cause === undefined ? undefined : { cause: details.cause });
|
|
3979
|
-
this.name = 'ItemCreateError';
|
|
3980
|
-
this.code = details.code;
|
|
3981
|
-
this.modelName = details.modelName;
|
|
3982
|
-
this.property = details.property;
|
|
3983
|
-
this.seedLocalId = details.seedLocalId;
|
|
3984
|
-
Object.setPrototypeOf(this, ItemCreateError.prototype);
|
|
3985
|
-
}
|
|
3986
|
-
}
|
|
3987
|
-
/** Thrown by Item.create when no model with that name exists (in the given schema, if any). */
|
|
3988
|
-
class UnknownModelError extends Error {
|
|
3989
|
-
constructor(modelName, schemaName) {
|
|
3990
|
-
super(`Model "${modelName}" not found${schemaName ? ` in schema "${schemaName}"` : ''}.`);
|
|
3991
|
-
this.name = 'UnknownModelError';
|
|
3992
|
-
this.modelName = modelName;
|
|
3993
|
-
this.schemaName = schemaName;
|
|
3994
|
-
Object.setPrototypeOf(this, UnknownModelError.prototype);
|
|
3995
|
-
}
|
|
3996
|
-
}
|
|
3997
|
-
|
|
3998
|
-
var errors = /*#__PURE__*/Object.freeze({
|
|
3999
|
-
__proto__: null,
|
|
4000
|
-
ItemCreateError: ItemCreateError,
|
|
4001
|
-
ItemCreateValidationError: ItemCreateValidationError,
|
|
4002
|
-
UnknownModelError: UnknownModelError
|
|
4003
|
-
});
|
|
4004
|
-
|
|
4005
|
-
export { ConflictError as C, ItemCreateValidationError as I, Model as M, TModelSchema as T, UnknownModelError as U, TModelValues as a, anyEvictionSince as b, currentEvictionEpoch as c, ItemCreateError as d, setupEntityLiveQuery as e, finishSchemaDestroy as f, getDraftSaveError as g, writeToDatabase$1 as h, Model$1 as i, errors as j, recordSchemaDestroy as r, schemaEvictedSince as s, trackDraftSave as t, writeProcessMachine as w };
|
|
4006
|
-
//# sourceMappingURL=errors-D1zHAqjr.js.map
|