@seedprotocol/sdk 0.6.10 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BasePathResolver-B6CKqoKw.js → BasePathResolver-DRT09oVv.js} +36 -2
- package/dist/BasePathResolver-DRT09oVv.js.map +1 -0
- package/dist/ClientManager-DngaZdw6.js +4763 -0
- package/dist/ClientManager-DngaZdw6.js.map +1 -0
- package/dist/{ClientManager-ChcmUL47.js → Item-ZUAmf943.js} +1175 -5903
- package/dist/Item-ZUAmf943.js.map +1 -0
- package/dist/{ModelProperty-D79uV42p.js → ModelProperty-6llK4MTH.js} +326 -109
- package/dist/ModelProperty-6llK4MTH.js.map +1 -0
- package/dist/{Schema-a8uO-hIZ.js → Schema-D-Ud-XYw.js} +164 -885
- package/dist/Schema-D-Ud-XYw.js.map +1 -0
- package/dist/{SchemaValidationService-Y49vqast.js → SchemaValidationService-IoS7JquP.js} +26 -18
- package/dist/SchemaValidationService-IoS7JquP.js.map +1 -0
- package/dist/{backfillMetadataPropertyIds-CBsf7LnU.js → backfillMetadataPropertyIds-BlVeE7w7.js} +11 -6
- package/dist/backfillMetadataPropertyIds-BlVeE7w7.js.map +1 -0
- package/dist/cjs/{ModelProperty-D9FGaGuE.js → ModelProperty-Dv7AQfmQ.js} +297 -89
- package/dist/cjs/ModelProperty-Dv7AQfmQ.js.map +1 -0
- package/dist/cjs/{Schema-BiJIPLEL.js → Schema-BHVp1g-M.js} +136 -868
- package/dist/cjs/Schema-BHVp1g-M.js.map +1 -0
- package/dist/cjs/{SchemaValidationService-DtYs_CNY.js → SchemaValidationService-BSU9knjf.js} +18 -11
- package/dist/cjs/SchemaValidationService-BSU9knjf.js.map +1 -0
- package/dist/cjs/{backfillMetadataPropertyIds-2XaN3b9S.js → backfillMetadataPropertyIds-WIovQ_d8.js} +8 -3
- package/dist/cjs/backfillMetadataPropertyIds-WIovQ_d8.js.map +1 -0
- package/dist/cjs/{eas-CMNppmGE.js → eas-CUYXdr2t.js} +4 -4
- package/dist/cjs/{eas-CMNppmGE.js.map → eas-CUYXdr2t.js.map} +1 -1
- package/dist/cjs/{getItem-B6jEINjW.js → getItem-z1tHu0hu.js} +2 -2
- package/dist/cjs/{getItem-B6jEINjW.js.map → getItem-z1tHu0hu.js.map} +1 -1
- package/dist/cjs/getLatestVersionRow-CcaabxIW.js +59 -0
- package/dist/cjs/getLatestVersionRow-CcaabxIW.js.map +1 -0
- package/dist/cjs/{getPublishPayload-D5DhLGvN.js → getPublishPayload-CEP_e-Kv.js} +283 -138
- package/dist/cjs/getPublishPayload-CEP_e-Kv.js.map +1 -0
- package/dist/cjs/{getPublishUploads-DMhCCKYF.js → getPublishUploads-Ctn5k1k6.js} +65 -19
- package/dist/cjs/getPublishUploads-Ctn5k1k6.js.map +1 -0
- package/dist/cjs/{index-CSNtEnbF.js → index-Cvvt0-5B.js} +2698 -1725
- package/dist/cjs/index-Cvvt0-5B.js.map +1 -0
- package/dist/cjs/{index-DPp9rdRL.js → index-DfGhF1dc.js} +3 -3
- package/dist/cjs/index-DfGhF1dc.js.map +1 -0
- package/dist/cjs/{property-DlKiAWt4.js → property-DbEPQ1m9.js} +40 -35
- package/dist/cjs/property-DbEPQ1m9.js.map +1 -0
- package/dist/cjs/publishDraftGraph-C38gf302.js +403 -0
- package/dist/cjs/publishDraftGraph-C38gf302.js.map +1 -0
- package/dist/cjs/resetAmbiguousLegacyItemData-CfMhRHwK.js +116 -0
- package/dist/cjs/resetAmbiguousLegacyItemData-CfMhRHwK.js.map +1 -0
- package/dist/cjs/resolveModelForSyncedSeed-CNYaLOir.js +49 -0
- package/dist/cjs/resolveModelForSyncedSeed-CNYaLOir.js.map +1 -0
- package/dist/cjs/syncDbWithEas-d1iOdTTI.js +886 -0
- package/dist/cjs/syncDbWithEas-d1iOdTTI.js.map +1 -0
- package/dist/{configurePlatform-CE0Vew17.js → configurePlatform-CZpjyuCo.js} +4 -4
- package/dist/{configurePlatform-CE0Vew17.js.map → configurePlatform-CZpjyuCo.js.map} +1 -1
- package/dist/db/drizzle/0015_add_model_file_id_to_seeds.sql +11 -0
- package/dist/db/drizzle/0016_add_revoked_at_to_metadata.sql +1 -0
- package/dist/db/drizzle/0017_add_revoked_at_to_versions.sql +1 -0
- package/dist/db/drizzle/0018_add_storage_settings_to_properties.sql +53 -0
- package/dist/db/drizzle/0019_add_derived_from_uid_to_metadata.sql +35 -0
- package/dist/db/drizzle/meta/0011_snapshot.json +1182 -0
- package/dist/db/drizzle/meta/0012_snapshot.json +1244 -0
- package/dist/db/drizzle/meta/0013_snapshot.json +63 -1
- package/dist/db/drizzle/meta/0014_snapshot.json +62 -0
- package/dist/db/drizzle/meta/0015_snapshot.json +1338 -0
- package/dist/db/drizzle/meta/0016_snapshot.json +1345 -0
- package/dist/db/drizzle/meta/0017_snapshot.json +1352 -0
- package/dist/db/drizzle/meta/0018_snapshot.json +1373 -0
- package/dist/db/drizzle/meta/0019_snapshot.json +1380 -0
- package/dist/db/drizzle/meta/_journal.json +35 -0
- package/dist/{db-WbrzEf7c.js → db-Lw0KVZmc.js} +546 -106
- package/dist/db-Lw0KVZmc.js.map +1 -0
- package/dist/{eas-CfYCu4IX.js → eas-CQ-Smzk2.js} +7 -7
- package/dist/eas-CQ-Smzk2.js.map +1 -0
- package/dist/errors-CrBxGWah.js +17 -0
- package/dist/errors-CrBxGWah.js.map +1 -0
- package/dist/{getItem-Br-bGNFO.js → getItem-CSfSok9f.js} +8 -10
- package/dist/{getItem-Br-bGNFO.js.map → getItem-CSfSok9f.js.map} +1 -1
- package/dist/getLatestVersionRow-72Atl9vB.js +46 -0
- package/dist/getLatestVersionRow-72Atl9vB.js.map +1 -0
- package/dist/{getPublishPayload-BIrTVUxb.js → getPublishPayload-76_KfB5Y.js} +297 -154
- package/dist/getPublishPayload-76_KfB5Y.js.map +1 -0
- package/dist/{getPublishUploads-C6cDeZdz.js → getPublishUploads-CqgMBD8f.js} +68 -21
- package/dist/getPublishUploads-CqgMBD8f.js.map +1 -0
- package/dist/{index-Djd3667v.js → index-5b3JsS55.js} +2 -2
- package/dist/index-5b3JsS55.js.map +1 -0
- package/dist/{index-ClSv5pTA.js → index-BpgD305l.js} +12 -11
- package/dist/index-BpgD305l.js.map +1 -0
- package/dist/{index-HRHkbVQd.js → index-Y-yYyRBf.js} +530 -196
- package/dist/index-Y-yYyRBf.js.map +1 -0
- package/dist/index.browser-DaTzn79x.js +4139 -0
- package/dist/index.browser-DaTzn79x.js.map +1 -0
- package/dist/{index.node-DBtQkEfz.js → index.node-NYKqFOoe.js} +9 -4
- package/dist/index.node-NYKqFOoe.js.map +1 -0
- package/dist/main.cjs +1 -1
- package/dist/main.js +886 -560
- package/dist/main.js.map +1 -1
- package/dist/node.js +19 -15
- package/dist/node.js.map +1 -1
- package/dist/platform.browser.js +18 -3719
- package/dist/platform.browser.js.map +1 -1
- package/dist/platform.node.js +3 -3
- package/dist/{property-CQjuZ4JW.js → property-ByaiSyUK.js} +53 -48
- package/dist/property-ByaiSyUK.js.map +1 -0
- package/dist/publishDraftGraph-BRm6Pq3Q.js +405 -0
- package/dist/publishDraftGraph-BRm6Pq3Q.js.map +1 -0
- package/dist/resetAmbiguousLegacyItemData-B4dX6ZA1.js +88 -0
- package/dist/resetAmbiguousLegacyItemData-B4dX6ZA1.js.map +1 -0
- package/dist/resolveModelForSyncedSeed-B0tco3zG.js +46 -0
- package/dist/resolveModelForSyncedSeed-B0tco3zG.js.map +1 -0
- package/dist/seedSchema/MetadataSchema.ts +9 -0
- package/dist/seedSchema/ModelSchema.ts +5 -0
- package/dist/seedSchema/SeedSchema.ts +3 -0
- package/dist/seedSchema/VersionSchema.ts +5 -0
- package/dist/src/Item/Item.d.ts +21 -0
- package/dist/src/Item/Item.d.ts.map +1 -1
- package/dist/src/Item/service/actors/loadOrCreateItem.d.ts.map +1 -1
- package/dist/src/Item/service/actors/waitForDb.d.ts.map +1 -1
- package/dist/src/Item/service/itemMachineSingle.d.ts +3 -29
- package/dist/src/Item/service/itemMachineSingle.d.ts.map +1 -1
- package/dist/src/ItemProperty/ItemProperty.d.ts +13 -4
- package/dist/src/ItemProperty/ItemProperty.d.ts.map +1 -1
- package/dist/src/ItemProperty/service/actors/hydrateFromDb.d.ts.map +1 -1
- package/dist/src/ItemProperty/service/actors/loadOrCreateProperty.d.ts.map +1 -1
- package/dist/src/ItemProperty/service/actors/saveValueToDb/saveFile.d.ts.map +1 -1
- package/dist/src/ItemProperty/service/actors/saveValueToDb/saveHtml.d.ts.map +1 -1
- package/dist/src/ItemProperty/service/actors/saveValueToDb/saveImage.d.ts.map +1 -1
- package/dist/src/ItemProperty/service/actors/saveValueToDb/saveItemStorage.d.ts.map +1 -1
- package/dist/src/ItemProperty/service/propertyMachine.d.ts +4 -9
- package/dist/src/ItemProperty/service/propertyMachine.d.ts.map +1 -1
- package/dist/src/Model/Model.d.ts +52 -2
- package/dist/src/Model/Model.d.ts.map +1 -1
- package/dist/src/Model/errors.d.ts +10 -0
- package/dist/src/Model/errors.d.ts.map +1 -0
- package/dist/src/Model/service/actors/createModelProperties.d.ts.map +1 -1
- package/dist/src/Model/service/actors/loadOrCreateModel.d.ts.map +1 -1
- package/dist/src/Model/service/actors/validateModel.d.ts.map +1 -1
- package/dist/src/Model/service/modelMachine.d.ts +0 -8
- package/dist/src/Model/service/modelMachine.d.ts.map +1 -1
- package/dist/src/ModelProperty/ModelProperty.d.ts +42 -2
- 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/saveToSchema.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 +2 -9
- package/dist/src/ModelProperty/service/modelPropertyMachine.d.ts.map +1 -1
- package/dist/src/Schema/Schema.d.ts +8 -2
- package/dist/src/Schema/Schema.d.ts.map +1 -1
- package/dist/src/Schema/service/actors/checkExistingSchema.d.ts.map +1 -1
- package/dist/src/Schema/service/actors/createModelInstances.d.ts.map +1 -1
- package/dist/src/Schema/service/actors/loadOrCreateSchema.d.ts.map +1 -1
- package/dist/src/Schema/service/schemaMachine.d.ts +1 -217
- package/dist/src/Schema/service/schemaMachine.d.ts.map +1 -1
- package/dist/src/Schema/service/validation/SchemaValidationService.d.ts.map +1 -1
- package/dist/src/browser/db/Db.d.ts +3 -0
- package/dist/src/browser/db/Db.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/db/embeddedMigrations.d.ts +11 -2
- package/dist/src/browser/db/embeddedMigrations.d.ts.map +1 -1
- package/dist/src/browser/db/sqliteBusyRetry.d.ts +14 -0
- package/dist/src/browser/db/sqliteBusyRetry.d.ts.map +1 -0
- package/dist/src/browser/helpers/FileManager.d.ts +1 -0
- package/dist/src/browser/helpers/FileManager.d.ts.map +1 -1
- package/dist/src/browser/helpers/QueryClient.d.ts +1 -0
- package/dist/src/browser/helpers/QueryClient.d.ts.map +1 -1
- package/dist/src/browser/helpers/opfsErrors.d.ts +2 -0
- package/dist/src/browser/helpers/opfsErrors.d.ts.map +1 -0
- package/dist/src/browser/helpers/tolerantWebAccess.d.ts +47 -0
- package/dist/src/browser/helpers/tolerantWebAccess.d.ts.map +1 -0
- package/dist/src/browser/workers/FileDownloader.d.ts.map +1 -1
- package/dist/src/browser/workers/ImageResizer.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.map +1 -1
- package/dist/src/client/actors/addModelsToDb.d.ts +14 -0
- package/dist/src/client/actors/addModelsToDb.d.ts.map +1 -1
- package/dist/src/client/actors/platformClassesInit.d.ts.map +1 -1
- package/dist/src/client/actors/processSchemaFiles.d.ts.map +1 -1
- package/dist/src/client/actors/saveConfig.d.ts +11 -0
- package/dist/src/client/actors/saveConfig.d.ts.map +1 -1
- package/dist/src/client/constants.d.ts +0 -8
- package/dist/src/client/constants.d.ts.map +1 -1
- package/dist/src/db/backfillMetadataPropertyIds.d.ts.map +1 -1
- package/dist/src/db/configs/{dev.schema.config.d.ts → migrations.config.d.ts} +1 -1
- package/dist/src/db/configs/migrations.config.d.ts.map +1 -0
- package/dist/src/db/read/batchLatestPublishedVersionBySeedLocalIds.d.ts +1 -1
- package/dist/src/db/read/batchLatestPublishedVersionBySeedLocalIds.d.ts.map +1 -1
- package/dist/src/db/read/getItemData.d.ts.map +1 -1
- package/dist/src/db/read/getItemProperties.d.ts.map +1 -1
- package/dist/src/db/read/getItems.d.ts +7 -2
- package/dist/src/db/read/getItems.d.ts.map +1 -1
- package/dist/src/db/read/getLatestPublishedVersionRow.d.ts +2 -1
- package/dist/src/db/read/getLatestPublishedVersionRow.d.ts.map +1 -1
- package/dist/src/db/read/getLatestVersionRow.d.ts +13 -0
- package/dist/src/db/read/getLatestVersionRow.d.ts.map +1 -0
- package/dist/src/db/read/getModelSchemas.d.ts.map +1 -1
- package/dist/src/db/read/getPropertyData.d.ts.map +1 -1
- package/dist/src/db/read/getPublishPayload.d.ts +18 -11
- package/dist/src/db/read/getPublishPayload.d.ts.map +1 -1
- package/dist/src/db/read/getPublishPendingDiff.d.ts +2 -1
- package/dist/src/db/read/getPublishPendingDiff.d.ts.map +1 -1
- package/dist/src/db/read/getPublishUploads.d.ts +5 -0
- 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 +8 -2
- package/dist/src/db/read/getSeedPublishState.d.ts.map +1 -1
- package/dist/src/db/read/isSeedRevoked.d.ts +7 -0
- package/dist/src/db/read/isSeedRevoked.d.ts.map +1 -0
- package/dist/src/db/read/publishDraftGraph.d.ts +57 -0
- package/dist/src/db/read/publishDraftGraph.d.ts.map +1 -0
- package/dist/src/db/read/publishErrors.d.ts +37 -0
- package/dist/src/db/read/publishErrors.d.ts.map +1 -0
- package/dist/src/db/read/resolveModelForSyncedSeed.d.ts +39 -0
- package/dist/src/db/read/resolveModelForSyncedSeed.d.ts.map +1 -0
- package/dist/src/db/read/resolveModelRecord.d.ts +69 -0
- package/dist/src/db/read/resolveModelRecord.d.ts.map +1 -0
- package/dist/src/db/read/resolveRelatedSeedRef.d.ts +34 -0
- package/dist/src/db/read/resolveRelatedSeedRef.d.ts.map +1 -0
- package/dist/src/db/read/subqueries/liveVersion.d.ts +12 -0
- package/dist/src/db/read/subqueries/liveVersion.d.ts.map +1 -0
- package/dist/src/db/read/subqueries/metadataLatest.d.ts +4 -0
- package/dist/src/db/read/subqueries/metadataLatest.d.ts.map +1 -1
- package/dist/src/db/read/subqueries/versionData.d.ts +7 -2
- package/dist/src/db/read/subqueries/versionData.d.ts.map +1 -1
- package/dist/src/db/read/summarizePublishWork.d.ts +10 -2
- package/dist/src/db/read/summarizePublishWork.d.ts.map +1 -1
- package/dist/src/db/resetAmbiguousLegacyItemData.d.ts +17 -0
- package/dist/src/db/resetAmbiguousLegacyItemData.d.ts.map +1 -0
- package/dist/src/db/sqlParamBatches.d.ts +27 -0
- package/dist/src/db/sqlParamBatches.d.ts.map +1 -0
- package/dist/src/db/write/addExcludedTransactions.d.ts +7 -0
- package/dist/src/db/write/addExcludedTransactions.d.ts.map +1 -0
- package/dist/src/db/write/applyPropertyAttestationUidsFromPublish.d.ts +6 -0
- package/dist/src/db/write/applyPropertyAttestationUidsFromPublish.d.ts.map +1 -1
- package/dist/src/db/write/createMetadata.d.ts +2 -0
- package/dist/src/db/write/createMetadata.d.ts.map +1 -1
- package/dist/src/db/write/createNewItem.d.ts +3 -1
- package/dist/src/db/write/createNewItem.d.ts.map +1 -1
- package/dist/src/db/write/createSeed.d.ts +2 -1
- package/dist/src/db/write/createSeed.d.ts.map +1 -1
- package/dist/src/db/write/createSeeds.d.ts +1 -1
- package/dist/src/db/write/createSeeds.d.ts.map +1 -1
- package/dist/src/db/write/linkModelToSchema.d.ts +12 -0
- package/dist/src/db/write/linkModelToSchema.d.ts.map +1 -0
- package/dist/src/db/write/updateItemPropertyValue.d.ts.map +1 -1
- package/dist/src/db/write/updateMetadata.d.ts.map +1 -1
- package/dist/src/db/write/updateSeedRevokedAt.d.ts +11 -1
- package/dist/src/db/write/updateSeedRevokedAt.d.ts.map +1 -1
- package/dist/src/db/write/updateSeedUid.d.ts.map +1 -1
- package/dist/src/db/write/updateVersionUid.d.ts +2 -1
- package/dist/src/db/write/updateVersionUid.d.ts.map +1 -1
- package/dist/src/events/files/download.d.ts.map +1 -1
- package/dist/src/events/item/easSyncManager.d.ts +2 -0
- package/dist/src/events/item/easSyncManager.d.ts.map +1 -1
- package/dist/src/events/item/index.d.ts.map +1 -1
- package/dist/src/events/item/syncDbWithEas.d.ts +16 -0
- package/dist/src/events/item/syncDbWithEas.d.ts.map +1 -1
- package/dist/src/helpers/FileManager/BaseFileManager.d.ts +1 -0
- package/dist/src/helpers/FileManager/BaseFileManager.d.ts.map +1 -1
- package/dist/src/helpers/FileManager/IFileManager.d.ts +5 -0
- package/dist/src/helpers/FileManager/IFileManager.d.ts.map +1 -1
- package/dist/src/helpers/FileManager/errors.d.ts +10 -0
- package/dist/src/helpers/FileManager/errors.d.ts.map +1 -0
- package/dist/src/helpers/compareMetadataRowsLatestFirst.d.ts +21 -3
- package/dist/src/helpers/compareMetadataRowsLatestFirst.d.ts.map +1 -1
- package/dist/src/helpers/constants.d.ts +9 -0
- package/dist/src/helpers/constants.d.ts.map +1 -1
- package/dist/src/helpers/crypto.d.ts +5 -0
- package/dist/src/helpers/crypto.d.ts.map +1 -1
- package/dist/src/helpers/db.d.ts +25 -5
- package/dist/src/helpers/db.d.ts.map +1 -1
- package/dist/src/helpers/easPropertyCanonical.d.ts +1 -1
- package/dist/src/helpers/easPropertyCanonical.d.ts.map +1 -1
- package/dist/src/helpers/entity/entityCommon.d.ts +10 -0
- package/dist/src/helpers/entity/entityCommon.d.ts.map +1 -1
- package/dist/src/helpers/entity/entityDestroy.d.ts +1 -1
- package/dist/src/helpers/entity/entityDestroy.d.ts.map +1 -1
- package/dist/src/helpers/entity/entityLiveQuery.d.ts +5 -0
- package/dist/src/helpers/entity/entityLiveQuery.d.ts.map +1 -1
- package/dist/src/helpers/entity/evictionEpoch.d.ts +18 -0
- package/dist/src/helpers/entity/evictionEpoch.d.ts.map +1 -0
- package/dist/src/helpers/file/fetchAll/actors.d.ts.map +1 -1
- package/dist/src/helpers/getSegmentedItemProperties.d.ts.map +1 -1
- package/dist/src/helpers/htmlEmbeddedDataUriPublish.d.ts +31 -3
- package/dist/src/helpers/htmlEmbeddedDataUriPublish.d.ts.map +1 -1
- package/dist/src/helpers/isPublishedMetadataRow.d.ts +10 -0
- package/dist/src/helpers/isPublishedMetadataRow.d.ts.map +1 -0
- package/dist/src/helpers/ownership.d.ts.map +1 -1
- package/dist/src/helpers/property.d.ts +6 -1
- package/dist/src/helpers/property.d.ts.map +1 -1
- package/dist/src/helpers/schema.d.ts +14 -4
- package/dist/src/helpers/schema.d.ts.map +1 -1
- package/dist/src/helpers/tabCoordinator.d.ts +45 -0
- package/dist/src/helpers/tabCoordinator.d.ts.map +1 -0
- package/dist/src/helpers/tabEvents.d.ts +27 -0
- package/dist/src/helpers/tabEvents.d.ts.map +1 -0
- package/dist/src/helpers/tabLocks.d.ts +28 -0
- package/dist/src/helpers/tabLocks.d.ts.map +1 -0
- package/dist/src/helpers/updateSchema.d.ts.map +1 -1
- package/dist/src/imports/json.d.ts.map +1 -1
- package/dist/src/imports/markdown.d.ts.map +1 -1
- package/dist/src/index.d.ts +13 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/node/db/Db.d.ts.map +1 -1
- package/dist/src/node/helpers/FileManager.d.ts +2 -0
- package/dist/src/node/helpers/FileManager.d.ts.map +1 -1
- package/dist/src/query/createLocalQueryDataSource.d.ts +4 -0
- package/dist/src/query/createLocalQueryDataSource.d.ts.map +1 -1
- package/dist/src/seedSchema/MetadataSchema.d.ts +36 -0
- package/dist/src/seedSchema/MetadataSchema.d.ts.map +1 -1
- package/dist/src/seedSchema/ModelSchema.d.ts +57 -0
- package/dist/src/seedSchema/ModelSchema.d.ts.map +1 -1
- package/dist/src/seedSchema/SeedSchema.d.ts +19 -0
- package/dist/src/seedSchema/SeedSchema.d.ts.map +1 -1
- package/dist/src/seedSchema/VersionSchema.d.ts +17 -0
- package/dist/src/seedSchema/VersionSchema.d.ts.map +1 -1
- package/dist/src/services/write/actors/validateEntity.d.ts.map +1 -1
- package/dist/src/services/write/actors/writeToDatabase.d.ts +5 -0
- package/dist/src/services/write/actors/writeToDatabase.d.ts.map +1 -1
- package/dist/src/services/write/writeProcessMachine.d.ts.map +1 -1
- package/dist/src/stores/eas.d.ts +1 -1
- package/dist/src/stores/eas.d.ts.map +1 -1
- package/dist/src/types/index.d.ts +7 -0
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/src/types/item.d.ts +19 -11
- package/dist/src/types/item.d.ts.map +1 -1
- package/dist/src/types/property.d.ts +3 -2
- package/dist/src/types/property.d.ts.map +1 -1
- package/dist/syncDbWithEas-BPap9Sru.js +855 -0
- package/dist/syncDbWithEas-BPap9Sru.js.map +1 -0
- package/dist/{index-280bL-n4.js → tabEvents-C6NXS6Jl.js} +231 -68
- package/dist/tabEvents-C6NXS6Jl.js.map +1 -0
- package/dist/tolerantWebAccess-NBQ1NB4i.js +160 -0
- package/dist/tolerantWebAccess-NBQ1NB4i.js.map +1 -0
- package/dist/updateSeedRevokedAt-CYwgVNlM.js +32 -0
- package/dist/updateSeedRevokedAt-CYwgVNlM.js.map +1 -0
- package/package.json +8 -10
- package/dist/BasePathResolver-B6CKqoKw.js.map +0 -1
- package/dist/ClientManager-ChcmUL47.js.map +0 -1
- package/dist/ModelProperty-D79uV42p.js.map +0 -1
- package/dist/Schema-a8uO-hIZ.js.map +0 -1
- package/dist/SchemaValidationService-Y49vqast.js.map +0 -1
- package/dist/backfillMetadataPropertyIds-CBsf7LnU.js.map +0 -1
- package/dist/cjs/ModelProperty-D9FGaGuE.js.map +0 -1
- package/dist/cjs/Schema-BiJIPLEL.js.map +0 -1
- package/dist/cjs/SchemaValidationService-DtYs_CNY.js.map +0 -1
- package/dist/cjs/backfillMetadataPropertyIds-2XaN3b9S.js.map +0 -1
- package/dist/cjs/getPublishPayload-D5DhLGvN.js.map +0 -1
- package/dist/cjs/getPublishUploads-DMhCCKYF.js.map +0 -1
- package/dist/cjs/getSegmentedItemProperties-CU8rtBAo.js +0 -148
- package/dist/cjs/getSegmentedItemProperties-CU8rtBAo.js.map +0 -1
- package/dist/cjs/index-CSNtEnbF.js.map +0 -1
- package/dist/cjs/index-DPp9rdRL.js.map +0 -1
- package/dist/cjs/property-DlKiAWt4.js.map +0 -1
- package/dist/cjs/syncDbWithEas-C6n34o_Q.js +0 -606
- package/dist/cjs/syncDbWithEas-C6n34o_Q.js.map +0 -1
- package/dist/db-WbrzEf7c.js.map +0 -1
- package/dist/eas-CfYCu4IX.js.map +0 -1
- package/dist/getPublishPayload-BIrTVUxb.js.map +0 -1
- package/dist/getPublishUploads-C6cDeZdz.js.map +0 -1
- package/dist/getSegmentedItemProperties-BmKE71vU.js +0 -148
- package/dist/getSegmentedItemProperties-BmKE71vU.js.map +0 -1
- package/dist/index-280bL-n4.js.map +0 -1
- package/dist/index-ClSv5pTA.js.map +0 -1
- package/dist/index-Djd3667v.js.map +0 -1
- package/dist/index-HRHkbVQd.js.map +0 -1
- package/dist/index.node-DBtQkEfz.js.map +0 -1
- package/dist/property-CQjuZ4JW.js.map +0 -1
- package/dist/src/Item/service/actors/fetchDataFromEas.d.ts +0 -4
- package/dist/src/Item/service/actors/fetchDataFromEas.d.ts.map +0 -1
- package/dist/src/Item/service/actors/hydrateExistingItem.d.ts +0 -4
- package/dist/src/Item/service/actors/hydrateExistingItem.d.ts.map +0 -1
- package/dist/src/Item/service/actors/hydrateNewItem.d.ts +0 -4
- package/dist/src/Item/service/actors/hydrateNewItem.d.ts.map +0 -1
- package/dist/src/Item/service/actors/initialize.d.ts +0 -4
- package/dist/src/Item/service/actors/initialize.d.ts.map +0 -1
- package/dist/src/Item/service/actors/reload.d.ts +0 -5
- package/dist/src/Item/service/actors/reload.d.ts.map +0 -1
- package/dist/src/Item/service/actors/saveDataToDb.d.ts +0 -4
- package/dist/src/Item/service/actors/saveDataToDb.d.ts.map +0 -1
- package/dist/src/ItemProperty/service/actors/resolveRemoteStorage.d.ts +0 -5
- package/dist/src/ItemProperty/service/actors/resolveRemoteStorage.d.ts.map +0 -1
- package/dist/src/Schema/service/addModelsMachine.d.ts +0 -123
- package/dist/src/Schema/service/addModelsMachine.d.ts.map +0 -1
- package/dist/src/browser/helpers/index.d.ts +0 -4
- package/dist/src/browser/helpers/index.d.ts.map +0 -1
- package/dist/src/db/configs/dev.schema.config.d.ts.map +0 -1
- package/dist/src/helpers/file/download/actors.d.ts +0 -17
- package/dist/src/helpers/file/download/actors.d.ts.map +0 -1
- package/dist/src/helpers/file/download/index.d.ts +0 -70
- package/dist/src/helpers/file/download/index.d.ts.map +0 -1
- package/dist/src/services/publish/actors/createPublishAttempt.d.ts +0 -4
- package/dist/src/services/publish/actors/createPublishAttempt.d.ts.map +0 -1
- package/dist/src/services/publish/actors/preparePublishRequestData.d.ts +0 -4
- package/dist/src/services/publish/actors/preparePublishRequestData.d.ts.map +0 -1
- package/dist/src/services/publish/actors/upload.d.ts +0 -4
- package/dist/src/services/publish/actors/upload.d.ts.map +0 -1
- package/dist/src/services/publish/actors/validateItemData.d.ts +0 -4
- package/dist/src/services/publish/actors/validateItemData.d.ts.map +0 -1
- package/dist/src/services/publish/publishMachine.d.ts +0 -32
- package/dist/src/services/publish/publishMachine.d.ts.map +0 -1
- package/dist/syncDbWithEas-CuARXfyN.js +0 -598
- package/dist/syncDbWithEas-CuARXfyN.js.map +0 -1
- package/dist/updateSeedRevokedAt-CBcLfHWs.js +0 -22
- package/dist/updateSeedRevokedAt-CBcLfHWs.js.map +0 -1
|
@@ -0,0 +1,4763 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
import { createActor, setup, assign, fromPromise, fromCallback, waitFor } from 'xstate';
|
|
3
|
+
import { m as eventEmitter, C as normalizeAddressConfig, E as normalizeAddressList, S as loadModelsFromDbForSchema, O as addSchemaToDb, n as normalizeDataType, U as linkModelToSchema, s as normalizePublisher, V as getWatchedAddressesFromDb, W as getOwnedAddressesFromDb } from './db-Lw0KVZmc.js';
|
|
4
|
+
import { u as MachineIds, W as ClientManagerEvents, C as ClientManagerState, X as resolvePublisherFromSeedRow, G as isLocalUnsealedDraft, I as Item } from './Item-ZUAmf943.js';
|
|
5
|
+
import { B as BaseDb, e as easSyncProcesses, t as BasePathResolver, b as appState, a as BaseFileManager, j as models$1, i as modelUids, r as seeds, s as schemas, K as isInternalSchema, L as INTERNAL_SCHEMA_IDS, F as SEED_PROTOCOL_SCHEMA_NAME, g as modelSchemas, m as metadata$1, v as versions, h as htmlEmbeddedImageCoPublish, c as arweaveL1FinalizeJobs, q as publishProcesses, M as CLIENT_NOT_INITIALIZED } from './BasePathResolver-DRT09oVv.js';
|
|
6
|
+
import { eq, inArray, and, isNotNull, ne, desc, countDistinct, or, isNull } from 'drizzle-orm';
|
|
7
|
+
import { o as withDbTabLock, j as isLeaderTab, v as postTabMessage, x as onTabMessage, w as whenLeaderTab, y as listenForOtherTabsEvents, z as isNode, B as isBrowser, C as startTabCoordination, D as downloadAllFilesRequestHandler, E as downloadAllFilesBinaryRequestHandler, t as toSnakeCase, u as withSeedDbLock, p as parseAddressesPersistedPayload, e as emitAcrossTabs, A as ADDRESSES_PERSISTED_EVENT, L as LOCAL_COPIES_REMOVED_EVENT } from './tabEvents-C6NXS6Jl.js';
|
|
8
|
+
import { seedGatewayConfigFromSeedConfig, setPreferredArweaveReadBaseUrls, resolveSeedGatewayEndpoints, setResolvedSeedGatewayEndpoints, BaseArweaveClient, ensureReadGatewaySelected, DEFAULT_ARWEAVE_HOST } from '@seedprotocol/arweave';
|
|
9
|
+
import { configureEasReadChain, BaseQueryClient, BaseEasClient, GET_SCHEMAS, whenEasReadChainSettled, getEasReadChainId, DEFAULT_EAS_CHAIN_ID, isEasReadChainConfigured, isValidEasAttestationUid } from '@seedprotocol/eas';
|
|
10
|
+
import 'pluralize';
|
|
11
|
+
import { sha3_256 } from '@noble/hashes/sha3';
|
|
12
|
+
import { bytesToHex, utf8ToBytes } from '@noble/hashes/utils';
|
|
13
|
+
import { generateId } from './generateId-BopUwD8F.js';
|
|
14
|
+
import { clearLocalQuerySource, registerLocalQuerySource, getSeed } from '@seedprotocol/query';
|
|
15
|
+
import { startCase } from 'lodash-es';
|
|
16
|
+
|
|
17
|
+
let writeTail = Promise.resolve();
|
|
18
|
+
function enqueueEasSyncDbWrite(fn) {
|
|
19
|
+
const next = writeTail.then(fn);
|
|
20
|
+
writeTail = next.catch(() => { });
|
|
21
|
+
return next;
|
|
22
|
+
}
|
|
23
|
+
const MAX_ERROR_MESSAGE_LENGTH = 500;
|
|
24
|
+
const MAX_ERROR_DETAILS_LENGTH = 2000;
|
|
25
|
+
async function insertEasSyncProcessRow(params) {
|
|
26
|
+
return enqueueEasSyncDbWrite(async () => {
|
|
27
|
+
const db = BaseDb.getAppDb();
|
|
28
|
+
if (!db) {
|
|
29
|
+
throw new Error('[easSyncProcess] App DB not available');
|
|
30
|
+
}
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
const [row] = await db
|
|
33
|
+
.insert(easSyncProcesses)
|
|
34
|
+
.values({
|
|
35
|
+
status: 'in_progress',
|
|
36
|
+
startedAt: now,
|
|
37
|
+
requestPayload: JSON.stringify(params.requestPayload),
|
|
38
|
+
persistedSnapshot: JSON.stringify(params.persistedSnapshot),
|
|
39
|
+
createdAt: now,
|
|
40
|
+
updatedAt: now,
|
|
41
|
+
})
|
|
42
|
+
.returning({ id: easSyncProcesses.id });
|
|
43
|
+
return row.id;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async function finalizeEasSyncProcessRow(id, params) {
|
|
47
|
+
return enqueueEasSyncDbWrite(async () => {
|
|
48
|
+
const db = BaseDb.getAppDb();
|
|
49
|
+
if (!db) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
await db
|
|
54
|
+
.update(easSyncProcesses)
|
|
55
|
+
.set({
|
|
56
|
+
status: params.status,
|
|
57
|
+
completedAt: now,
|
|
58
|
+
updatedAt: now,
|
|
59
|
+
errorMessage: params.errorMessage?.slice(0, MAX_ERROR_MESSAGE_LENGTH) ?? null,
|
|
60
|
+
errorDetails: params.errorDetails?.slice(0, MAX_ERROR_DETAILS_LENGTH) ?? null,
|
|
61
|
+
persistedSnapshot: JSON.stringify(params.persistedSnapshot),
|
|
62
|
+
})
|
|
63
|
+
.where(eq(easSyncProcesses.id, id));
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Merge multiple EAS sync intents: full sync (no concrete address list) wins;
|
|
69
|
+
* otherwise union addresses (first-seen casing).
|
|
70
|
+
*/
|
|
71
|
+
function mergeEasSyncRequestIntents(parts) {
|
|
72
|
+
const norm = parts.filter((p) => p != null);
|
|
73
|
+
if (norm.length === 0) {
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
let full = false;
|
|
77
|
+
const addressMap = new Map();
|
|
78
|
+
for (const p of norm) {
|
|
79
|
+
if (!('addresses' in p) || p.addresses === undefined) {
|
|
80
|
+
full = true;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
for (const addr of p.addresses) {
|
|
84
|
+
const k = addr.toLowerCase();
|
|
85
|
+
if (!addressMap.has(k)) {
|
|
86
|
+
addressMap.set(k, addr);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (full) {
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
if (addressMap.size === 0) {
|
|
94
|
+
return { addresses: [] };
|
|
95
|
+
}
|
|
96
|
+
return { addresses: [...addressMap.values()] };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const log = debug('seedSdk:events:easSyncManager');
|
|
100
|
+
function toRunSyncArg(options) {
|
|
101
|
+
if (options &&
|
|
102
|
+
'addresses' in options &&
|
|
103
|
+
options.addresses !== undefined) {
|
|
104
|
+
return options;
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
const runEasSync = fromPromise(async ({ input, }) => {
|
|
109
|
+
const rowId = await insertEasSyncProcessRow({
|
|
110
|
+
requestPayload: input.requestPayload,
|
|
111
|
+
persistedSnapshot: input.runningSnapshot,
|
|
112
|
+
});
|
|
113
|
+
const { runSyncFromEas } = await import('./syncDbWithEas-BPap9Sru.js');
|
|
114
|
+
try {
|
|
115
|
+
// Sync checks for rows and then inserts the rest (no unique index on uids), so syncs in
|
|
116
|
+
// different tabs take turns. A sync can run for minutes, so wait without a timeout.
|
|
117
|
+
await withDbTabLock('eas-sync', () => runSyncFromEas(toRunSyncArg(input.options)), {
|
|
118
|
+
timeoutMs: Infinity,
|
|
119
|
+
});
|
|
120
|
+
await finalizeEasSyncProcessRow(rowId, {
|
|
121
|
+
status: 'completed',
|
|
122
|
+
persistedSnapshot: {
|
|
123
|
+
phase: 'completed',
|
|
124
|
+
correlationIds: input.requestPayload.correlationIds,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
130
|
+
const details = err instanceof Error && err.stack
|
|
131
|
+
? err.stack.slice(0, 2000)
|
|
132
|
+
: undefined;
|
|
133
|
+
await finalizeEasSyncProcessRow(rowId, {
|
|
134
|
+
status: 'failed',
|
|
135
|
+
errorMessage: message,
|
|
136
|
+
errorDetails: details,
|
|
137
|
+
persistedSnapshot: {
|
|
138
|
+
phase: 'failed',
|
|
139
|
+
correlationIds: input.requestPayload.correlationIds,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
const initialContext = {
|
|
146
|
+
runOptions: {},
|
|
147
|
+
activeCorrelationIds: [],
|
|
148
|
+
activePrimarySource: 'event_bus',
|
|
149
|
+
pendingMergedOptions: undefined,
|
|
150
|
+
pendingCorrelationIds: [],
|
|
151
|
+
pendingPrimarySource: null,
|
|
152
|
+
lastFinishedCorrelationIds: null,
|
|
153
|
+
lastFinishedStatus: null,
|
|
154
|
+
lastFinishedError: null,
|
|
155
|
+
};
|
|
156
|
+
const easSyncMachine = setup({
|
|
157
|
+
types: {
|
|
158
|
+
context: {},
|
|
159
|
+
events: {},
|
|
160
|
+
},
|
|
161
|
+
actors: {
|
|
162
|
+
runEasSync,
|
|
163
|
+
},
|
|
164
|
+
}).createMachine({
|
|
165
|
+
id: MachineIds.EAS_SYNC_MANAGER,
|
|
166
|
+
initial: 'idle',
|
|
167
|
+
context: initialContext,
|
|
168
|
+
states: {
|
|
169
|
+
idle: {
|
|
170
|
+
on: {
|
|
171
|
+
REQUEST: {
|
|
172
|
+
target: 'running',
|
|
173
|
+
actions: assign({
|
|
174
|
+
runOptions: ({ event }) => event.options === undefined
|
|
175
|
+
? {}
|
|
176
|
+
: mergeEasSyncRequestIntents([event.options]),
|
|
177
|
+
activeCorrelationIds: ({ event }) => [event.correlationId],
|
|
178
|
+
activePrimarySource: ({ event }) => event.source,
|
|
179
|
+
}),
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
running: {
|
|
184
|
+
on: {
|
|
185
|
+
REQUEST: {
|
|
186
|
+
actions: assign({
|
|
187
|
+
pendingMergedOptions: ({ context, event }) => mergeEasSyncRequestIntents([
|
|
188
|
+
context.runOptions,
|
|
189
|
+
context.pendingMergedOptions,
|
|
190
|
+
event.options === undefined ? {} : event.options,
|
|
191
|
+
].filter((x) => x != null)),
|
|
192
|
+
pendingCorrelationIds: ({ context, event }) => [
|
|
193
|
+
...context.pendingCorrelationIds,
|
|
194
|
+
event.correlationId,
|
|
195
|
+
],
|
|
196
|
+
pendingPrimarySource: ({ context, event }) => context.pendingPrimarySource ?? event.source,
|
|
197
|
+
}),
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
invoke: {
|
|
201
|
+
src: 'runEasSync',
|
|
202
|
+
input: ({ context }) => ({
|
|
203
|
+
options: context.runOptions,
|
|
204
|
+
requestPayload: {
|
|
205
|
+
source: context.activePrimarySource,
|
|
206
|
+
correlationIds: context.activeCorrelationIds,
|
|
207
|
+
options: context.runOptions,
|
|
208
|
+
},
|
|
209
|
+
runningSnapshot: {
|
|
210
|
+
phase: 'running',
|
|
211
|
+
correlationIds: context.activeCorrelationIds,
|
|
212
|
+
},
|
|
213
|
+
}),
|
|
214
|
+
onDone: {
|
|
215
|
+
target: 'afterRun',
|
|
216
|
+
actions: assign({
|
|
217
|
+
lastFinishedCorrelationIds: ({ context }) => [
|
|
218
|
+
...context.activeCorrelationIds,
|
|
219
|
+
],
|
|
220
|
+
lastFinishedStatus: () => 'success',
|
|
221
|
+
lastFinishedError: () => null,
|
|
222
|
+
activeCorrelationIds: () => [],
|
|
223
|
+
runOptions: () => ({}),
|
|
224
|
+
}),
|
|
225
|
+
},
|
|
226
|
+
onError: {
|
|
227
|
+
target: 'afterRun',
|
|
228
|
+
actions: [
|
|
229
|
+
assign({
|
|
230
|
+
lastFinishedCorrelationIds: ({ context }) => [
|
|
231
|
+
...context.activeCorrelationIds,
|
|
232
|
+
],
|
|
233
|
+
lastFinishedStatus: () => 'failed',
|
|
234
|
+
lastFinishedError: ({ event }) => {
|
|
235
|
+
const e = event.error;
|
|
236
|
+
if (e instanceof Error) {
|
|
237
|
+
return e.message;
|
|
238
|
+
}
|
|
239
|
+
return e != null ? String(e) : 'EAS sync failed';
|
|
240
|
+
},
|
|
241
|
+
activeCorrelationIds: () => [],
|
|
242
|
+
runOptions: () => ({}),
|
|
243
|
+
}),
|
|
244
|
+
({ event }) => {
|
|
245
|
+
log('runEasSync error', event.error);
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
afterRun: {
|
|
252
|
+
always: [
|
|
253
|
+
{
|
|
254
|
+
guard: ({ context }) => context.pendingCorrelationIds.length > 0,
|
|
255
|
+
target: 'running',
|
|
256
|
+
actions: assign({
|
|
257
|
+
runOptions: ({ context }) => mergeEasSyncRequestIntents([context.pendingMergedOptions]),
|
|
258
|
+
activeCorrelationIds: ({ context }) => [
|
|
259
|
+
...context.pendingCorrelationIds,
|
|
260
|
+
],
|
|
261
|
+
activePrimarySource: ({ context }) => context.pendingPrimarySource ?? 'event_bus',
|
|
262
|
+
pendingMergedOptions: () => undefined,
|
|
263
|
+
pendingCorrelationIds: () => [],
|
|
264
|
+
pendingPrimarySource: () => null,
|
|
265
|
+
}),
|
|
266
|
+
},
|
|
267
|
+
{ target: 'idle' },
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
const easSyncActor = createActor(easSyncMachine);
|
|
273
|
+
let easSyncActorStarted = false;
|
|
274
|
+
function startEasSyncActor() {
|
|
275
|
+
if (easSyncActorStarted) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
easSyncActorStarted = true;
|
|
279
|
+
easSyncActor.start();
|
|
280
|
+
}
|
|
281
|
+
function newCorrelationId() {
|
|
282
|
+
const c = globalThis.crypto;
|
|
283
|
+
if (c?.randomUUID) {
|
|
284
|
+
return c.randomUUID();
|
|
285
|
+
}
|
|
286
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
287
|
+
}
|
|
288
|
+
/** Requests deferred in this tab until it leads, by source; each runs once on takeover. */
|
|
289
|
+
const deferredUntilLeader = new Set();
|
|
290
|
+
/**
|
|
291
|
+
* Automatic syncs run in the leader tab only (docs/MULTI_TAB.md). A tab that skips one runs it if
|
|
292
|
+
* it later becomes leader, so closing the leader doesn't lose the catch-up sync.
|
|
293
|
+
*/
|
|
294
|
+
function runInLeaderTab(source, request) {
|
|
295
|
+
if (isLeaderTab()) {
|
|
296
|
+
request();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
log(`${source} sync skipped: another tab leads`);
|
|
300
|
+
if (deferredUntilLeader.has(source))
|
|
301
|
+
return;
|
|
302
|
+
deferredUntilLeader.add(source);
|
|
303
|
+
void whenLeaderTab().then(() => {
|
|
304
|
+
deferredUntilLeader.delete(source);
|
|
305
|
+
request();
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
let listeningToOtherTabs = false;
|
|
309
|
+
/** The leader runs address-change syncs requested by other tabs. */
|
|
310
|
+
function listenForEasSyncRequestsFromOtherTabs() {
|
|
311
|
+
if (listeningToOtherTabs)
|
|
312
|
+
return;
|
|
313
|
+
listeningToOtherTabs = true;
|
|
314
|
+
onTabMessage((message) => {
|
|
315
|
+
if (message.type === 'eas-sync-address-change' && isLeaderTab()) {
|
|
316
|
+
requestEasSyncFromAddressChange(message.addresses);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
function requestEasSyncFromEventBus() {
|
|
321
|
+
runInLeaderTab('event_bus', sendEventBusRequest);
|
|
322
|
+
}
|
|
323
|
+
function sendEventBusRequest() {
|
|
324
|
+
startEasSyncActor();
|
|
325
|
+
easSyncActor.send({
|
|
326
|
+
type: 'REQUEST',
|
|
327
|
+
correlationId: newCorrelationId(),
|
|
328
|
+
source: 'event_bus',
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
function requestEasSyncFromAddressChange(addresses) {
|
|
332
|
+
if (!isLeaderTab()) {
|
|
333
|
+
// The leader syncs these addresses; its own address list may be stale.
|
|
334
|
+
postTabMessage({ type: 'eas-sync-address-change', addresses });
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
startEasSyncActor();
|
|
338
|
+
easSyncActor.send({
|
|
339
|
+
type: 'REQUEST',
|
|
340
|
+
correlationId: newCorrelationId(),
|
|
341
|
+
options: { addresses },
|
|
342
|
+
source: 'address_change',
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
function requestEasSyncFromModelsInit() {
|
|
346
|
+
runInLeaderTab('models_init', sendModelsInitRequest);
|
|
347
|
+
}
|
|
348
|
+
function sendModelsInitRequest() {
|
|
349
|
+
startEasSyncActor();
|
|
350
|
+
easSyncActor.send({
|
|
351
|
+
type: 'REQUEST',
|
|
352
|
+
correlationId: newCorrelationId(),
|
|
353
|
+
source: 'models_init',
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
function sendEasSyncClientRequest(correlationId, options) {
|
|
357
|
+
startEasSyncActor();
|
|
358
|
+
easSyncActor.send({
|
|
359
|
+
type: 'REQUEST',
|
|
360
|
+
correlationId,
|
|
361
|
+
options,
|
|
362
|
+
source: 'client_api',
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
var easSyncManager = /*#__PURE__*/Object.freeze({
|
|
367
|
+
__proto__: null,
|
|
368
|
+
easSyncActor: easSyncActor,
|
|
369
|
+
easSyncMachine: easSyncMachine,
|
|
370
|
+
listenForEasSyncRequestsFromOtherTabs: listenForEasSyncRequestsFromOtherTabs,
|
|
371
|
+
requestEasSyncFromAddressChange: requestEasSyncFromAddressChange,
|
|
372
|
+
requestEasSyncFromEventBus: requestEasSyncFromEventBus,
|
|
373
|
+
requestEasSyncFromModelsInit: requestEasSyncFromModelsInit,
|
|
374
|
+
sendEasSyncClientRequest: sendEasSyncClientRequest,
|
|
375
|
+
startEasSyncActor: startEasSyncActor
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const logger$b = debug('seedSdk:services:events');
|
|
379
|
+
const handleServiceSaveState = (event) => {
|
|
380
|
+
const { state, serviceId } = event;
|
|
381
|
+
logger$b(`[browser] [service.saveState.request] serviceId: ${serviceId}`);
|
|
382
|
+
// TODO: This is called from Node as well. Do we need it? If so, need to generalize this to all platforms.
|
|
383
|
+
// localStorage.setItem(`seed_sdk_service_${serviceId}`, JSON.stringify(state))
|
|
384
|
+
};
|
|
385
|
+
const setupServicesEventHandlers = () => {
|
|
386
|
+
eventEmitter.addListener('service.saveState.request', handleServiceSaveState);
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const setupAllItemsEventHandlers = () => {
|
|
390
|
+
startEasSyncActor();
|
|
391
|
+
listenForEasSyncRequestsFromOtherTabs();
|
|
392
|
+
listenForOtherTabsEvents();
|
|
393
|
+
eventEmitter.addListener('syncDbWithEas', requestEasSyncFromEventBus);
|
|
394
|
+
};
|
|
395
|
+
// Note: getAreItemEventHandlersReady removed - was only used by useItemIsReady hook which has been removed
|
|
396
|
+
|
|
397
|
+
// AllItems services have been removed - this handler is no longer needed
|
|
398
|
+
// Service state persistence is now handled by entity instances directly
|
|
399
|
+
const saveServiceHandler = async (event) => {
|
|
400
|
+
// AllItems services removed - this functionality is no longer needed
|
|
401
|
+
// Entity instances (Schema, Model, Item) manage their own state
|
|
402
|
+
console.warn('[saveServiceHandler] AllItems services removed - handler is a no-op');
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const setupServiceHandlers = () => {
|
|
406
|
+
eventEmitter.addListener('service.save', saveServiceHandler);
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* On Node, storage uses `filesDir`. `endpoints.filePaths` and `endpoints.files` are
|
|
411
|
+
* required by init but have no separate Node consumer, so fill them from `filesDir`.
|
|
412
|
+
* Browser configs are left unchanged.
|
|
413
|
+
*/
|
|
414
|
+
function normalizeSeedConfigEndpoints(config) {
|
|
415
|
+
if (!config)
|
|
416
|
+
return config;
|
|
417
|
+
const endpoints = config.endpoints;
|
|
418
|
+
if (endpoints?.filePaths && endpoints?.files)
|
|
419
|
+
return config;
|
|
420
|
+
if (!(isNode() && !isBrowser()))
|
|
421
|
+
return config;
|
|
422
|
+
const filesDir = config.filesDir;
|
|
423
|
+
if (!filesDir)
|
|
424
|
+
return config;
|
|
425
|
+
const next = {
|
|
426
|
+
filePaths: endpoints?.filePaths || filesDir,
|
|
427
|
+
files: endpoints?.files || filesDir,
|
|
428
|
+
};
|
|
429
|
+
return { ...config, endpoints: next };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const logger$a = debug('seedSdk:ClientManager:initialize');
|
|
433
|
+
const platformClassesInit = fromCallback(({ sendBack, input: { context, event } }) => {
|
|
434
|
+
logger$a('initialize from ClientManager');
|
|
435
|
+
const { isInitialized } = context;
|
|
436
|
+
if (!event || !('options' in event)) {
|
|
437
|
+
throw new Error('Initialize event must include options');
|
|
438
|
+
}
|
|
439
|
+
const { options } = event;
|
|
440
|
+
if (isInitialized) {
|
|
441
|
+
sendBack({ type: 'initialized' });
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
// Validate synchronously before starting async operations
|
|
445
|
+
// This ensures errors are handled immediately, preventing unhandled rejections
|
|
446
|
+
const { config } = options;
|
|
447
|
+
const { endpoints } = config || {};
|
|
448
|
+
// Track if the operation is cancelled
|
|
449
|
+
let cancelled = false;
|
|
450
|
+
// Create a promise that will handle validation errors immediately
|
|
451
|
+
// We use Promise.resolve().then() to ensure the catch handler is attached synchronously
|
|
452
|
+
// before the async function starts executing, preventing unhandled rejections
|
|
453
|
+
const initPromise = Promise.resolve().then(async () => {
|
|
454
|
+
// Check if cancelled before starting
|
|
455
|
+
if (cancelled) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const { addresses } = options;
|
|
459
|
+
const config = normalizeSeedConfigEndpoints(options.config);
|
|
460
|
+
// Validate required endpoints - this should happen early in the initialization process.
|
|
461
|
+
// Node fills these from filesDir; browser callers still pass them.
|
|
462
|
+
if (!config?.endpoints || !config.endpoints.filePaths || !config.endpoints.files) {
|
|
463
|
+
throw new Error('Config must include endpoints with filePaths and files');
|
|
464
|
+
}
|
|
465
|
+
// Prefer the condition-resolved factory when available; fall back to explicit
|
|
466
|
+
// platform imports so source/vitest resolution stays reliable.
|
|
467
|
+
const { configurePlatform } = await import('./configurePlatform-CZpjyuCo.js');
|
|
468
|
+
if (isBrowser()) {
|
|
469
|
+
const { createPlatformServices } = await import('./platform.browser.js');
|
|
470
|
+
configurePlatform(createPlatformServices());
|
|
471
|
+
}
|
|
472
|
+
else if (isNode()) {
|
|
473
|
+
const { createPlatformServices } = await import('./platform.node.js');
|
|
474
|
+
configurePlatform(createPlatformServices());
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
throw new Error(`Unable to determine environment. isBrowser()=${isBrowser()}, isNode()=${isNode()}. Platform-specific implementations could not be loaded.`);
|
|
478
|
+
}
|
|
479
|
+
// Check if cancelled after async imports
|
|
480
|
+
if (cancelled) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const { models, endpoints, arweaveDomain, dbConfig, filesDir, schemaFile, schema, gateway, uploadApiBaseUrl, eas } = config;
|
|
484
|
+
if (eas) {
|
|
485
|
+
configureEasReadChain('sdk', { chainId: eas.chainId, indexerUrl: eas.indexerUrl });
|
|
486
|
+
}
|
|
487
|
+
const gatewayConfig = seedGatewayConfigFromSeedConfig({
|
|
488
|
+
...config,
|
|
489
|
+
arweaveDomain,
|
|
490
|
+
uploadApiBaseUrl,
|
|
491
|
+
gateway,
|
|
492
|
+
});
|
|
493
|
+
// Only when configured, so a host app's own setPreferredArweaveReadBaseUrls() call survives init.
|
|
494
|
+
if (gatewayConfig.preferredReadBaseUrls) {
|
|
495
|
+
setPreferredArweaveReadBaseUrls(gatewayConfig.preferredReadBaseUrls);
|
|
496
|
+
}
|
|
497
|
+
const resolvedGateway = await resolveSeedGatewayEndpoints(gatewayConfig);
|
|
498
|
+
setResolvedSeedGatewayEndpoints(resolvedGateway);
|
|
499
|
+
BaseArweaveClient.setHost(`${resolvedGateway.arweaveProtocol}://${resolvedGateway.arweaveHost}`);
|
|
500
|
+
// Note: Validation already happened above, but we have endpoints here for path normalization
|
|
501
|
+
// Normalize filesDir for Node.js environment
|
|
502
|
+
// In Node.js, filesDir should be resolved relative to .seed directory at project root
|
|
503
|
+
let normalizedFilesDir = filesDir || endpoints?.files;
|
|
504
|
+
// jsdom (and any env with both Node + window) is "browser" for SDK: PathResolver is OPFS/virtual.
|
|
505
|
+
// Do not run real fs against those paths (e.g. mkdir '/.seed').
|
|
506
|
+
if (isNode() && !isBrowser() && normalizedFilesDir) {
|
|
507
|
+
const path = (await import('node:path')).default;
|
|
508
|
+
const fs = (await import('node:fs')).default;
|
|
509
|
+
const dotSeedDir = BasePathResolver.getDotSeedDir();
|
|
510
|
+
// Ensure .seed directory exists
|
|
511
|
+
if (!fs.existsSync(dotSeedDir)) {
|
|
512
|
+
fs.mkdirSync(dotSeedDir, { recursive: true });
|
|
513
|
+
}
|
|
514
|
+
// Check if filesDir is a browser OPFS-style path (absolute path starting with '/'
|
|
515
|
+
// that is a simple single-level path like '/app-files', not a real filesystem path)
|
|
516
|
+
// Browser OPFS paths are typically simple like '/app-files', '/files', etc.
|
|
517
|
+
const pathWithoutLeadingSlash = normalizedFilesDir.slice(1);
|
|
518
|
+
const isBrowserOpfsPath = path.isAbsolute(normalizedFilesDir) &&
|
|
519
|
+
normalizedFilesDir.startsWith('/') &&
|
|
520
|
+
pathWithoutLeadingSlash.length > 0 &&
|
|
521
|
+
!pathWithoutLeadingSlash.includes('/') && // Single directory name, no subdirectories
|
|
522
|
+
!fs.existsSync(normalizedFilesDir); // Doesn't exist on filesystem
|
|
523
|
+
if (isBrowserOpfsPath) {
|
|
524
|
+
// Convert browser OPFS path to .seed subdirectory
|
|
525
|
+
// e.g., '/app-files' -> '.seed/app-files'
|
|
526
|
+
const dirName = normalizedFilesDir.slice(1) || 'app-files';
|
|
527
|
+
normalizedFilesDir = path.join(dotSeedDir, dirName);
|
|
528
|
+
}
|
|
529
|
+
else if (!path.isAbsolute(normalizedFilesDir)) {
|
|
530
|
+
// If it's a relative path, check if it already starts with .seed
|
|
531
|
+
if (normalizedFilesDir.startsWith('.seed') || normalizedFilesDir.startsWith('.seed/')) {
|
|
532
|
+
// Already relative to project root, resolve it
|
|
533
|
+
normalizedFilesDir = path.resolve(normalizedFilesDir);
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
// Resolve relative to .seed directory
|
|
537
|
+
normalizedFilesDir = path.join(dotSeedDir, normalizedFilesDir);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
// If it's an absolute path that exists or is a valid filesystem path, use it as-is
|
|
541
|
+
}
|
|
542
|
+
const normalizedAddresses = normalizeAddressConfig(addresses);
|
|
543
|
+
sendBack({ type: 'updateContext', context: {
|
|
544
|
+
models: models || {},
|
|
545
|
+
endpoints,
|
|
546
|
+
arweaveDomain: resolvedGateway.arweaveHost,
|
|
547
|
+
uploadApiBaseUrl: resolvedGateway.uploadApiBaseUrl,
|
|
548
|
+
gatewayTransport: resolvedGateway.mode,
|
|
549
|
+
gatewayHyperKey: resolvedGateway.gatewayHyperKey,
|
|
550
|
+
gatewaySidecarHost: gatewayConfig.hyper?.localSidecarHost,
|
|
551
|
+
gatewaySidecarPort: gatewayConfig.hyper?.localSidecarPort,
|
|
552
|
+
gatewayProxyBaseUrl: resolvedGateway.proxyBaseUrl ?? gatewayConfig.proxyBaseUrl,
|
|
553
|
+
addresses: normalizedAddresses.owned,
|
|
554
|
+
ownedAddresses: normalizedAddresses.owned,
|
|
555
|
+
watchedAddresses: normalizedAddresses.watched,
|
|
556
|
+
filesDir: normalizedFilesDir,
|
|
557
|
+
dbConfig,
|
|
558
|
+
schemaFile,
|
|
559
|
+
schema,
|
|
560
|
+
syncFromEasOnAddressChange: options.syncFromEasOnAddressChange ?? true,
|
|
561
|
+
} });
|
|
562
|
+
// Elect a leader tab for background work before anything requests it (docs/MULTI_TAB.md).
|
|
563
|
+
if (normalizedFilesDir) {
|
|
564
|
+
startTabCoordination({ filesDir: normalizedFilesDir, mode: options.multiTab });
|
|
565
|
+
}
|
|
566
|
+
if (resolvedGateway.activePath !== 'hyper-sidecar' && resolvedGateway.activePath !== 'http-proxy') {
|
|
567
|
+
void ensureReadGatewaySelected().catch(() => {
|
|
568
|
+
/* non-blocking warm-up for read gateway probe */
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
// Models are now Model instances, no registration needed
|
|
572
|
+
// They should be created via Model.create() and are accessible via Model static methods
|
|
573
|
+
setupAllItemsEventHandlers();
|
|
574
|
+
setupServicesEventHandlers();
|
|
575
|
+
setupServiceHandlers();
|
|
576
|
+
});
|
|
577
|
+
// Handle the promise immediately - this ensures rejections are caught synchronously
|
|
578
|
+
// The catch handler is attached synchronously before the async function executes
|
|
579
|
+
initPromise
|
|
580
|
+
.then(() => {
|
|
581
|
+
if (!cancelled) {
|
|
582
|
+
sendBack({ type: 'platformClassesReady' });
|
|
583
|
+
}
|
|
584
|
+
})
|
|
585
|
+
.catch((error) => {
|
|
586
|
+
// Only send error if not cancelled (prevents unhandled rejections after cleanup)
|
|
587
|
+
if (!cancelled) {
|
|
588
|
+
// Send error event using sendBack - this is the recommended XState pattern
|
|
589
|
+
// The parent state machine should handle the 'error' event
|
|
590
|
+
sendBack({
|
|
591
|
+
type: 'error',
|
|
592
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
// Explicitly handle the rejection to prevent unhandled promise rejection warnings
|
|
596
|
+
// The error has been sent via sendBack, so we've handled it
|
|
597
|
+
});
|
|
598
|
+
// Return cleanup function to cancel pending operations
|
|
599
|
+
return () => {
|
|
600
|
+
cancelled = true;
|
|
601
|
+
// The promise will still resolve/reject, but we won't send events if cancelled
|
|
602
|
+
// This prevents unhandled rejections after the callback is cleaned up
|
|
603
|
+
};
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
const logger$9 = debug('seedSdk:client:writeToDb');
|
|
607
|
+
const saveAppState = fromCallback(({ sendBack, input: { key, value } }) => {
|
|
608
|
+
const _saveAppState = async () => {
|
|
609
|
+
try {
|
|
610
|
+
const appDb = BaseDb.getAppDb();
|
|
611
|
+
if (!appDb) {
|
|
612
|
+
// In test environments, continue anyway
|
|
613
|
+
if (process.env.NODE_ENV === 'test' || process.env.IS_SEED_DEV) {
|
|
614
|
+
logger$9('[client/actors] [saveAppState] App DB not found, but continuing in test environment');
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
throw new Error('App DB not found');
|
|
618
|
+
}
|
|
619
|
+
const result = await appDb.insert(appState)
|
|
620
|
+
.values({
|
|
621
|
+
key: key,
|
|
622
|
+
value: JSON.stringify(value),
|
|
623
|
+
})
|
|
624
|
+
.onConflictDoUpdate({
|
|
625
|
+
target: appState.key,
|
|
626
|
+
set: {
|
|
627
|
+
value: JSON.stringify(value),
|
|
628
|
+
},
|
|
629
|
+
});
|
|
630
|
+
logger$9('result', result);
|
|
631
|
+
}
|
|
632
|
+
catch (error) {
|
|
633
|
+
logger$9('[client/actors] [saveAppState] Error saving app state:', error);
|
|
634
|
+
// In test environments, continue anyway
|
|
635
|
+
if (process.env.NODE_ENV === 'test' || process.env.IS_SEED_DEV) {
|
|
636
|
+
logger$9('[client/actors] [saveAppState] Continuing despite error in test environment');
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
throw error;
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
_saveAppState()
|
|
643
|
+
.then(() => {
|
|
644
|
+
sendBack({
|
|
645
|
+
type: 'saveAppStateSuccess',
|
|
646
|
+
key,
|
|
647
|
+
value,
|
|
648
|
+
});
|
|
649
|
+
})
|
|
650
|
+
.catch((error) => {
|
|
651
|
+
logger$9('[client/actors] [saveAppState] Error in promise chain:', error);
|
|
652
|
+
// In test environments, still send success to allow state machine to progress
|
|
653
|
+
if (process.env.NODE_ENV === 'test' || process.env.IS_SEED_DEV) {
|
|
654
|
+
logger$9('[client/actors] [saveAppState] Sending success despite error in test environment');
|
|
655
|
+
sendBack({
|
|
656
|
+
type: 'saveAppStateSuccess',
|
|
657
|
+
key,
|
|
658
|
+
value,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
sendBack({
|
|
663
|
+
type: 'saveAppStateError',
|
|
664
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
665
|
+
key,
|
|
666
|
+
value,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
let isInitialized = false;
|
|
673
|
+
const fsInitHandler = async () => {
|
|
674
|
+
if (isInitialized) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
isInitialized = true;
|
|
678
|
+
};
|
|
679
|
+
let areReady = false;
|
|
680
|
+
const setupFsListeners = () => {
|
|
681
|
+
eventEmitter.addListener('fs.downloadAll.request', downloadAllFilesRequestHandler);
|
|
682
|
+
eventEmitter.addListener('fs.downloadAllBinary.request', downloadAllFilesBinaryRequestHandler);
|
|
683
|
+
eventEmitter.addListener('fs.init', fsInitHandler);
|
|
684
|
+
areReady = true;
|
|
685
|
+
};
|
|
686
|
+
const areFsListenersReady = () => {
|
|
687
|
+
return areReady;
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
debug('seedSdk:client:actors:fileSystemInit');
|
|
691
|
+
const fileSystemInit = fromCallback(({ sendBack, input: { context }, }) => {
|
|
692
|
+
setupFsListeners();
|
|
693
|
+
if (!areFsListenersReady()) {
|
|
694
|
+
throw new Error('fs listeners not ready during init');
|
|
695
|
+
}
|
|
696
|
+
// Check for filesDir synchronously before starting async operation
|
|
697
|
+
// This ensures the error is thrown synchronously and caught by XState
|
|
698
|
+
const { filesDir } = context;
|
|
699
|
+
if (!filesDir) {
|
|
700
|
+
throw new Error('filesDir is required');
|
|
701
|
+
}
|
|
702
|
+
const _fileSystemInit = async () => {
|
|
703
|
+
await BaseFileManager.initializeFileSystem(filesDir);
|
|
704
|
+
eventEmitter.emit('fs.init');
|
|
705
|
+
};
|
|
706
|
+
_fileSystemInit()
|
|
707
|
+
.then(() => {
|
|
708
|
+
sendBack({ type: 'fileSystemReady' });
|
|
709
|
+
})
|
|
710
|
+
.catch((error) => {
|
|
711
|
+
// Send error event using sendBack - this is the recommended XState pattern
|
|
712
|
+
// The parent state machine should handle the 'ERROR' event
|
|
713
|
+
sendBack({
|
|
714
|
+
type: 'error',
|
|
715
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
// Return cleanup function
|
|
719
|
+
return () => {
|
|
720
|
+
// Cleanup if needed
|
|
721
|
+
};
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
const logger$8 = debug('seedSdk:client:actors:dbInit');
|
|
725
|
+
const dbInit = fromCallback(({ sendBack, input: { context } }) => {
|
|
726
|
+
const _dbInit = async () => {
|
|
727
|
+
const { filesDir } = context;
|
|
728
|
+
if (!filesDir) {
|
|
729
|
+
throw new Error('filesDir is required');
|
|
730
|
+
}
|
|
731
|
+
// Prepare databases - this handles all initialization, migration, and file setup
|
|
732
|
+
await BaseDb.prepareDb(filesDir);
|
|
733
|
+
// Verify database is ready
|
|
734
|
+
const appDb = BaseDb.getAppDb();
|
|
735
|
+
if (!appDb) {
|
|
736
|
+
throw new Error('Database not available after preparation');
|
|
737
|
+
}
|
|
738
|
+
logger$8('[client/actors] [dbInit] Database prepared and ready');
|
|
739
|
+
};
|
|
740
|
+
_dbInit()
|
|
741
|
+
.then(() => {
|
|
742
|
+
sendBack({ type: ClientManagerEvents.DB_READY });
|
|
743
|
+
})
|
|
744
|
+
.catch((error) => {
|
|
745
|
+
logger$8('Error in dbInit:', error);
|
|
746
|
+
sendBack({
|
|
747
|
+
type: 'error',
|
|
748
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
const logger$7 = debug('seedSdk:client:actors:addModelsToStore');
|
|
754
|
+
const addModelsToStore = fromCallback(({ sendBack, input: { context } }) => {
|
|
755
|
+
const _addModelsToStore = async () => {
|
|
756
|
+
let { models } = context;
|
|
757
|
+
if (!models) {
|
|
758
|
+
models = {};
|
|
759
|
+
}
|
|
760
|
+
// Internal models (Seed, Version, Metadata, Image) are now loaded via SEEDPROTOCOL_Seed_Protocol_v1.json schema
|
|
761
|
+
// They should already be in context.models from processSchemaFiles
|
|
762
|
+
const allModels = { ...models };
|
|
763
|
+
sendBack({ type: ClientManagerEvents.UPDATE_CONTEXT, context: { models: allModels } });
|
|
764
|
+
// Models are now Model instances, no registration needed
|
|
765
|
+
// They should be created via Model.create() and are accessible via Model static methods
|
|
766
|
+
for (const [key] of Object.entries(allModels)) {
|
|
767
|
+
logger$7('model available:', key);
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
_addModelsToStore()
|
|
771
|
+
.then(() => {
|
|
772
|
+
sendBack({ type: ClientManagerEvents.ADD_MODELS_TO_STORE_SUCCESS });
|
|
773
|
+
})
|
|
774
|
+
.catch((error) => {
|
|
775
|
+
logger$7('[client/actors] [addModelsToStore] Error:', error);
|
|
776
|
+
sendBack({
|
|
777
|
+
type: 'error',
|
|
778
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
const logger$6 = debug('seedSdk:client:actors:addModelsToDb');
|
|
784
|
+
/**
|
|
785
|
+
* Returns every `models` row named in `modelNames`, inserting a null-schemaFileId stub for each name
|
|
786
|
+
* with no row yet (the schema import adopts stubs via findOrCreateModelRecord). Check-then-insert:
|
|
787
|
+
* another tab initializing at the same time would insert the same stubs, so tabs take turns
|
|
788
|
+
* (docs/MULTI_TAB.md).
|
|
789
|
+
*/
|
|
790
|
+
async function ensureModelStubs(appDb, modelNames, filesDir) {
|
|
791
|
+
return withSeedDbLock('init', filesDir, async () => {
|
|
792
|
+
// One query for all names. Model names are only unique per schema, so callers match each
|
|
793
|
+
// Model by its id (schemaFileId) first.
|
|
794
|
+
const rows = (await appDb
|
|
795
|
+
.select({ id: models$1.id, name: models$1.name, schemaFileId: models$1.schemaFileId })
|
|
796
|
+
.from(models$1)
|
|
797
|
+
.where(inArray(models$1.name, modelNames)));
|
|
798
|
+
const modelsToInsert = modelNames.filter((name) => !rows.some((m) => m.name === name));
|
|
799
|
+
if (modelsToInsert.length > 0) {
|
|
800
|
+
const newlyInserted = (await appDb
|
|
801
|
+
.insert(models$1)
|
|
802
|
+
.values(modelsToInsert.map((name) => ({ name })))
|
|
803
|
+
.returning({ id: models$1.id, name: models$1.name, schemaFileId: models$1.schemaFileId }));
|
|
804
|
+
rows.push(...newlyInserted);
|
|
805
|
+
for (const name of modelsToInsert) {
|
|
806
|
+
logger$6('[client/actors] [addModelsToDb] inserted model:', name);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return rows;
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
const addModelsToDb = fromCallback(({ sendBack, input: { context } }) => {
|
|
813
|
+
const { models } = context;
|
|
814
|
+
const _addModelsToDb = async () => {
|
|
815
|
+
const appDb = BaseDb.getAppDb();
|
|
816
|
+
if (!appDb) {
|
|
817
|
+
throw new Error('Database not ready');
|
|
818
|
+
}
|
|
819
|
+
if (!models) {
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
// Internal models (Seed, Version, Metadata) are now loaded via SEEDPROTOCOL_Seed_Protocol_v1.json schema
|
|
823
|
+
// They should already be in context.models from processSchemaFiles
|
|
824
|
+
const allModels = { ...models };
|
|
825
|
+
const modelNames = Object.keys(allModels);
|
|
826
|
+
if (modelNames.length === 0) {
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
const schemaDefsByModelName = new Map();
|
|
830
|
+
// Model names are only unique per schema, so match each Model by its id (schemaFileId) first.
|
|
831
|
+
const existingModels = await ensureModelStubs(appDb, modelNames, context.filesDir);
|
|
832
|
+
const rowFor = (modelName) => {
|
|
833
|
+
const modelFileId = allModels[modelName]?.id;
|
|
834
|
+
const sameName = existingModels.filter((m) => m.name === modelName);
|
|
835
|
+
return ((modelFileId ? sameName.find((m) => m.schemaFileId === modelFileId) : undefined) ??
|
|
836
|
+
(sameName.length === 1 ? sameName[0] : undefined) ??
|
|
837
|
+
sameName.find((m) => !m.schemaFileId));
|
|
838
|
+
};
|
|
839
|
+
let hasModelsInDb = true;
|
|
840
|
+
for (const modelName of modelNames) {
|
|
841
|
+
const foundModel = rowFor(modelName);
|
|
842
|
+
if (!foundModel) {
|
|
843
|
+
logger$6('[client/actors] [addModelsToDb] Warning: Could not find or create model:', modelName);
|
|
844
|
+
hasModelsInDb = false;
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
// The EAS schema string depends only on the model name, so every same-name row shares its uid.
|
|
848
|
+
schemaDefsByModelName.set(modelName, {
|
|
849
|
+
dbIds: existingModels.filter((m) => m.name === modelName).map((m) => m.id),
|
|
850
|
+
schemaDef: `bytes32 ${toSnakeCase(modelName)}`,
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
// If we have no models to process, still send success (empty models is valid)
|
|
854
|
+
if (schemaDefsByModelName.size === 0) {
|
|
855
|
+
logger$6('[client/actors] [addModelsToDb] No models to process, but continuing');
|
|
856
|
+
sendBack({ type: ClientManagerEvents.ADD_MODELS_TO_DB_SUCCESS });
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const schemaDefs = Array.from(schemaDefsByModelName.values()).map(({ schemaDef }) => schemaDef);
|
|
860
|
+
// Fetch schemas from EAS in background - do not block init. EAS can be slow (10s+);
|
|
861
|
+
// modelUids will be populated async and EAS sync orchestrator runs when done.
|
|
862
|
+
const fetchEasAndPopulateModelUids = async () => {
|
|
863
|
+
try {
|
|
864
|
+
const queryClient = BaseQueryClient.getQueryClient();
|
|
865
|
+
const easClient = BaseEasClient.getEasClient();
|
|
866
|
+
const queryPromise = queryClient.fetchQuery({
|
|
867
|
+
queryKey: [`getSchemasVersion`, [...schemaDefs].sort()],
|
|
868
|
+
queryFn: async () => easClient.request(GET_SCHEMAS, {
|
|
869
|
+
where: {
|
|
870
|
+
schema: {
|
|
871
|
+
in: schemaDefs,
|
|
872
|
+
},
|
|
873
|
+
},
|
|
874
|
+
}),
|
|
875
|
+
});
|
|
876
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('EAS query timeout after 10 seconds')), 10000));
|
|
877
|
+
const { schemas } = await Promise.race([queryPromise, timeoutPromise]);
|
|
878
|
+
if (schemas && schemas.length > 0) {
|
|
879
|
+
const db = BaseDb.getAppDb();
|
|
880
|
+
if (db) {
|
|
881
|
+
for (const schema of schemas) {
|
|
882
|
+
const modelIds = Array.from(schemaDefsByModelName.values()).find(({ schemaDef }) => schemaDef === schema.schema)?.dbIds ?? [];
|
|
883
|
+
for (const modelId of modelIds) {
|
|
884
|
+
await db
|
|
885
|
+
.insert(modelUids)
|
|
886
|
+
.values({
|
|
887
|
+
modelId,
|
|
888
|
+
uid: schema.id,
|
|
889
|
+
})
|
|
890
|
+
.onConflictDoNothing();
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
const { requestEasSyncFromModelsInit } = await Promise.resolve().then(function () { return easSyncManager; });
|
|
895
|
+
requestEasSyncFromModelsInit();
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
logger$6('[client/actors] [addModelsToDb] No schemas found from EAS, but continuing');
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
catch (error) {
|
|
902
|
+
if (process.env.NODE_ENV === 'test' || process.env.IS_SEED_DEV) {
|
|
903
|
+
logger$6('[client/actors] [addModelsToDb] Warning: Could not fetch schemas from EAS (background):', error.message);
|
|
904
|
+
}
|
|
905
|
+
else {
|
|
906
|
+
logger$6('[client/actors] [addModelsToDb] Error fetching schemas (background):', error.message);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
fetchEasAndPopulateModelUids();
|
|
911
|
+
return hasModelsInDb;
|
|
912
|
+
};
|
|
913
|
+
_addModelsToDb()
|
|
914
|
+
.then(() => {
|
|
915
|
+
sendBack({ type: ClientManagerEvents.ADD_MODELS_TO_DB_SUCCESS });
|
|
916
|
+
})
|
|
917
|
+
.catch((error) => {
|
|
918
|
+
logger$6('[client/actors] [addModelsToDb] Error:', error);
|
|
919
|
+
// In test environments, still send success to allow initialization to complete
|
|
920
|
+
if (process.env.NODE_ENV === 'test' || process.env.IS_SEED_DEV) {
|
|
921
|
+
logger$6('[client/actors] [addModelsToDb] Continuing despite error in test environment');
|
|
922
|
+
sendBack({ type: ClientManagerEvents.ADD_MODELS_TO_DB_SUCCESS });
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
sendBack({
|
|
926
|
+
type: 'error',
|
|
927
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
return () => { };
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
const logger$5 = debug('seedSdk:helpers:localDbChain');
|
|
935
|
+
/** appState key recording which EAS chain the local database holds attestations from. */
|
|
936
|
+
const EAS_CHAIN_ID_APP_STATE_KEY = 'easChainId';
|
|
937
|
+
/**
|
|
938
|
+
* Databases written before the chain was recorded only ever held Optimism Sepolia data
|
|
939
|
+
* (the only chain the SDK supported then).
|
|
940
|
+
*/
|
|
941
|
+
const LEGACY_CHAIN_ID = DEFAULT_EAS_CHAIN_ID;
|
|
942
|
+
async function readRecordedChainId(appDb) {
|
|
943
|
+
const rows = await appDb
|
|
944
|
+
.select({ value: appState.value })
|
|
945
|
+
.from(appState)
|
|
946
|
+
.where(eq(appState.key, EAS_CHAIN_ID_APP_STATE_KEY))
|
|
947
|
+
.limit(1);
|
|
948
|
+
const parsed = Number(rows[0]?.value);
|
|
949
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
950
|
+
}
|
|
951
|
+
async function recordChainId(appDb, chainId) {
|
|
952
|
+
const value = String(chainId);
|
|
953
|
+
await appDb
|
|
954
|
+
.insert(appState)
|
|
955
|
+
.values({ key: EAS_CHAIN_ID_APP_STATE_KEY, value })
|
|
956
|
+
.onConflictDoUpdate({ target: appState.key, set: { value } });
|
|
957
|
+
}
|
|
958
|
+
async function hasOnChainData(appDb) {
|
|
959
|
+
const rows = await appDb
|
|
960
|
+
.select({ uid: seeds.uid })
|
|
961
|
+
.from(seeds)
|
|
962
|
+
.where(and(isNotNull(seeds.uid), ne(seeds.uid, '')))
|
|
963
|
+
.limit(1);
|
|
964
|
+
return rows.length > 0;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Chain the local DB holds data from: the recorded one, or Optimism Sepolia for an unrecorded
|
|
968
|
+
* DB that already has attestations. Records the legacy value so later checks are cheap.
|
|
969
|
+
*/
|
|
970
|
+
async function resolveLocalDbChainId(appDb) {
|
|
971
|
+
const recorded = await readRecordedChainId(appDb);
|
|
972
|
+
if (recorded !== undefined)
|
|
973
|
+
return recorded;
|
|
974
|
+
if (await hasOnChainData(appDb)) {
|
|
975
|
+
await recordChainId(appDb, LEGACY_CHAIN_ID);
|
|
976
|
+
return LEGACY_CHAIN_ID;
|
|
977
|
+
}
|
|
978
|
+
return undefined;
|
|
979
|
+
}
|
|
980
|
+
/** How long EAS sync waits for `initPublish` to set the chain before using the current one. */
|
|
981
|
+
const EAS_CHAIN_SETTLE_TIMEOUT_MS = 30000;
|
|
982
|
+
/**
|
|
983
|
+
* Wait until the read chain is known before syncing. Only waits when `@seedprotocol/publish`
|
|
984
|
+
* is loaded but `initPublish` has not run, and no chain is configured or recorded locally.
|
|
985
|
+
*/
|
|
986
|
+
async function waitForEasReadChain(timeoutMs = EAS_CHAIN_SETTLE_TIMEOUT_MS) {
|
|
987
|
+
if (await whenEasReadChainSettled(timeoutMs))
|
|
988
|
+
return;
|
|
989
|
+
console.warn(`[Seed Protocol] EAS sync waited ${Math.round(timeoutMs / 1000)}s for initPublish to set the chain and is continuing on chain ${getEasReadChainId()}. Set SeedConfig.eas.chainId to skip the wait.`);
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Called at SDK init once the DB is ready. Registers the DB's chain with the EAS read side so
|
|
993
|
+
* reads start on that chain even before publish is initialized, and throws when SeedConfig.eas
|
|
994
|
+
* or PublishConfig.chain names a different chain. An empty DB is recorded only when a chain
|
|
995
|
+
* is configured explicitly; otherwise the first sync or publish records it.
|
|
996
|
+
*/
|
|
997
|
+
async function loadLocalDbChain() {
|
|
998
|
+
const appDb = BaseDb.getAppDb();
|
|
999
|
+
if (!appDb)
|
|
1000
|
+
return;
|
|
1001
|
+
const localChainId = await resolveLocalDbChainId(appDb);
|
|
1002
|
+
if (localChainId !== undefined) {
|
|
1003
|
+
configureEasReadChain('localDb', { chainId: localChainId });
|
|
1004
|
+
logger$5('local DB holds chain %d', localChainId);
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
if (isEasReadChainConfigured()) {
|
|
1008
|
+
const chainId = getEasReadChainId();
|
|
1009
|
+
await recordChainId(appDb, chainId);
|
|
1010
|
+
configureEasReadChain('localDb', { chainId });
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Call before writing chain data to the local DB (EAS sync, publish, revoke). Records the
|
|
1015
|
+
* current chain on an empty DB; throws when the DB holds data from a different chain.
|
|
1016
|
+
*/
|
|
1017
|
+
async function assertLocalDbChain() {
|
|
1018
|
+
const appDb = BaseDb.getAppDb();
|
|
1019
|
+
if (!appDb)
|
|
1020
|
+
return;
|
|
1021
|
+
const chainId = getEasReadChainId();
|
|
1022
|
+
const localChainId = await resolveLocalDbChainId(appDb);
|
|
1023
|
+
if (localChainId === undefined) {
|
|
1024
|
+
await recordChainId(appDb, chainId);
|
|
1025
|
+
}
|
|
1026
|
+
// Throws with a remediation message when the local chain differs from the configured one.
|
|
1027
|
+
configureEasReadChain('localDb', { chainId: localChainId ?? chainId });
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const logger$4 = debug('seedSdk:client:actors:saveConfig');
|
|
1031
|
+
/**
|
|
1032
|
+
* Saves the addresses passed to init. Hosts pass `addresses: []` before a wallet connects. In the
|
|
1033
|
+
* first (leader) tab that clears stored addresses, as it always has, so drafts made before the
|
|
1034
|
+
* wallet reconnects stay editable. A tab opened while another is running (`keepStoredWhenEmpty`)
|
|
1035
|
+
* keeps them instead: they're the other tab's connected wallet, which EAS sync, downloads and
|
|
1036
|
+
* publishing read. See docs/MULTI_TAB.md.
|
|
1037
|
+
*/
|
|
1038
|
+
async function persistInitAddresses(appDb, { addresses, ownedAddresses, watchedAddresses }, { keepStoredWhenEmpty }) {
|
|
1039
|
+
const owned = normalizeAddressList(ownedAddresses ?? addresses ?? []);
|
|
1040
|
+
const watched = normalizeAddressList(watchedAddresses ?? []);
|
|
1041
|
+
if (owned.length === 0 && watched.length === 0 && keepStoredWhenEmpty)
|
|
1042
|
+
return;
|
|
1043
|
+
const value = JSON.stringify({ owned, watched });
|
|
1044
|
+
await appDb
|
|
1045
|
+
.insert(appState)
|
|
1046
|
+
.values({ key: 'addresses', value })
|
|
1047
|
+
.onConflictDoUpdate({ target: appState.key, set: { value } });
|
|
1048
|
+
}
|
|
1049
|
+
const saveConfig = fromCallback(({ sendBack, input: { context } }) => {
|
|
1050
|
+
logger$4('saveConfig starting');
|
|
1051
|
+
const { endpoints, addresses, ownedAddresses, watchedAddresses, arweaveDomain, uploadApiBaseUrl, gatewayTransport, gatewayHyperKey, gatewaySidecarHost, gatewaySidecarPort, gatewayProxyBaseUrl, } = context;
|
|
1052
|
+
// Validate endpoints - required for proper initialization
|
|
1053
|
+
// If endpoints are missing or invalid, initialization should fail
|
|
1054
|
+
if (!endpoints || !endpoints.filePaths || !endpoints.files) {
|
|
1055
|
+
const error = new Error('saveConfig called with invalid endpoints: endpoints must include both filePaths and files');
|
|
1056
|
+
logger$4('[internal/actors] [saveConfig] Invalid endpoints:', { endpoints });
|
|
1057
|
+
throw error;
|
|
1058
|
+
}
|
|
1059
|
+
const _saveConfig = async () => {
|
|
1060
|
+
try {
|
|
1061
|
+
const appDb = BaseDb.getAppDb();
|
|
1062
|
+
if (!appDb) {
|
|
1063
|
+
// In test environments, continue anyway
|
|
1064
|
+
if (process.env.NODE_ENV === 'test' || process.env.IS_SEED_DEV) {
|
|
1065
|
+
logger$4('[internal/actors] [saveConfig] App DB not found, but continuing in test environment');
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
throw new Error('App DB not found');
|
|
1069
|
+
}
|
|
1070
|
+
const endpointsValueString = JSON.stringify(endpoints);
|
|
1071
|
+
// TODO: Figure out how to define on conflict with multiple rows added
|
|
1072
|
+
await appDb
|
|
1073
|
+
.insert(appState)
|
|
1074
|
+
.values({
|
|
1075
|
+
key: 'endpoints',
|
|
1076
|
+
value: endpointsValueString,
|
|
1077
|
+
})
|
|
1078
|
+
.onConflictDoUpdate({
|
|
1079
|
+
target: appState.key,
|
|
1080
|
+
set: {
|
|
1081
|
+
value: endpointsValueString,
|
|
1082
|
+
},
|
|
1083
|
+
});
|
|
1084
|
+
await persistInitAddresses(appDb, { addresses, ownedAddresses, watchedAddresses }, { keepStoredWhenEmpty: !isLeaderTab() });
|
|
1085
|
+
await appDb
|
|
1086
|
+
.insert(appState)
|
|
1087
|
+
.values({
|
|
1088
|
+
key: 'arweaveDomain',
|
|
1089
|
+
value: arweaveDomain || DEFAULT_ARWEAVE_HOST,
|
|
1090
|
+
})
|
|
1091
|
+
.onConflictDoUpdate({
|
|
1092
|
+
target: appState.key,
|
|
1093
|
+
set: {
|
|
1094
|
+
value: arweaveDomain || DEFAULT_ARWEAVE_HOST,
|
|
1095
|
+
},
|
|
1096
|
+
});
|
|
1097
|
+
const persistKey = async (key, value) => {
|
|
1098
|
+
if (value == null || value === '')
|
|
1099
|
+
return;
|
|
1100
|
+
await appDb
|
|
1101
|
+
.insert(appState)
|
|
1102
|
+
.values({ key, value })
|
|
1103
|
+
.onConflictDoUpdate({
|
|
1104
|
+
target: appState.key,
|
|
1105
|
+
set: { value },
|
|
1106
|
+
});
|
|
1107
|
+
};
|
|
1108
|
+
await persistKey('uploadApiBaseUrl', uploadApiBaseUrl);
|
|
1109
|
+
await persistKey('gatewayTransport', gatewayTransport);
|
|
1110
|
+
await persistKey('gatewayHyperKey', gatewayHyperKey);
|
|
1111
|
+
await persistKey('gatewaySidecarHost', gatewaySidecarHost);
|
|
1112
|
+
if (gatewaySidecarPort != null) {
|
|
1113
|
+
await persistKey('gatewaySidecarPort', String(gatewaySidecarPort));
|
|
1114
|
+
}
|
|
1115
|
+
await persistKey('gatewayProxyBaseUrl', gatewayProxyBaseUrl);
|
|
1116
|
+
// Throws when the DB holds attestations from a different chain than the one configured.
|
|
1117
|
+
await loadLocalDbChain();
|
|
1118
|
+
}
|
|
1119
|
+
catch (error) {
|
|
1120
|
+
logger$4('[internal/actors] [saveConfig] Error saving config:', error);
|
|
1121
|
+
// In test environments, continue anyway
|
|
1122
|
+
if (process.env.NODE_ENV === 'test' || process.env.IS_SEED_DEV) {
|
|
1123
|
+
logger$4('[internal/actors] [saveConfig] Continuing despite error in test environment');
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
throw error;
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
// Init writes run one tab at a time (docs/MULTI_TAB.md).
|
|
1130
|
+
withSeedDbLock('init', context.filesDir, _saveConfig)
|
|
1131
|
+
.then(() => {
|
|
1132
|
+
logger$4('[internal/actors] [saveConfig] saveConfig success');
|
|
1133
|
+
return sendBack({ type: ClientManagerEvents.SAVE_CONFIG_SUCCESS });
|
|
1134
|
+
})
|
|
1135
|
+
.catch((error) => {
|
|
1136
|
+
logger$4('[internal/actors] [saveConfig] Error in saveConfig promise chain:', error);
|
|
1137
|
+
// In test environments, still send success to allow state machine to progress
|
|
1138
|
+
if (process.env.NODE_ENV === 'test' || process.env.IS_SEED_DEV) {
|
|
1139
|
+
logger$4('[internal/actors] [saveConfig] Sending success despite error in test environment');
|
|
1140
|
+
sendBack({ type: ClientManagerEvents.SAVE_CONFIG_SUCCESS });
|
|
1141
|
+
}
|
|
1142
|
+
else {
|
|
1143
|
+
sendBack({
|
|
1144
|
+
type: 'error',
|
|
1145
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
});
|
|
1149
|
+
return () => { };
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
const logger$3 = debug('seedSdk:helpers:schema');
|
|
1153
|
+
/**
|
|
1154
|
+
* Sanitize a schema name to be filesystem-safe
|
|
1155
|
+
* Replaces all special characters (except alphanumeric, hyphens, underscores) with underscores
|
|
1156
|
+
* Converts spaces to underscores
|
|
1157
|
+
* Removes leading/trailing underscores
|
|
1158
|
+
*
|
|
1159
|
+
* @param name - Schema name to sanitize
|
|
1160
|
+
* @returns Sanitized name safe for use in filenames
|
|
1161
|
+
*/
|
|
1162
|
+
const sanitizeSchemaName$1 = (name) => {
|
|
1163
|
+
return name
|
|
1164
|
+
.replace(/[^a-zA-Z0-9\s_-]/g, '_') // Replace special chars (except spaces, hyphens, underscores) with underscore
|
|
1165
|
+
.replace(/\s+/g, '_') // Convert spaces to underscores
|
|
1166
|
+
.replace(/^_+|_+$/g, '') // Remove leading/trailing underscores
|
|
1167
|
+
.replace(/_+/g, '_'); // Collapse multiple underscores to single
|
|
1168
|
+
};
|
|
1169
|
+
/**
|
|
1170
|
+
* Generate filename for a schema
|
|
1171
|
+
* Format: {schemaFileId}_{schemaName}_v{version}.json
|
|
1172
|
+
*
|
|
1173
|
+
* The ID-first format ensures all files for a schema group together when sorted alphabetically.
|
|
1174
|
+
*
|
|
1175
|
+
* @param name - Schema name
|
|
1176
|
+
* @param version - Schema version
|
|
1177
|
+
* @param schemaFileId - Schema file ID (required)
|
|
1178
|
+
*/
|
|
1179
|
+
const getSchemaFilename = (name, version, schemaFileId) => {
|
|
1180
|
+
const sanitizedName = sanitizeSchemaName$1(name);
|
|
1181
|
+
return `${schemaFileId}_${sanitizedName}_v${version}.json`;
|
|
1182
|
+
};
|
|
1183
|
+
/**
|
|
1184
|
+
* Get the full file path for a schema
|
|
1185
|
+
* Format: {schemaFileId}_{schemaName}_v{version}.json
|
|
1186
|
+
*
|
|
1187
|
+
* The ID-first format ensures all files for a schema group together when sorted alphabetically.
|
|
1188
|
+
*
|
|
1189
|
+
* @param name - Schema name
|
|
1190
|
+
* @param version - Schema version
|
|
1191
|
+
* @param schemaFileId - Schema file ID (required)
|
|
1192
|
+
*/
|
|
1193
|
+
const getSchemaFilePath$1 = (name, version, schemaFileId) => {
|
|
1194
|
+
const path = BaseFileManager.getPathModule();
|
|
1195
|
+
const filename = getSchemaFilename(name, version, schemaFileId);
|
|
1196
|
+
const workingDir = BaseFileManager.getWorkingDir();
|
|
1197
|
+
return path.join(workingDir, filename);
|
|
1198
|
+
};
|
|
1199
|
+
/**
|
|
1200
|
+
* Create a new schema file
|
|
1201
|
+
* @param schema - The schema object to save
|
|
1202
|
+
* @param schemaFileId - Schema file ID (required)
|
|
1203
|
+
* @throws Error if schema already exists or if workingDir is invalid
|
|
1204
|
+
*/
|
|
1205
|
+
const createSchema = async (schema, schemaFileId) => {
|
|
1206
|
+
if (!schema.name || !schema.version) {
|
|
1207
|
+
throw new Error('Schema must have a name and version');
|
|
1208
|
+
}
|
|
1209
|
+
const filePath = getSchemaFilePath$1(schema.name, schema.version, schemaFileId);
|
|
1210
|
+
// Check if schema already exists
|
|
1211
|
+
const exists = await BaseFileManager.pathExists(filePath);
|
|
1212
|
+
if (exists) {
|
|
1213
|
+
throw new Error(`Schema ${schema.name} v${schema.version} already exists`);
|
|
1214
|
+
}
|
|
1215
|
+
// Ensure working directory exists
|
|
1216
|
+
const workingDir = BaseFileManager.getWorkingDir();
|
|
1217
|
+
await BaseFileManager.createDirIfNotExists(workingDir);
|
|
1218
|
+
// Write schema to file
|
|
1219
|
+
const content = JSON.stringify(schema, null, 2);
|
|
1220
|
+
await BaseFileManager.saveFile(filePath, content);
|
|
1221
|
+
};
|
|
1222
|
+
/**
|
|
1223
|
+
* Read a schema file by name and version
|
|
1224
|
+
* @param name - The name of the schema
|
|
1225
|
+
* @param version - The version of the schema
|
|
1226
|
+
* @param schemaFileId - Schema file ID (required)
|
|
1227
|
+
* @returns The schema object, or null if not found
|
|
1228
|
+
*/
|
|
1229
|
+
const readSchema = async (name, version, schemaFileId) => {
|
|
1230
|
+
const filePath = getSchemaFilePath$1(name, version, schemaFileId);
|
|
1231
|
+
const exists = await BaseFileManager.pathExists(filePath);
|
|
1232
|
+
if (!exists) {
|
|
1233
|
+
return null;
|
|
1234
|
+
}
|
|
1235
|
+
try {
|
|
1236
|
+
const content = await BaseFileManager.readFileAsString(filePath);
|
|
1237
|
+
return JSON.parse(content);
|
|
1238
|
+
}
|
|
1239
|
+
catch (error) {
|
|
1240
|
+
throw new Error(`Failed to read schema ${name} v${version}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
/**
|
|
1244
|
+
* Update an existing schema file
|
|
1245
|
+
* @param schema - The updated schema object (must have same name and version as existing)
|
|
1246
|
+
* @param schemaFileId - Schema file ID (required)
|
|
1247
|
+
* @throws Error if schema doesn't exist
|
|
1248
|
+
*/
|
|
1249
|
+
async function updateSchema(schema, schemaFileId) {
|
|
1250
|
+
if (!schema.name || !schema.version) {
|
|
1251
|
+
throw new Error('Schema must have a name and version');
|
|
1252
|
+
}
|
|
1253
|
+
const filePath = getSchemaFilePath$1(schema.name, schema.version, schemaFileId);
|
|
1254
|
+
// Check if schema exists
|
|
1255
|
+
const exists = await BaseFileManager.pathExists(filePath);
|
|
1256
|
+
if (!exists) {
|
|
1257
|
+
throw new Error(`Schema ${schema.name} v${schema.version} does not exist`);
|
|
1258
|
+
}
|
|
1259
|
+
// Write updated schema to file
|
|
1260
|
+
const content = JSON.stringify(schema, null, 2);
|
|
1261
|
+
await BaseFileManager.saveFile(filePath, content);
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Delete a schema file
|
|
1265
|
+
* @param name - The name of the schema
|
|
1266
|
+
* @param version - The version of the schema
|
|
1267
|
+
* @param schemaFileId - Schema file ID (required)
|
|
1268
|
+
* @throws Error if schema doesn't exist
|
|
1269
|
+
*/
|
|
1270
|
+
async function deleteSchema(name, version, schemaFileId) {
|
|
1271
|
+
const filePath = getSchemaFilePath$1(name, version, schemaFileId);
|
|
1272
|
+
// Check if schema exists
|
|
1273
|
+
const exists = await BaseFileManager.pathExists(filePath);
|
|
1274
|
+
if (!exists) {
|
|
1275
|
+
throw new Error(`Schema ${name} v${version} does not exist`);
|
|
1276
|
+
}
|
|
1277
|
+
// Delete the file
|
|
1278
|
+
const fs = await BaseFileManager.getFs();
|
|
1279
|
+
BaseFileManager.getPathModule();
|
|
1280
|
+
try {
|
|
1281
|
+
await fs.promises.unlink(filePath);
|
|
1282
|
+
}
|
|
1283
|
+
catch (error) {
|
|
1284
|
+
throw new Error(`Failed to delete schema ${name} v${version}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* List all schema files in the working directory
|
|
1289
|
+
* Only returns files that are NOT already complete schema files (i.e., files without $schema field)
|
|
1290
|
+
* This allows processing of minimal import files while skipping already-processed schema files
|
|
1291
|
+
* @returns Array of objects containing name, version, and file path for each schema
|
|
1292
|
+
*/
|
|
1293
|
+
async function listSchemaFiles() {
|
|
1294
|
+
const fs = await BaseFileManager.getFs();
|
|
1295
|
+
const path = BaseFileManager.getPathModule();
|
|
1296
|
+
// Check if working directory exists
|
|
1297
|
+
const workingDir = BaseFileManager.getWorkingDir();
|
|
1298
|
+
try {
|
|
1299
|
+
const files = await fs.promises.readdir(workingDir);
|
|
1300
|
+
const schemas = [];
|
|
1301
|
+
for (const file of files) {
|
|
1302
|
+
// Match filename pattern: {name}-v{version}.json
|
|
1303
|
+
const match = file.match(/^(.+)-v(\d+)\.json$/);
|
|
1304
|
+
if (match) {
|
|
1305
|
+
const [, name, versionStr] = match;
|
|
1306
|
+
const version = parseInt(versionStr, 10);
|
|
1307
|
+
if (!isNaN(version)) {
|
|
1308
|
+
const filePath = path.join(workingDir, file);
|
|
1309
|
+
// Check if this file is already a complete schema file (has $schema field)
|
|
1310
|
+
// If it is, skip it since it's already been processed
|
|
1311
|
+
try {
|
|
1312
|
+
const content = await BaseFileManager.readFileAsString(filePath);
|
|
1313
|
+
const data = JSON.parse(content);
|
|
1314
|
+
// Skip files that already have $schema (complete schema format)
|
|
1315
|
+
// These are already processed and don't need to be imported again
|
|
1316
|
+
if (data.$schema) {
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
catch (error) {
|
|
1321
|
+
// If we can't read/parse the file, skip it
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
schemas.push({
|
|
1325
|
+
name,
|
|
1326
|
+
version,
|
|
1327
|
+
filePath,
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
return schemas;
|
|
1333
|
+
}
|
|
1334
|
+
catch (error) {
|
|
1335
|
+
throw new Error(`Failed to list schemas: ${error instanceof Error ? error.message : String(error)}`);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
async function listCompleteSchemaFiles() {
|
|
1339
|
+
const fs = await BaseFileManager.getFs();
|
|
1340
|
+
const path = BaseFileManager.getPathModule();
|
|
1341
|
+
// Check if working directory exists
|
|
1342
|
+
const workingDir = BaseFileManager.getWorkingDir();
|
|
1343
|
+
try {
|
|
1344
|
+
// Check if directory exists before trying to read it
|
|
1345
|
+
const dirExists = await BaseFileManager.pathExists(workingDir);
|
|
1346
|
+
if (!dirExists) {
|
|
1347
|
+
// Directory doesn't exist yet - return empty array (not an error condition)
|
|
1348
|
+
logger$3(`Working directory does not exist yet: ${workingDir}, returning empty schema list`);
|
|
1349
|
+
return [];
|
|
1350
|
+
}
|
|
1351
|
+
const files = await fs.promises.readdir(workingDir);
|
|
1352
|
+
const schemas = [];
|
|
1353
|
+
for (const file of files) {
|
|
1354
|
+
// Match filename pattern: {schemaFileId}_{schemaName}_v{version}.json
|
|
1355
|
+
const match = file.match(/^(.+)_(.+)_v(\d+)\.json$/);
|
|
1356
|
+
if (match) {
|
|
1357
|
+
const [, schemaFileId, schemaName, versionStr] = match;
|
|
1358
|
+
const version = parseInt(versionStr, 10);
|
|
1359
|
+
if (!isNaN(version)) {
|
|
1360
|
+
const filePath = path.join(workingDir, file);
|
|
1361
|
+
// Check if this file is a complete schema file (has $schema field)
|
|
1362
|
+
try {
|
|
1363
|
+
const content = await BaseFileManager.readFileAsString(filePath);
|
|
1364
|
+
const data = JSON.parse(content);
|
|
1365
|
+
// Only include files that have $schema (complete schema format)
|
|
1366
|
+
if (data.$schema && data.metadata?.name) {
|
|
1367
|
+
// CRITICAL: Use schemaFileId from filename (source of truth), not from JSON content
|
|
1368
|
+
// The filename pattern is {schemaFileId}_{schemaName}_v{version}.json
|
|
1369
|
+
// The JSON content's id might be out of sync, but the filename tells us what file actually exists
|
|
1370
|
+
schemas.push({
|
|
1371
|
+
name: data.metadata.name,
|
|
1372
|
+
version: data.version || version,
|
|
1373
|
+
filePath,
|
|
1374
|
+
schemaFileId: schemaFileId, // Use from filename, not data.id
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
catch (error) {
|
|
1379
|
+
// If we can't read/parse the file, skip it
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
return schemas;
|
|
1386
|
+
}
|
|
1387
|
+
catch (error) {
|
|
1388
|
+
throw new Error(`Failed to list complete schemas: ${error instanceof Error ? error.message : String(error)}`);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Find a schema by name (returns the latest version if multiple versions exist)
|
|
1393
|
+
* @param name - The name of the schema
|
|
1394
|
+
* @returns The schema with the highest version, or null if not found
|
|
1395
|
+
*/
|
|
1396
|
+
async function findSchemaByName(name) {
|
|
1397
|
+
const schemas = await listCompleteSchemaFiles();
|
|
1398
|
+
const matchingSchemas = schemas.filter((s) => s.name === name);
|
|
1399
|
+
if (matchingSchemas.length === 0) {
|
|
1400
|
+
return null;
|
|
1401
|
+
}
|
|
1402
|
+
// Find the schema with the highest version
|
|
1403
|
+
const latest = matchingSchemas.reduce((prev, current) => current.version > prev.version ? current : prev);
|
|
1404
|
+
if (!latest.schemaFileId) {
|
|
1405
|
+
throw new Error(`Schema ${latest.name} v${latest.version} is missing schemaFileId`);
|
|
1406
|
+
}
|
|
1407
|
+
return readSchema(latest.name, latest.version, latest.schemaFileId);
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Get the latest version number for a schema by name
|
|
1411
|
+
* Only considers complete schema files (with $schema field)
|
|
1412
|
+
* @param name - The name of the schema
|
|
1413
|
+
* @returns The latest version number, or 0 if no schema found
|
|
1414
|
+
*/
|
|
1415
|
+
async function getLatestSchemaVersion(name) {
|
|
1416
|
+
const completeSchemas = await listCompleteSchemaFiles();
|
|
1417
|
+
const matchingSchemas = completeSchemas.filter((s) => s.name === name);
|
|
1418
|
+
if (matchingSchemas.length === 0) {
|
|
1419
|
+
return 0;
|
|
1420
|
+
}
|
|
1421
|
+
// Find the schema with the highest version
|
|
1422
|
+
const latest = matchingSchemas.reduce((prev, current) => current.version > prev.version ? current : prev);
|
|
1423
|
+
return latest.version;
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Get only the latest version of each schema
|
|
1427
|
+
* @returns Array of objects containing name, version, and file path for the latest version of each schema
|
|
1428
|
+
*/
|
|
1429
|
+
async function listLatestSchemaFiles() {
|
|
1430
|
+
const completeSchemas = await listCompleteSchemaFiles();
|
|
1431
|
+
// Group by schema name and keep only the latest version of each
|
|
1432
|
+
const schemaMap = new Map();
|
|
1433
|
+
for (const schema of completeSchemas) {
|
|
1434
|
+
const existing = schemaMap.get(schema.name);
|
|
1435
|
+
if (!existing || schema.version > existing.version) {
|
|
1436
|
+
schemaMap.set(schema.name, schema);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
return Array.from(schemaMap.values());
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Filter an array of schemas to only include the latest version for each schema name
|
|
1443
|
+
* Works with any object that has a name (via metadata.name or name property) and a version property
|
|
1444
|
+
* @param schemas - Array of schema objects
|
|
1445
|
+
* @returns Array containing only the latest version of each schema
|
|
1446
|
+
*/
|
|
1447
|
+
function filterLatestSchemas(schemas) {
|
|
1448
|
+
// Group by schema name and keep only the latest version of each
|
|
1449
|
+
const schemaMap = new Map();
|
|
1450
|
+
for (const schema of schemas) {
|
|
1451
|
+
const schemaName = schema.metadata?.name || schema.name;
|
|
1452
|
+
if (!schemaName) {
|
|
1453
|
+
continue; // Skip schemas without a name
|
|
1454
|
+
}
|
|
1455
|
+
const existing = schemaMap.get(schemaName);
|
|
1456
|
+
if (!existing || schema.version > existing.version) {
|
|
1457
|
+
schemaMap.set(schemaName, schema);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
return Array.from(schemaMap.values());
|
|
1461
|
+
}
|
|
1462
|
+
async function loadAllSchemasFromDb() {
|
|
1463
|
+
const db = BaseDb.getAppDb();
|
|
1464
|
+
if (!db) {
|
|
1465
|
+
throw new Error('Database not found');
|
|
1466
|
+
}
|
|
1467
|
+
const result = [];
|
|
1468
|
+
// STEP 1: Query all schemas from database
|
|
1469
|
+
const dbSchemas = await db
|
|
1470
|
+
.select()
|
|
1471
|
+
.from(schemas)
|
|
1472
|
+
.orderBy(schemas.name, desc(schemas.version));
|
|
1473
|
+
const processedSchemaNames = new Set();
|
|
1474
|
+
// List the schema files once. listCompleteSchemaFiles reads and parses every file in the working
|
|
1475
|
+
// dir, so listing per schema made this O(files x schemas).
|
|
1476
|
+
let completeSchemaFiles;
|
|
1477
|
+
const getCompleteSchemaFiles = async () => {
|
|
1478
|
+
if (!completeSchemaFiles) {
|
|
1479
|
+
try {
|
|
1480
|
+
completeSchemaFiles = await listCompleteSchemaFiles();
|
|
1481
|
+
}
|
|
1482
|
+
catch (error) {
|
|
1483
|
+
// If we can't list schema files (e.g., directory doesn't exist yet), continue without file-based loading
|
|
1484
|
+
logger$3(`Error listing complete schema files (continuing with DB data): ${error instanceof Error ? error.message : String(error)}`);
|
|
1485
|
+
completeSchemaFiles = [];
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
return completeSchemaFiles;
|
|
1489
|
+
};
|
|
1490
|
+
// STEP 2: Process each database schema
|
|
1491
|
+
for (const dbSchema of dbSchemas) {
|
|
1492
|
+
const schemaName = dbSchema.name;
|
|
1493
|
+
// Skip if we've already processed a newer version of this schema
|
|
1494
|
+
if (processedSchemaNames.has(schemaName)) {
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
// If it's a draft, load from schemaData
|
|
1498
|
+
if (dbSchema.isDraft === true && dbSchema.schemaData) {
|
|
1499
|
+
try {
|
|
1500
|
+
let schemaFile = JSON.parse(dbSchema.schemaData);
|
|
1501
|
+
// Merge models from DB only when schemaData is incomplete. When schemaData has models (e.g. init),
|
|
1502
|
+
// skip the expensive loadModelsFromDbForSchema - it can return 1000s of orphaned model_schemas rows
|
|
1503
|
+
// and cause init to hang. The merge is for drafts where schemaData may be missing models.
|
|
1504
|
+
const schemaModelCount = Object.keys(schemaFile.models || {}).length;
|
|
1505
|
+
let mergedModels = { ...(schemaFile.models || {}) };
|
|
1506
|
+
if (dbSchema.id && schemaModelCount === 0) {
|
|
1507
|
+
const dbModels = await loadModelsFromDbForSchema(dbSchema.id);
|
|
1508
|
+
if (Object.keys(dbModels).length > 0) {
|
|
1509
|
+
mergedModels = { ...mergedModels, ...dbModels };
|
|
1510
|
+
for (const [modelName, dbModel] of Object.entries(dbModels)) {
|
|
1511
|
+
if (mergedModels[modelName]) {
|
|
1512
|
+
mergedModels[modelName] = {
|
|
1513
|
+
...mergedModels[modelName],
|
|
1514
|
+
properties: {
|
|
1515
|
+
...mergedModels[modelName].properties,
|
|
1516
|
+
...dbModel.properties,
|
|
1517
|
+
},
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
schemaFile = {
|
|
1524
|
+
...schemaFile,
|
|
1525
|
+
models: mergedModels,
|
|
1526
|
+
};
|
|
1527
|
+
result.push({
|
|
1528
|
+
schema: schemaFile,
|
|
1529
|
+
isDraft: true,
|
|
1530
|
+
source: 'db',
|
|
1531
|
+
schemaRecordId: dbSchema.id || undefined,
|
|
1532
|
+
});
|
|
1533
|
+
processedSchemaNames.add(schemaName);
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
catch (error) {
|
|
1537
|
+
logger$3(`Error parsing schemaData for ${schemaName}:`, error);
|
|
1538
|
+
// Fall through to file-based loading
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
// If it's not a draft and has schemaFileId, try to load from file
|
|
1542
|
+
if (dbSchema.isDraft === false && dbSchema.schemaFileId) {
|
|
1543
|
+
const completeSchemas = await getCompleteSchemaFiles();
|
|
1544
|
+
const matchingSchemas = completeSchemas.filter((s) => s.name === schemaName);
|
|
1545
|
+
if (matchingSchemas.length > 0) {
|
|
1546
|
+
// Find the schema with the highest version
|
|
1547
|
+
const latest = matchingSchemas.reduce((prev, current) => current.version > prev.version ? current : prev);
|
|
1548
|
+
try {
|
|
1549
|
+
// Read the file directly to get SchemaFileFormat
|
|
1550
|
+
const content = await BaseFileManager.readFileAsString(latest.filePath);
|
|
1551
|
+
const schemaFile = JSON.parse(content);
|
|
1552
|
+
// Check if file is newer than DB record
|
|
1553
|
+
const fileUpdatedAt = new Date(schemaFile.metadata.updatedAt).getTime();
|
|
1554
|
+
const dbUpdatedAt = dbSchema.updatedAt || 0;
|
|
1555
|
+
const dbMatchesFile = schemaFile.id === dbSchema.schemaFileId &&
|
|
1556
|
+
schemaFile.version === dbSchema.version &&
|
|
1557
|
+
fileUpdatedAt <= dbUpdatedAt;
|
|
1558
|
+
// If file is newer, update DB with file content
|
|
1559
|
+
if (fileUpdatedAt > dbUpdatedAt && schemaFile.id === dbSchema.schemaFileId) {
|
|
1560
|
+
await addSchemaToDb({
|
|
1561
|
+
name: schemaName,
|
|
1562
|
+
version: schemaFile.version,
|
|
1563
|
+
createdAt: new Date(schemaFile.metadata.createdAt).getTime(),
|
|
1564
|
+
updatedAt: fileUpdatedAt,
|
|
1565
|
+
}, schemaFile.id, JSON.stringify(schemaFile, null, 2), false);
|
|
1566
|
+
}
|
|
1567
|
+
result.push({
|
|
1568
|
+
schema: schemaFile,
|
|
1569
|
+
isDraft: false,
|
|
1570
|
+
source: 'db+file',
|
|
1571
|
+
schemaRecordId: dbSchema.id || undefined,
|
|
1572
|
+
filePath: latest.filePath,
|
|
1573
|
+
dbMatchesFile,
|
|
1574
|
+
});
|
|
1575
|
+
processedSchemaNames.add(schemaName);
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
catch (error) {
|
|
1579
|
+
logger$3(`Error loading schema file for ${schemaName}:`, error);
|
|
1580
|
+
// Fall through to try loading from schemaData if available
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
// If file doesn't exist but we have schemaData, use that
|
|
1584
|
+
if (dbSchema.schemaData) {
|
|
1585
|
+
try {
|
|
1586
|
+
let schemaFile = JSON.parse(dbSchema.schemaData);
|
|
1587
|
+
// CRITICAL: Merge models from database (model_schemas join table) with models from schemaData
|
|
1588
|
+
// This ensures models added to the database are included even if they're not in schemaData
|
|
1589
|
+
// Build merged models without mutating schemaFile (read-only approach)
|
|
1590
|
+
const schemaModelCount = Object.keys(schemaFile.models || {}).length;
|
|
1591
|
+
let mergedModels = { ...(schemaFile.models || {}) };
|
|
1592
|
+
if (dbSchema.id && schemaModelCount === 0) {
|
|
1593
|
+
const dbModels = await loadModelsFromDbForSchema(dbSchema.id);
|
|
1594
|
+
if (Object.keys(dbModels).length > 0) {
|
|
1595
|
+
mergedModels = { ...mergedModels, ...dbModels };
|
|
1596
|
+
for (const [modelName, dbModel] of Object.entries(dbModels)) {
|
|
1597
|
+
if (mergedModels[modelName]) {
|
|
1598
|
+
mergedModels[modelName] = {
|
|
1599
|
+
...mergedModels[modelName],
|
|
1600
|
+
properties: {
|
|
1601
|
+
...mergedModels[modelName].properties,
|
|
1602
|
+
...dbModel.properties,
|
|
1603
|
+
},
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
schemaFile = {
|
|
1610
|
+
...schemaFile,
|
|
1611
|
+
models: mergedModels,
|
|
1612
|
+
};
|
|
1613
|
+
result.push({
|
|
1614
|
+
schema: schemaFile,
|
|
1615
|
+
isDraft: false,
|
|
1616
|
+
source: 'db',
|
|
1617
|
+
schemaRecordId: dbSchema.id || undefined,
|
|
1618
|
+
});
|
|
1619
|
+
processedSchemaNames.add(schemaName);
|
|
1620
|
+
continue;
|
|
1621
|
+
}
|
|
1622
|
+
catch (error) {
|
|
1623
|
+
logger$3(`Error parsing schemaData for ${schemaName}:`, error);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
// STEP 3: Find schema files not yet in database (for migration/backward compatibility)
|
|
1629
|
+
const completeSchemas = await getCompleteSchemaFiles();
|
|
1630
|
+
for (const schemaFileInfo of completeSchemas) {
|
|
1631
|
+
// Only process if not already in result
|
|
1632
|
+
if (!processedSchemaNames.has(schemaFileInfo.name)) {
|
|
1633
|
+
if (isInternalSchema(schemaFileInfo.name, schemaFileInfo.schemaFileId)) {
|
|
1634
|
+
logger$3(`STEP 3: skipping internal schema file: ${schemaFileInfo.filePath}`);
|
|
1635
|
+
continue;
|
|
1636
|
+
}
|
|
1637
|
+
try {
|
|
1638
|
+
logger$3(`STEP 3: processing schema file: ${schemaFileInfo.name} from ${schemaFileInfo.filePath}`);
|
|
1639
|
+
const content = await BaseFileManager.readFileAsString(schemaFileInfo.filePath);
|
|
1640
|
+
const schemaFile = JSON.parse(content);
|
|
1641
|
+
// Import this schema to database (mark as non-draft since it's in a file)
|
|
1642
|
+
const schemaRecord = await addSchemaToDb({
|
|
1643
|
+
name: schemaFile.metadata.name,
|
|
1644
|
+
version: schemaFile.version,
|
|
1645
|
+
createdAt: new Date(schemaFile.metadata.createdAt).getTime(),
|
|
1646
|
+
updatedAt: new Date(schemaFile.metadata.updatedAt).getTime(),
|
|
1647
|
+
}, schemaFile.id, JSON.stringify(schemaFile, null, 2), false);
|
|
1648
|
+
result.push({
|
|
1649
|
+
schema: schemaFile,
|
|
1650
|
+
isDraft: false,
|
|
1651
|
+
source: 'file',
|
|
1652
|
+
schemaRecordId: schemaRecord.id || undefined,
|
|
1653
|
+
filePath: schemaFileInfo.filePath,
|
|
1654
|
+
});
|
|
1655
|
+
processedSchemaNames.add(schemaFileInfo.name);
|
|
1656
|
+
}
|
|
1657
|
+
catch (error) {
|
|
1658
|
+
logger$3(`Error loading schema file ${schemaFileInfo.filePath}:`, error);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
return result;
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Migration helper: Migrate existing file-based schemas to the database
|
|
1666
|
+
* This function should be called once to migrate existing schema files to the new database-first approach
|
|
1667
|
+
* All existing schemas will be marked as published (isDraft = false) since they already exist in files
|
|
1668
|
+
* @returns Number of schemas migrated
|
|
1669
|
+
*/
|
|
1670
|
+
async function migrateFileSchemasToDb() {
|
|
1671
|
+
const db = BaseDb.getAppDb();
|
|
1672
|
+
if (!db) {
|
|
1673
|
+
throw new Error('Database not found');
|
|
1674
|
+
}
|
|
1675
|
+
logger$3('Starting migration of file-based schemas to database');
|
|
1676
|
+
// Get all complete schema files
|
|
1677
|
+
const completeSchemas = await listCompleteSchemaFiles();
|
|
1678
|
+
let migratedCount = 0;
|
|
1679
|
+
for (const schemaFileInfo of completeSchemas) {
|
|
1680
|
+
try {
|
|
1681
|
+
// Read the schema file
|
|
1682
|
+
const content = await BaseFileManager.readFileAsString(schemaFileInfo.filePath);
|
|
1683
|
+
const schemaFile = JSON.parse(content);
|
|
1684
|
+
if (!schemaFile.$schema || !schemaFile.metadata?.name) {
|
|
1685
|
+
logger$3(`Skipping invalid schema file: ${schemaFileInfo.filePath}`);
|
|
1686
|
+
continue;
|
|
1687
|
+
}
|
|
1688
|
+
const schemaName = schemaFile.metadata.name;
|
|
1689
|
+
// Check if schema already exists in database
|
|
1690
|
+
const existingSchemas = await db
|
|
1691
|
+
.select()
|
|
1692
|
+
.from(schemas)
|
|
1693
|
+
.where(eq(schemas.name, schemaName))
|
|
1694
|
+
.limit(1);
|
|
1695
|
+
// Only migrate if it doesn't exist or if existing record doesn't have schemaFileId
|
|
1696
|
+
if (existingSchemas.length === 0 || !existingSchemas[0].schemaFileId) {
|
|
1697
|
+
// Add schema to database as published (isDraft = false)
|
|
1698
|
+
await addSchemaToDb({
|
|
1699
|
+
name: schemaName,
|
|
1700
|
+
version: schemaFile.version,
|
|
1701
|
+
createdAt: new Date(schemaFile.metadata.createdAt).getTime(),
|
|
1702
|
+
updatedAt: new Date(schemaFile.metadata.updatedAt).getTime(),
|
|
1703
|
+
}, schemaFile.id, // schemaFileId
|
|
1704
|
+
JSON.stringify(schemaFile, null, 2), // schemaData
|
|
1705
|
+
false);
|
|
1706
|
+
migratedCount++;
|
|
1707
|
+
logger$3(`Migrated schema: ${schemaName} v${schemaFile.version}`);
|
|
1708
|
+
}
|
|
1709
|
+
else {
|
|
1710
|
+
logger$3(`Schema ${schemaName} already exists in database, skipping`);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
catch (error) {
|
|
1714
|
+
logger$3(`Error migrating schema file ${schemaFileInfo.filePath}:`, error);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
logger$3(`Migration complete: ${migratedCount} schemas migrated`);
|
|
1718
|
+
return migratedCount;
|
|
1719
|
+
}
|
|
1720
|
+
/**
|
|
1721
|
+
* Extract the schema name from a schemaId string
|
|
1722
|
+
* @param schemaId - Schema ID in the format `${schemaName}-${schemaVersion}`
|
|
1723
|
+
* @returns The schema name, or null if the format is invalid
|
|
1724
|
+
*/
|
|
1725
|
+
function getSchemaNameFromId(schemaId) {
|
|
1726
|
+
if (!schemaId) {
|
|
1727
|
+
return null;
|
|
1728
|
+
}
|
|
1729
|
+
const lastDashIndex = schemaId.lastIndexOf('-');
|
|
1730
|
+
if (lastDashIndex === -1 || lastDashIndex === 0 || lastDashIndex === schemaId.length - 1) {
|
|
1731
|
+
return null;
|
|
1732
|
+
}
|
|
1733
|
+
return schemaId.substring(0, lastDashIndex);
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
var schema = /*#__PURE__*/Object.freeze({
|
|
1737
|
+
__proto__: null,
|
|
1738
|
+
createSchema: createSchema,
|
|
1739
|
+
deleteSchema: deleteSchema,
|
|
1740
|
+
filterLatestSchemas: filterLatestSchemas,
|
|
1741
|
+
findSchemaByName: findSchemaByName,
|
|
1742
|
+
getLatestSchemaVersion: getLatestSchemaVersion,
|
|
1743
|
+
getSchemaNameFromId: getSchemaNameFromId,
|
|
1744
|
+
listCompleteSchemaFiles: listCompleteSchemaFiles,
|
|
1745
|
+
listLatestSchemaFiles: listLatestSchemaFiles,
|
|
1746
|
+
listSchemaFiles: listSchemaFiles,
|
|
1747
|
+
loadAllSchemasFromDb: loadAllSchemasFromDb,
|
|
1748
|
+
migrateFileSchemasToDb: migrateFileSchemasToDb,
|
|
1749
|
+
readSchema: readSchema,
|
|
1750
|
+
updateSchema: updateSchema
|
|
1751
|
+
});
|
|
1752
|
+
|
|
1753
|
+
function toBytes(data) {
|
|
1754
|
+
if (typeof data === 'string')
|
|
1755
|
+
return utf8ToBytes(data);
|
|
1756
|
+
if (data instanceof Uint8Array)
|
|
1757
|
+
return data;
|
|
1758
|
+
return new Uint8Array(data);
|
|
1759
|
+
}
|
|
1760
|
+
const getContentHash = async (data) => {
|
|
1761
|
+
return bytesToHex(sha3_256(toBytes(data)));
|
|
1762
|
+
};
|
|
1763
|
+
/**
|
|
1764
|
+
* Returns a function that maps `suffix` to `getDeterministicId(prefix + suffix)`, hashing `prefix` only
|
|
1765
|
+
* once. Use it when deriving many ids from one long shared prefix (e.g. a whole serialized schema).
|
|
1766
|
+
*/
|
|
1767
|
+
const getDeterministicIdsWithPrefix = (prefix) => {
|
|
1768
|
+
const prefixHash = sha3_256.create().update(utf8ToBytes(prefix));
|
|
1769
|
+
return (suffix) => bytesToHex(prefixHash.clone().update(utf8ToBytes(suffix)).digest()).slice(0, 10);
|
|
1770
|
+
};
|
|
1771
|
+
|
|
1772
|
+
const logger$2 = debug('seedSdk:imports:json');
|
|
1773
|
+
/**
|
|
1774
|
+
* Get refValueType from a property object with case-insensitive key lookup.
|
|
1775
|
+
* Handles refValueType, refvaluetype, ref_value_type, elementType, and legacy items.type / items.Type.
|
|
1776
|
+
* Also handles items as string (e.g. items: "Relation").
|
|
1777
|
+
*/
|
|
1778
|
+
/**
|
|
1779
|
+
* Normalize schema file format: convert models/properties arrays to objects if needed.
|
|
1780
|
+
* Some tools output models/properties as arrays; we need object format keyed by name or index.
|
|
1781
|
+
*/
|
|
1782
|
+
function normalizeSchemaModels(models) {
|
|
1783
|
+
if (!models || typeof models !== 'object')
|
|
1784
|
+
return {};
|
|
1785
|
+
if (!Array.isArray(models))
|
|
1786
|
+
return models;
|
|
1787
|
+
const result = {};
|
|
1788
|
+
for (let i = 0; i < models.length; i++) {
|
|
1789
|
+
const m = models[i];
|
|
1790
|
+
const key = m?.name ?? m?.id ?? String(i);
|
|
1791
|
+
const props = m?.properties;
|
|
1792
|
+
let properties = {};
|
|
1793
|
+
if (props && typeof props === 'object') {
|
|
1794
|
+
if (Array.isArray(props)) {
|
|
1795
|
+
for (let j = 0; j < props.length; j++) {
|
|
1796
|
+
const p = props[j];
|
|
1797
|
+
const pKey = p?.name ?? p?.id ?? String(j);
|
|
1798
|
+
properties[pKey] = p;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
else {
|
|
1802
|
+
properties = props;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
result[key] = { ...m, properties };
|
|
1806
|
+
}
|
|
1807
|
+
return result;
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* Get refValueType from a property object with case-insensitive key lookup.
|
|
1811
|
+
*/
|
|
1812
|
+
function getRefValueType(prop) {
|
|
1813
|
+
const refValueType = prop.refValueType ??
|
|
1814
|
+
prop.refvaluetype ??
|
|
1815
|
+
prop.RefValueType ??
|
|
1816
|
+
prop.REFVALUETYPE ??
|
|
1817
|
+
prop.ref_value_type;
|
|
1818
|
+
if (refValueType != null && String(refValueType).trim() !== '') {
|
|
1819
|
+
return String(refValueType);
|
|
1820
|
+
}
|
|
1821
|
+
const elementType = prop.elementType ?? prop.element_type;
|
|
1822
|
+
if (elementType != null && String(elementType).trim() !== '') {
|
|
1823
|
+
return String(elementType);
|
|
1824
|
+
}
|
|
1825
|
+
const items = prop.items;
|
|
1826
|
+
if (items != null) {
|
|
1827
|
+
if (typeof items === 'string' && items.trim() !== '') {
|
|
1828
|
+
return items.trim();
|
|
1829
|
+
}
|
|
1830
|
+
if (items && typeof items === 'object') {
|
|
1831
|
+
const itemsType = items.type ?? items.Type ?? items.TYPE;
|
|
1832
|
+
if (itemsType != null && String(itemsType).trim() !== '') {
|
|
1833
|
+
return String(itemsType);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
return undefined;
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* Verify that properties are persisted to the database for a given model
|
|
1841
|
+
* This is important in browser environments where database writes may not be immediately visible
|
|
1842
|
+
* @param db - Database instance
|
|
1843
|
+
* @param modelId - Database ID of the model
|
|
1844
|
+
* @param modelName - Name of the model (for logging)
|
|
1845
|
+
* @param maxRetries - Maximum number of retry attempts
|
|
1846
|
+
* @param retryDelay - Delay between retries in milliseconds
|
|
1847
|
+
* @returns Promise that resolves when properties are found, or rejects if not found after max retries
|
|
1848
|
+
*/
|
|
1849
|
+
const verifyPropertiesPersisted = async (db, modelId, modelName, maxRetries = 10, retryDelay = 100) => {
|
|
1850
|
+
const _mod_0 = await import('./BasePathResolver-DRT09oVv.js');
|
|
1851
|
+
const _ns_0 = _mod_0.N;
|
|
1852
|
+
const modelSchemaMod = _ns_0;
|
|
1853
|
+
const { properties: propertiesTable } = modelSchemaMod;
|
|
1854
|
+
const drizzleMod = await import('drizzle-orm');
|
|
1855
|
+
const { eq } = drizzleMod;
|
|
1856
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
1857
|
+
const props = await db
|
|
1858
|
+
.select()
|
|
1859
|
+
.from(propertiesTable)
|
|
1860
|
+
.where(eq(propertiesTable.modelId, modelId))
|
|
1861
|
+
.limit(1);
|
|
1862
|
+
if (props.length > 0) {
|
|
1863
|
+
logger$2(`Verified properties exist for model "${modelName}" (modelId: ${modelId}) after ${attempt + 1} attempt(s)`);
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
if (attempt < maxRetries - 1) {
|
|
1867
|
+
logger$2(`Properties not yet visible for model "${modelName}" (modelId: ${modelId}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${maxRetries})`);
|
|
1868
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
throw new Error(`Properties not found for model "${modelName}" (modelId: ${modelId}) after ${maxRetries} attempts. This may indicate a database write issue in the browser environment.`);
|
|
1872
|
+
};
|
|
1873
|
+
/**
|
|
1874
|
+
* Compare two schema files structurally, ignoring metadata timestamps
|
|
1875
|
+
* This is used to determine if an existing schema file matches the schema being imported
|
|
1876
|
+
* @param schema1 - First schema to compare
|
|
1877
|
+
* @param schema2 - Second schema to compare
|
|
1878
|
+
* @returns true if schemas are structurally equivalent, false otherwise
|
|
1879
|
+
*/
|
|
1880
|
+
function compareSchemasStructurally(schema1, schema2) {
|
|
1881
|
+
// Compare basic fields
|
|
1882
|
+
if (schema1.$schema !== schema2.$schema)
|
|
1883
|
+
return false;
|
|
1884
|
+
if (schema1.version !== schema2.version)
|
|
1885
|
+
return false;
|
|
1886
|
+
if (schema1.id !== schema2.id)
|
|
1887
|
+
return false;
|
|
1888
|
+
// Compare metadata (ignoring timestamps)
|
|
1889
|
+
if (schema1.metadata?.name !== schema2.metadata?.name)
|
|
1890
|
+
return false;
|
|
1891
|
+
// Compare models structure
|
|
1892
|
+
const models1 = schema1.models || {};
|
|
1893
|
+
const models2 = schema2.models || {};
|
|
1894
|
+
const modelNames1 = Object.keys(models1).sort();
|
|
1895
|
+
const modelNames2 = Object.keys(models2).sort();
|
|
1896
|
+
if (modelNames1.length !== modelNames2.length)
|
|
1897
|
+
return false;
|
|
1898
|
+
if (!modelNames1.every((name, i) => name === modelNames2[i]))
|
|
1899
|
+
return false;
|
|
1900
|
+
// Compare each model's structure
|
|
1901
|
+
for (const modelName of modelNames1) {
|
|
1902
|
+
const model1 = models1[modelName];
|
|
1903
|
+
const model2 = models2[modelName];
|
|
1904
|
+
// Compare model IDs if both have them
|
|
1905
|
+
if (model1.id !== model2.id)
|
|
1906
|
+
return false;
|
|
1907
|
+
// Compare properties
|
|
1908
|
+
const props1 = model1.properties || {};
|
|
1909
|
+
const props2 = model2.properties || {};
|
|
1910
|
+
const propNames1 = Object.keys(props1).sort();
|
|
1911
|
+
const propNames2 = Object.keys(props2).sort();
|
|
1912
|
+
if (propNames1.length !== propNames2.length)
|
|
1913
|
+
return false;
|
|
1914
|
+
if (!propNames1.every((name, i) => name === propNames2[i]))
|
|
1915
|
+
return false;
|
|
1916
|
+
// Compare each property's structure
|
|
1917
|
+
for (const propName of propNames1) {
|
|
1918
|
+
const prop1 = props1[propName];
|
|
1919
|
+
const prop2 = props2[propName];
|
|
1920
|
+
// Compare property IDs if both have them
|
|
1921
|
+
if (prop1.id !== prop2.id)
|
|
1922
|
+
return false;
|
|
1923
|
+
// Compare property types
|
|
1924
|
+
if (prop1.type !== prop2.type)
|
|
1925
|
+
return false;
|
|
1926
|
+
// Compare other relevant fields (ref, refValueType for List, etc.)
|
|
1927
|
+
if (prop1.ref !== prop2.ref)
|
|
1928
|
+
return false;
|
|
1929
|
+
const refVal1 = prop1.refValueType ?? prop1.items?.type;
|
|
1930
|
+
const refVal2 = prop2.refValueType ?? prop2.items?.type;
|
|
1931
|
+
if (refVal1 !== refVal2)
|
|
1932
|
+
return false;
|
|
1933
|
+
const model1 = prop1.ref ?? prop1.items?.model;
|
|
1934
|
+
const model2 = prop2.ref ?? prop2.items?.model;
|
|
1935
|
+
if (model1 !== model2)
|
|
1936
|
+
return false;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
// Compare enums
|
|
1940
|
+
const enums1 = schema1.enums || {};
|
|
1941
|
+
const enums2 = schema2.enums || {};
|
|
1942
|
+
const enumNames1 = Object.keys(enums1).sort();
|
|
1943
|
+
const enumNames2 = Object.keys(enums2).sort();
|
|
1944
|
+
if (enumNames1.length !== enumNames2.length)
|
|
1945
|
+
return false;
|
|
1946
|
+
if (!enumNames1.every((name, i) => name === enumNames2[i]))
|
|
1947
|
+
return false;
|
|
1948
|
+
// Note: We don't compare migrations as they're historical records
|
|
1949
|
+
// and may differ even for the same schema structure
|
|
1950
|
+
return true;
|
|
1951
|
+
}
|
|
1952
|
+
/**
|
|
1953
|
+
* Create a canonical JSON string for schema comparison (ignores IDs and timestamps).
|
|
1954
|
+
* Used to detect if schema content has changed via string comparison of schemaData.
|
|
1955
|
+
*/
|
|
1956
|
+
function getSchemaContentForComparison(schema) {
|
|
1957
|
+
const normalized = {
|
|
1958
|
+
name: schema.metadata?.name,
|
|
1959
|
+
version: schema.version,
|
|
1960
|
+
models: {},
|
|
1961
|
+
enums: schema.enums || {},
|
|
1962
|
+
};
|
|
1963
|
+
for (const modelName of Object.keys(schema.models || {}).sort()) {
|
|
1964
|
+
const model = schema.models[modelName];
|
|
1965
|
+
const props = {};
|
|
1966
|
+
for (const propName of Object.keys(model.properties || {}).sort()) {
|
|
1967
|
+
const prop = model.properties[propName];
|
|
1968
|
+
const items = prop.items;
|
|
1969
|
+
props[propName] = {
|
|
1970
|
+
type: prop.type ?? prop.dataType,
|
|
1971
|
+
ref: prop.ref ?? prop.refModelName,
|
|
1972
|
+
refValueType: prop.refValueType ?? items?.type,
|
|
1973
|
+
model: prop.model ?? items?.model,
|
|
1974
|
+
storage: prop.storage,
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
normalized.models[modelName] = {
|
|
1978
|
+
description: model.description,
|
|
1979
|
+
properties: props,
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
return JSON.stringify(normalized);
|
|
1983
|
+
}
|
|
1984
|
+
/**
|
|
1985
|
+
* Compare two schemas by structure (ignoring IDs and timestamps).
|
|
1986
|
+
* Returns true if schema content is equivalent.
|
|
1987
|
+
*/
|
|
1988
|
+
function compareSchemaContent(schema1, schema2) {
|
|
1989
|
+
return getSchemaContentForComparison(schema1) === getSchemaContentForComparison(schema2);
|
|
1990
|
+
}
|
|
1991
|
+
/**
|
|
1992
|
+
* Generate a deterministic schemaFileId from schema content.
|
|
1993
|
+
* Same schema content always produces the same id, preventing duplicate schema files
|
|
1994
|
+
* when processSchemaFiles runs multiple times (e.g. React StrictMode, app restarts).
|
|
1995
|
+
*/
|
|
1996
|
+
async function getDeterministicSchemaFileId(schemaFile) {
|
|
1997
|
+
const content = getSchemaContentForComparison(schemaFile);
|
|
1998
|
+
const hash = await getContentHash(content);
|
|
1999
|
+
return hash.slice(0, 10);
|
|
2000
|
+
}
|
|
2001
|
+
/**
|
|
2002
|
+
* Transform import JSON format to full schema file format
|
|
2003
|
+
* @param importData - The JSON import data
|
|
2004
|
+
* @param version - The version number for the schema (defaults to 1)
|
|
2005
|
+
* @returns The transformed schema file format
|
|
2006
|
+
*/
|
|
2007
|
+
const transformImportToSchemaFile = (importData, version = 1) => {
|
|
2008
|
+
const now = new Date().toISOString();
|
|
2009
|
+
const schemaId = generateId();
|
|
2010
|
+
// Generate IDs for models and properties
|
|
2011
|
+
const modelsWithIds = {};
|
|
2012
|
+
for (const [modelName, modelDef] of Object.entries(importData.models)) {
|
|
2013
|
+
const modelId = generateId();
|
|
2014
|
+
const propertiesWithIds = {};
|
|
2015
|
+
for (const [propertyName, propertyDef] of Object.entries(modelDef.properties)) {
|
|
2016
|
+
const propertyId = generateId();
|
|
2017
|
+
const prop = propertyDef;
|
|
2018
|
+
const isList = (prop.type ?? prop.dataType) === 'List';
|
|
2019
|
+
let normalized = { ...prop, id: propertyId };
|
|
2020
|
+
// Normalize List: convert legacy items to refValueType/ref (case-insensitive)
|
|
2021
|
+
const existingRefValueType = getRefValueType(prop);
|
|
2022
|
+
if (isList && existingRefValueType) {
|
|
2023
|
+
normalized = { ...normalized, refValueType: normalizeDataType(existingRefValueType) };
|
|
2024
|
+
const items = prop.items;
|
|
2025
|
+
if (items?.model && !normalized.ref)
|
|
2026
|
+
normalized.ref = items.model;
|
|
2027
|
+
}
|
|
2028
|
+
propertiesWithIds[propertyName] = normalized;
|
|
2029
|
+
}
|
|
2030
|
+
modelsWithIds[modelName] = {
|
|
2031
|
+
...modelDef,
|
|
2032
|
+
id: modelId,
|
|
2033
|
+
properties: propertiesWithIds,
|
|
2034
|
+
};
|
|
2035
|
+
}
|
|
2036
|
+
return {
|
|
2037
|
+
$schema: 'https://seedprotocol.org/schemas/data-model/v1',
|
|
2038
|
+
version,
|
|
2039
|
+
id: schemaId,
|
|
2040
|
+
metadata: {
|
|
2041
|
+
name: importData.name,
|
|
2042
|
+
createdAt: now,
|
|
2043
|
+
updatedAt: now,
|
|
2044
|
+
},
|
|
2045
|
+
models: modelsWithIds,
|
|
2046
|
+
enums: {},
|
|
2047
|
+
migrations: [
|
|
2048
|
+
{
|
|
2049
|
+
version,
|
|
2050
|
+
timestamp: now,
|
|
2051
|
+
description: 'Initial schema',
|
|
2052
|
+
changes: [],
|
|
2053
|
+
},
|
|
2054
|
+
],
|
|
2055
|
+
};
|
|
2056
|
+
};
|
|
2057
|
+
/**
|
|
2058
|
+
* Parse JSON import content (string) into JsonImportSchema format
|
|
2059
|
+
* Supports two formats:
|
|
2060
|
+
* 1. Minimal format: { name: string, models: {...} }
|
|
2061
|
+
* 2. Complete schema format: { $schema: string, metadata: { name: string }, models: {...}, ... }
|
|
2062
|
+
* @param content - The JSON content as a string
|
|
2063
|
+
* @returns The parsed JSON import data normalized to JsonImportSchema format
|
|
2064
|
+
* @throws Error if content cannot be parsed
|
|
2065
|
+
*/
|
|
2066
|
+
const parseJsonImportContent = (content) => {
|
|
2067
|
+
try {
|
|
2068
|
+
const data = JSON.parse(content);
|
|
2069
|
+
// Determine the format and extract name
|
|
2070
|
+
let schemaName;
|
|
2071
|
+
// Check if it's a complete schema format (has $schema field)
|
|
2072
|
+
if (data.$schema) {
|
|
2073
|
+
// Complete schema format: name is in metadata.name
|
|
2074
|
+
schemaName = data.metadata?.name;
|
|
2075
|
+
}
|
|
2076
|
+
else {
|
|
2077
|
+
// Minimal format: name is at top level
|
|
2078
|
+
schemaName = data.name;
|
|
2079
|
+
}
|
|
2080
|
+
if (!schemaName) {
|
|
2081
|
+
throw new Error('Schema name is required (either at top level or in metadata.name)');
|
|
2082
|
+
}
|
|
2083
|
+
// Return normalized JsonImportSchema format
|
|
2084
|
+
const models = normalizeSchemaModels(data.models);
|
|
2085
|
+
return {
|
|
2086
|
+
name: schemaName,
|
|
2087
|
+
models,
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
catch (error) {
|
|
2091
|
+
if (error instanceof SyntaxError) {
|
|
2092
|
+
throw new Error(`Invalid JSON content: ${error.message}`);
|
|
2093
|
+
}
|
|
2094
|
+
throw new Error(`Failed to parse JSON import content: ${error instanceof Error ? error.message : String(error)}`);
|
|
2095
|
+
}
|
|
2096
|
+
};
|
|
2097
|
+
/**
|
|
2098
|
+
* Read and parse a JSON import file
|
|
2099
|
+
* Supports two formats:
|
|
2100
|
+
* 1. Minimal format: { name: string, models: {...} }
|
|
2101
|
+
* 2. Complete schema format: { $schema: string, metadata: { name: string }, models: {...}, ... }
|
|
2102
|
+
* @param filePath - Path to the JSON import file
|
|
2103
|
+
* @returns The parsed JSON import data normalized to JsonImportSchema format
|
|
2104
|
+
* @throws Error if file cannot be read or parsed
|
|
2105
|
+
*/
|
|
2106
|
+
const readJsonImportFile = async (filePath) => {
|
|
2107
|
+
try {
|
|
2108
|
+
const workingDir = BaseFileManager.getWorkingDir();
|
|
2109
|
+
let internalFilePath = filePath;
|
|
2110
|
+
if (!filePath.startsWith(workingDir)) {
|
|
2111
|
+
internalFilePath = `${workingDir}/${filePath}`;
|
|
2112
|
+
}
|
|
2113
|
+
const content = await BaseFileManager.readFileAsString(internalFilePath);
|
|
2114
|
+
return parseJsonImportContent(content);
|
|
2115
|
+
}
|
|
2116
|
+
catch (error) {
|
|
2117
|
+
if (error instanceof SyntaxError) {
|
|
2118
|
+
throw new Error(`Invalid JSON in file ${filePath}: ${error.message}`);
|
|
2119
|
+
}
|
|
2120
|
+
throw new Error(`Failed to read JSON import file ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2121
|
+
}
|
|
2122
|
+
};
|
|
2123
|
+
/**
|
|
2124
|
+
* Sanitize a schema name to be filesystem-safe
|
|
2125
|
+
* Replaces all special characters (except alphanumeric, hyphens, underscores) with underscores
|
|
2126
|
+
* Converts spaces to underscores
|
|
2127
|
+
* Removes leading/trailing underscores
|
|
2128
|
+
*
|
|
2129
|
+
* @param name - Schema name to sanitize
|
|
2130
|
+
* @returns Sanitized name safe for use in filenames
|
|
2131
|
+
*/
|
|
2132
|
+
const sanitizeSchemaName = (name) => {
|
|
2133
|
+
return name
|
|
2134
|
+
.replace(/[^a-zA-Z0-9\s_-]/g, '_') // Replace special chars (except spaces, hyphens, underscores) with underscore
|
|
2135
|
+
.replace(/\s+/g, '_') // Convert spaces to underscores
|
|
2136
|
+
.replace(/^_+|_+$/g, '') // Remove leading/trailing underscores
|
|
2137
|
+
.replace(/_+/g, '_'); // Collapse multiple underscores to single
|
|
2138
|
+
};
|
|
2139
|
+
/**
|
|
2140
|
+
* Get the full file path for a schema file
|
|
2141
|
+
* Format: {schemaFileId}_{schemaName}_v{version}.json
|
|
2142
|
+
*
|
|
2143
|
+
* The ID-first format ensures all files for a schema group together when sorted alphabetically.
|
|
2144
|
+
*
|
|
2145
|
+
* @param workingDir - Working directory path
|
|
2146
|
+
* @param name - Schema name
|
|
2147
|
+
* @param version - Schema version
|
|
2148
|
+
* @param schemaFileId - Schema file ID (required)
|
|
2149
|
+
*/
|
|
2150
|
+
const getSchemaFilePath = (workingDir, name, version, schemaFileId) => {
|
|
2151
|
+
const path = BaseFileManager.getPathModule();
|
|
2152
|
+
const sanitizedName = sanitizeSchemaName(name);
|
|
2153
|
+
const filename = `${schemaFileId}_${sanitizedName}_v${version}.json`;
|
|
2154
|
+
return path.join(workingDir, filename);
|
|
2155
|
+
};
|
|
2156
|
+
async function importJsonSchema(importFilePathOrContents, version = 1) {
|
|
2157
|
+
// Determine if we have a file path or file contents
|
|
2158
|
+
let importData;
|
|
2159
|
+
let importFilePath;
|
|
2160
|
+
let originalSchemaFile;
|
|
2161
|
+
if (typeof importFilePathOrContents === 'string') {
|
|
2162
|
+
// It's a file path
|
|
2163
|
+
importFilePath = importFilePathOrContents;
|
|
2164
|
+
importData = await readJsonImportFile(importFilePath);
|
|
2165
|
+
}
|
|
2166
|
+
else {
|
|
2167
|
+
// It's file contents - check if it's already a complete schema format
|
|
2168
|
+
try {
|
|
2169
|
+
const parsed = JSON.parse(importFilePathOrContents.contents);
|
|
2170
|
+
if (parsed.$schema && parsed.metadata) {
|
|
2171
|
+
// It's already a complete schema format - preserve it (id may be missing, will be generated)
|
|
2172
|
+
originalSchemaFile = parsed;
|
|
2173
|
+
logger$2(`Input is complete schema format (${originalSchemaFile.id ? `preserving ID: ${originalSchemaFile.id}` : 'id will be generated'})`);
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
catch {
|
|
2177
|
+
// Not valid JSON or not a complete schema, parse as import format
|
|
2178
|
+
}
|
|
2179
|
+
if (!originalSchemaFile) {
|
|
2180
|
+
importData = parseJsonImportContent(importFilePathOrContents.contents);
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
// Use original schema file if available, otherwise transform from import format
|
|
2184
|
+
let schemaFile;
|
|
2185
|
+
if (originalSchemaFile) {
|
|
2186
|
+
schemaFile = originalSchemaFile;
|
|
2187
|
+
// Normalize models if provided as array (some tools output array format)
|
|
2188
|
+
if (schemaFile.models) {
|
|
2189
|
+
schemaFile.models = normalizeSchemaModels(schemaFile.models);
|
|
2190
|
+
}
|
|
2191
|
+
// Use provided version if different, otherwise use version from schema
|
|
2192
|
+
if (version !== undefined && version !== schemaFile.version) {
|
|
2193
|
+
schemaFile = { ...schemaFile, version };
|
|
2194
|
+
}
|
|
2195
|
+
// Check DB for existing schema by name+version (same as minimal path)
|
|
2196
|
+
const db = BaseDb.getAppDb();
|
|
2197
|
+
if (db) {
|
|
2198
|
+
const _mod_1 = await import('./BasePathResolver-DRT09oVv.js');
|
|
2199
|
+
const _ns_1 = _mod_1.Q;
|
|
2200
|
+
const schemaSchemaMod = _ns_1;
|
|
2201
|
+
const { schemas: schemasTable } = schemaSchemaMod;
|
|
2202
|
+
const drizzleMod = await import('drizzle-orm');
|
|
2203
|
+
const { eq, and, desc } = drizzleMod;
|
|
2204
|
+
const existingByName = await db
|
|
2205
|
+
.select()
|
|
2206
|
+
.from(schemasTable)
|
|
2207
|
+
.where(and(eq(schemasTable.name, schemaFile.metadata.name), eq(schemasTable.version, schemaFile.version ?? version)))
|
|
2208
|
+
.orderBy(desc(schemasTable.isDraft))
|
|
2209
|
+
.limit(1);
|
|
2210
|
+
if (existingByName.length > 0 && existingByName[0].schemaData) {
|
|
2211
|
+
const existing = existingByName[0];
|
|
2212
|
+
logger$2(`Idempotency: found existing schema by name+version: ${schemaFile.metadata.name} v${schemaFile.version ?? version}`);
|
|
2213
|
+
const existingSchema = JSON.parse(existing.schemaData);
|
|
2214
|
+
const contentMatches = compareSchemaContent(schemaFile, existingSchema);
|
|
2215
|
+
logger$2(`Idempotency: compareSchemaContent=${contentMatches} for ${schemaFile.metadata.name}`);
|
|
2216
|
+
if (contentMatches) {
|
|
2217
|
+
if (existing.schemaFileId) {
|
|
2218
|
+
const workingDir = BaseFileManager.getWorkingDir();
|
|
2219
|
+
const existingFilePath = getSchemaFilePath(workingDir, schemaFile.metadata.name, schemaFile.version ?? version, existing.schemaFileId);
|
|
2220
|
+
if (await BaseFileManager.pathExists(existingFilePath)) {
|
|
2221
|
+
logger$2(`Schema ${schemaFile.metadata.name} v${schemaFile.version} already exists with same content, loading existing file`);
|
|
2222
|
+
return await loadSchemaFromFile(existingFilePath);
|
|
2223
|
+
}
|
|
2224
|
+
await BaseFileManager.createDirIfNotExists(workingDir);
|
|
2225
|
+
await BaseFileManager.saveFile(existingFilePath, existing.schemaData);
|
|
2226
|
+
await BaseFileManager.waitForFileWithContent(existingFilePath, 100, 5000);
|
|
2227
|
+
return await loadSchemaFromFile(existingFilePath);
|
|
2228
|
+
}
|
|
2229
|
+
logger$2(`Schema ${schemaFile.metadata.name} v${schemaFile.version} already exists with same content (no schemaFileId), using DB`);
|
|
2230
|
+
return '';
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
// Add metadata defaults when missing (prevents NaN in addSchemaToDb)
|
|
2235
|
+
const now = new Date().toISOString();
|
|
2236
|
+
if (!schemaFile.metadata.createdAt)
|
|
2237
|
+
schemaFile.metadata.createdAt = now;
|
|
2238
|
+
if (!schemaFile.metadata.updatedAt)
|
|
2239
|
+
schemaFile.metadata.updatedAt = now;
|
|
2240
|
+
// Generate missing IDs for complete schema files.
|
|
2241
|
+
// Use deterministic id from content hash to prevent duplicate schema files when
|
|
2242
|
+
// processSchemaFiles runs multiple times (React StrictMode, app restarts).
|
|
2243
|
+
if (!schemaFile.id) {
|
|
2244
|
+
schemaFile.id = await getDeterministicSchemaFileId(schemaFile);
|
|
2245
|
+
logger$2(`Generated deterministic schema ID for ${schemaFile.metadata.name}:`, schemaFile.id);
|
|
2246
|
+
}
|
|
2247
|
+
// Before generating IDs, check if a file already exists with this schemaFileId
|
|
2248
|
+
// This handles the case where the schema was already loaded via loadSchemaFromFile
|
|
2249
|
+
const workingDir = BaseFileManager.getWorkingDir();
|
|
2250
|
+
BaseFileManager.getPathModule();
|
|
2251
|
+
const existingFilePath = getSchemaFilePath(workingDir, schemaFile.metadata.name, schemaFile.version, schemaFile.id);
|
|
2252
|
+
const existingFileExists = await BaseFileManager.pathExists(existingFilePath);
|
|
2253
|
+
if (existingFileExists) {
|
|
2254
|
+
// File already exists with this schemaFileId - just load it instead of creating a new one
|
|
2255
|
+
logger$2(`Schema file already exists with schemaFileId ${schemaFile.id}, loading it instead of creating new file`);
|
|
2256
|
+
return await loadSchemaFromFile(existingFilePath);
|
|
2257
|
+
}
|
|
2258
|
+
// Generate missing model and property IDs.
|
|
2259
|
+
// Use deterministic IDs (from schema content hash) so addModelsToDb finds existing
|
|
2260
|
+
// records when processSchemaFiles runs multiple times with different DB/file contexts.
|
|
2261
|
+
// Hash the schema content once: rehashing it per id is quadratic in schema size.
|
|
2262
|
+
const getDeterministicId = getDeterministicIdsWithPrefix(getSchemaContentForComparison(schemaFile));
|
|
2263
|
+
for (const [modelName, model] of Object.entries(schemaFile.models || {})) {
|
|
2264
|
+
if (!model.id) {
|
|
2265
|
+
model.id = getDeterministicId(modelName);
|
|
2266
|
+
logger$2(`Generated deterministic model ID for ${modelName}:`, model.id);
|
|
2267
|
+
}
|
|
2268
|
+
for (const [propName, prop] of Object.entries(model.properties || {})) {
|
|
2269
|
+
if (!prop.id) {
|
|
2270
|
+
prop.id = getDeterministicId(modelName + propName);
|
|
2271
|
+
logger$2(`Generated deterministic property ID for ${modelName}.${propName}:`, prop.id);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
else {
|
|
2277
|
+
if (!importData) {
|
|
2278
|
+
throw new Error('Failed to parse import data: neither complete schema nor import format could be determined');
|
|
2279
|
+
}
|
|
2280
|
+
// For minimal imports: check DB for existing schema before creating new file
|
|
2281
|
+
const db = BaseDb.getAppDb();
|
|
2282
|
+
if (db) {
|
|
2283
|
+
const _mod_2 = await import('./BasePathResolver-DRT09oVv.js');
|
|
2284
|
+
const _ns_2 = _mod_2.Q;
|
|
2285
|
+
const schemaSchemaMod = _ns_2;
|
|
2286
|
+
const { schemas: schemasTable } = schemaSchemaMod;
|
|
2287
|
+
const drizzleMod = await import('drizzle-orm');
|
|
2288
|
+
const { eq, and, desc } = drizzleMod;
|
|
2289
|
+
const existingByName = await db
|
|
2290
|
+
.select()
|
|
2291
|
+
.from(schemasTable)
|
|
2292
|
+
.where(and(eq(schemasTable.name, importData.name), eq(schemasTable.version, version)))
|
|
2293
|
+
.orderBy(desc(schemasTable.isDraft))
|
|
2294
|
+
.limit(1);
|
|
2295
|
+
if (existingByName.length > 0) {
|
|
2296
|
+
const existing = existingByName[0];
|
|
2297
|
+
const schemaFileForCompare = transformImportToSchemaFile(importData, version);
|
|
2298
|
+
if (existing.schemaData) {
|
|
2299
|
+
try {
|
|
2300
|
+
const existingSchema = JSON.parse(existing.schemaData);
|
|
2301
|
+
if (compareSchemaContent(schemaFileForCompare, existingSchema)) {
|
|
2302
|
+
// Same content - use existing schema
|
|
2303
|
+
if (existing.schemaFileId) {
|
|
2304
|
+
const workingDir = BaseFileManager.getWorkingDir();
|
|
2305
|
+
const existingFilePath = getSchemaFilePath(workingDir, importData.name, version, existing.schemaFileId);
|
|
2306
|
+
if (await BaseFileManager.pathExists(existingFilePath)) {
|
|
2307
|
+
logger$2(`Schema ${importData.name} v${version} already exists with same content, loading existing file`);
|
|
2308
|
+
return await loadSchemaFromFile(existingFilePath);
|
|
2309
|
+
}
|
|
2310
|
+
// File missing but schemaData exists - restore file from schemaData
|
|
2311
|
+
await BaseFileManager.createDirIfNotExists(workingDir);
|
|
2312
|
+
await BaseFileManager.saveFile(existingFilePath, existing.schemaData);
|
|
2313
|
+
await BaseFileManager.waitForFileWithContent(existingFilePath, 100, 5000);
|
|
2314
|
+
logger$2(`Restored schema file from schemaData for ${importData.name} v${version}`);
|
|
2315
|
+
return await loadSchemaFromFile(existingFilePath);
|
|
2316
|
+
}
|
|
2317
|
+
// No schemaFileId - restore file from schemaData using id from parsed schema
|
|
2318
|
+
const schemaIdForPath = existingSchema.id || existing.schemaFileId;
|
|
2319
|
+
if (schemaIdForPath) {
|
|
2320
|
+
const workingDir = BaseFileManager.getWorkingDir();
|
|
2321
|
+
const existingFilePath = getSchemaFilePath(workingDir, importData.name, version, schemaIdForPath);
|
|
2322
|
+
await BaseFileManager.createDirIfNotExists(workingDir);
|
|
2323
|
+
await BaseFileManager.saveFile(existingFilePath, existing.schemaData);
|
|
2324
|
+
await BaseFileManager.waitForFileWithContent(existingFilePath, 100, 5000);
|
|
2325
|
+
logger$2(`Restored schema file from schemaData for ${importData.name} v${version}`);
|
|
2326
|
+
return await loadSchemaFromFile(existingFilePath);
|
|
2327
|
+
}
|
|
2328
|
+
logger$2(`Schema ${importData.name} v${version} already exists with same content (no id), using DB`);
|
|
2329
|
+
return '';
|
|
2330
|
+
}
|
|
2331
|
+
// Content differs - create new version
|
|
2332
|
+
const latestVersionRows = await db
|
|
2333
|
+
.select({ version: schemasTable.version })
|
|
2334
|
+
.from(schemasTable)
|
|
2335
|
+
.where(eq(schemasTable.name, importData.name))
|
|
2336
|
+
.orderBy(desc(schemasTable.version))
|
|
2337
|
+
.limit(1);
|
|
2338
|
+
const newVersion = latestVersionRows.length > 0 ? latestVersionRows[0].version + 1 : version;
|
|
2339
|
+
logger$2(`Schema ${importData.name} content changed, creating new version v${newVersion}`);
|
|
2340
|
+
schemaFile = transformImportToSchemaFile(importData, newVersion);
|
|
2341
|
+
version = newVersion;
|
|
2342
|
+
}
|
|
2343
|
+
catch {
|
|
2344
|
+
// Parse error - proceed with normal import
|
|
2345
|
+
schemaFile = schemaFileForCompare;
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
else {
|
|
2349
|
+
schemaFile = schemaFileForCompare;
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
else {
|
|
2353
|
+
schemaFile = transformImportToSchemaFile(importData, version);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
else {
|
|
2357
|
+
schemaFile = transformImportToSchemaFile(importData, version);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
// Check if this is an internal SDK schema (should not create files in app directory)
|
|
2361
|
+
const isInternal = isInternalSchema(schemaFile.metadata.name, schemaFile.id);
|
|
2362
|
+
if (isInternal) {
|
|
2363
|
+
logger$2(`Skipping file creation for internal schema: ${schemaFile.metadata.name}`);
|
|
2364
|
+
// For internal schemas, just load to database and store, don't create file
|
|
2365
|
+
// Extract schema name and version
|
|
2366
|
+
const schemaName = schemaFile.metadata?.name;
|
|
2367
|
+
const version = schemaFile.version;
|
|
2368
|
+
if (!schemaName) {
|
|
2369
|
+
throw new Error('Schema name is required in metadata.name');
|
|
2370
|
+
}
|
|
2371
|
+
// Convert to JsonImportSchema format for processing
|
|
2372
|
+
const importDataForInternal = {
|
|
2373
|
+
name: schemaName,
|
|
2374
|
+
models: Object.fromEntries(Object.entries(schemaFile.models || {}).map(([modelName, model]) => [
|
|
2375
|
+
modelName,
|
|
2376
|
+
{
|
|
2377
|
+
...model,
|
|
2378
|
+
// Remove id field for import format
|
|
2379
|
+
id: undefined,
|
|
2380
|
+
properties: Object.fromEntries(Object.entries(model.properties || {}).map(([propName, prop]) => {
|
|
2381
|
+
const schemaProp = prop;
|
|
2382
|
+
const jsonProp = {
|
|
2383
|
+
type: schemaProp.dataType || schemaProp.type,
|
|
2384
|
+
// Remove id field for import format
|
|
2385
|
+
id: undefined,
|
|
2386
|
+
};
|
|
2387
|
+
// Handle Relation type - convert ref to model (not for List)
|
|
2388
|
+
const rawDataType = (schemaProp.dataType ?? schemaProp.type ?? '').toString().toLowerCase();
|
|
2389
|
+
if (rawDataType !== 'list' && (schemaProp.ref || schemaProp.refModelName)) {
|
|
2390
|
+
jsonProp.model = schemaProp.refModelName || schemaProp.ref;
|
|
2391
|
+
}
|
|
2392
|
+
// Handle List type (support both refValueType and legacy items, case-insensitive dataType)
|
|
2393
|
+
if (rawDataType === 'list') {
|
|
2394
|
+
let refValueType = getRefValueType(schemaProp);
|
|
2395
|
+
// When ref is present but refValueType is not, derive refValueType as "Relation" (list of Relations)
|
|
2396
|
+
if (!refValueType && (schemaProp.ref ?? schemaProp.refModelName ?? schemaProp.items?.model)) {
|
|
2397
|
+
refValueType = 'Relation';
|
|
2398
|
+
}
|
|
2399
|
+
if (refValueType) {
|
|
2400
|
+
jsonProp.refValueType = refValueType;
|
|
2401
|
+
const ref = schemaProp.ref ?? schemaProp.refModelName ?? schemaProp.items?.model;
|
|
2402
|
+
if (ref)
|
|
2403
|
+
jsonProp.ref = ref;
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
// Copy other properties (storage, etc.)
|
|
2407
|
+
Object.keys(schemaProp).forEach(key => {
|
|
2408
|
+
if (key !== 'id' && key !== 'dataType' && key !== 'type' && key !== 'ref' && key !== 'refModelName' && key !== 'refValueType') {
|
|
2409
|
+
jsonProp[key] = schemaProp[key];
|
|
2410
|
+
}
|
|
2411
|
+
});
|
|
2412
|
+
return [propName, jsonProp];
|
|
2413
|
+
})),
|
|
2414
|
+
},
|
|
2415
|
+
])),
|
|
2416
|
+
};
|
|
2417
|
+
// Use fixed internal schema ID if missing (never generate for internal)
|
|
2418
|
+
if (!schemaFile.id) {
|
|
2419
|
+
schemaFile.id = INTERNAL_SCHEMA_IDS[0];
|
|
2420
|
+
logger$2('Using fixed internal schema ID:', schemaFile.id);
|
|
2421
|
+
}
|
|
2422
|
+
// Extract schemaFileIds from JSON file and generate missing ones BEFORE creating models
|
|
2423
|
+
// This ensures Model instances are created with correct IDs
|
|
2424
|
+
const modelFileIds = new Map();
|
|
2425
|
+
const propertyFileIds = new Map();
|
|
2426
|
+
for (const [modelName, model] of Object.entries(schemaFile.models || {})) {
|
|
2427
|
+
// Generate model ID if missing
|
|
2428
|
+
if (!model.id) {
|
|
2429
|
+
model.id = generateId();
|
|
2430
|
+
logger$2(`Generated model ID for ${modelName}:`, model.id);
|
|
2431
|
+
// Update the schemaFile object so it's included in schemaData
|
|
2432
|
+
schemaFile.models[modelName].id = model.id;
|
|
2433
|
+
}
|
|
2434
|
+
modelFileIds.set(modelName, model.id);
|
|
2435
|
+
const propIds = new Map();
|
|
2436
|
+
for (const [propName, prop] of Object.entries(model.properties || {})) {
|
|
2437
|
+
// Generate property ID if missing
|
|
2438
|
+
if (!prop.id) {
|
|
2439
|
+
prop.id = generateId();
|
|
2440
|
+
logger$2(`Generated property ID for ${modelName}.${propName}:`, prop.id);
|
|
2441
|
+
// Update the schemaFile object so it's included in schemaData
|
|
2442
|
+
schemaFile.models[modelName].properties[propName].id = prop.id;
|
|
2443
|
+
}
|
|
2444
|
+
propIds.set(propName, prop.id);
|
|
2445
|
+
}
|
|
2446
|
+
if (propIds.size > 0) {
|
|
2447
|
+
propertyFileIds.set(modelName, propIds);
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
// Convert JSON models to Model classes, passing modelFileIds and propertyFileIds so Model instances use correct IDs
|
|
2451
|
+
const modelDefinitions = await createModelsFromJson(importDataForInternal, modelFileIds, propertyFileIds);
|
|
2452
|
+
logger$2('loadSchemaFromFile (internal) - modelDefinitions length:', Object.keys(modelDefinitions).length);
|
|
2453
|
+
// Convert schema file metadata to schema input for database
|
|
2454
|
+
const schemaInput = {
|
|
2455
|
+
name: schemaName,
|
|
2456
|
+
version,
|
|
2457
|
+
schemaFileId: schemaFile.id || null,
|
|
2458
|
+
createdAt: new Date(schemaFile.metadata.createdAt).getTime(),
|
|
2459
|
+
updatedAt: new Date(schemaFile.metadata.updatedAt).getTime(),
|
|
2460
|
+
};
|
|
2461
|
+
// Use dynamic import to break circular dependency
|
|
2462
|
+
const _mod_3 = await import('./db-Lw0KVZmc.js');
|
|
2463
|
+
const _ns_3 = _mod_3.Z;
|
|
2464
|
+
const dbMod = _ns_3;
|
|
2465
|
+
const { addSchemaToDb, addModelsToDb } = dbMod;
|
|
2466
|
+
// Try to add schema and models to database if database is available
|
|
2467
|
+
try {
|
|
2468
|
+
const db = BaseDb.getAppDb();
|
|
2469
|
+
if (db) {
|
|
2470
|
+
// Store full schema data in database as fallback when file is not available
|
|
2471
|
+
// schemaFile already has all IDs (from JSON file or generated), so we can use it directly
|
|
2472
|
+
const schemaData = JSON.stringify(schemaFile, null, 2);
|
|
2473
|
+
// Add schema to database (creates or returns existing) with schemaFileId and schemaData
|
|
2474
|
+
const schemaRecord = await addSchemaToDb(schemaInput, schemaFile.id, schemaData, false);
|
|
2475
|
+
// Add models to database and link them to the schema with schemaFileIds (only if there are models)
|
|
2476
|
+
if (Object.keys(modelDefinitions).length > 0) {
|
|
2477
|
+
await addModelsToDb(modelDefinitions, schemaRecord, undefined, {
|
|
2478
|
+
schemaFileId: schemaFile.id,
|
|
2479
|
+
modelFileIds,
|
|
2480
|
+
propertyFileIds,
|
|
2481
|
+
});
|
|
2482
|
+
// Verify expected models are linked (single attempt - addModelsToDb already confirmed)
|
|
2483
|
+
const expectedModelNames = Object.keys(schemaFile.models || {});
|
|
2484
|
+
const _mod_4 = await import('./BasePathResolver-DRT09oVv.js');
|
|
2485
|
+
const _ns_4 = _mod_4.R;
|
|
2486
|
+
const modelSchemaSchemaMod = _ns_4;
|
|
2487
|
+
const { modelSchemas } = modelSchemaSchemaMod;
|
|
2488
|
+
const _mod_5 = await import('./BasePathResolver-DRT09oVv.js');
|
|
2489
|
+
const _ns_5 = _mod_5.N;
|
|
2490
|
+
const modelSchemaMod = _ns_5;
|
|
2491
|
+
const { models: modelsTable } = modelSchemaMod;
|
|
2492
|
+
const drizzleMod = await import('drizzle-orm');
|
|
2493
|
+
const { eq, and, isNull } = drizzleMod;
|
|
2494
|
+
const modelLinks = await db
|
|
2495
|
+
.select({
|
|
2496
|
+
modelId: modelSchemas.modelId,
|
|
2497
|
+
modelName: modelsTable.name,
|
|
2498
|
+
})
|
|
2499
|
+
.from(modelSchemas)
|
|
2500
|
+
.innerJoin(modelsTable, eq(modelSchemas.modelId, modelsTable.id))
|
|
2501
|
+
.where(eq(modelSchemas.schemaId, schemaRecord.id));
|
|
2502
|
+
const linkedModelNames = modelLinks
|
|
2503
|
+
.map((link) => link.modelName)
|
|
2504
|
+
.filter((n) => n !== null);
|
|
2505
|
+
const missingModels = expectedModelNames.filter(name => !linkedModelNames.includes(name));
|
|
2506
|
+
const allModelsLinked = missingModels.length === 0;
|
|
2507
|
+
if (!allModelsLinked) {
|
|
2508
|
+
console.warn(`[importJsonSchema] WARNING: Not all expected models are linked. Expected: ${expectedModelNames.join(', ')}, Linked: ${linkedModelNames.join(', ')}, Missing: ${missingModels.join(', ') || 'none'}`);
|
|
2509
|
+
// Try to link missing models directly by finding them in the database
|
|
2510
|
+
if (missingModels.length > 0) {
|
|
2511
|
+
for (const missingModelName of missingModels) {
|
|
2512
|
+
try {
|
|
2513
|
+
const modelFileId = modelFileIds.get(missingModelName);
|
|
2514
|
+
if (modelFileId) {
|
|
2515
|
+
// Find model by schemaFileId
|
|
2516
|
+
const missingModel = await db
|
|
2517
|
+
.select()
|
|
2518
|
+
.from(modelsTable)
|
|
2519
|
+
.where(eq(modelsTable.schemaFileId, modelFileId))
|
|
2520
|
+
.limit(1);
|
|
2521
|
+
if (missingModel.length > 0 && missingModel[0].id) {
|
|
2522
|
+
// Check if join entry already exists
|
|
2523
|
+
const existingJoin = await db
|
|
2524
|
+
.select()
|
|
2525
|
+
.from(modelSchemas)
|
|
2526
|
+
.where(and(eq(modelSchemas.modelId, missingModel[0].id), eq(modelSchemas.schemaId, schemaRecord.id)))
|
|
2527
|
+
.limit(1);
|
|
2528
|
+
if (existingJoin.length === 0) {
|
|
2529
|
+
await linkModelToSchema(db, missingModel[0].id, schemaRecord.id);
|
|
2530
|
+
}
|
|
2531
|
+
else {
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
else {
|
|
2535
|
+
console.warn(`[importJsonSchema] Could not find model "${missingModelName}" with schemaFileId "${modelFileId}" in database`);
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
else {
|
|
2539
|
+
// Fallback: find by name, but only a stub no schema claims (model names are only
|
|
2540
|
+
// unique per schema; an unlinked row with a schemaFileId is a deleted schema's leftover)
|
|
2541
|
+
const missingModel = await db
|
|
2542
|
+
.select({ id: modelsTable.id })
|
|
2543
|
+
.from(modelsTable)
|
|
2544
|
+
.leftJoin(modelSchemas, eq(modelsTable.id, modelSchemas.modelId))
|
|
2545
|
+
.where(and(eq(modelsTable.name, missingModelName), isNull(modelSchemas.id), isNull(modelsTable.schemaFileId)))
|
|
2546
|
+
.limit(1);
|
|
2547
|
+
if (missingModel.length > 0 && missingModel[0].id) {
|
|
2548
|
+
const existingJoin = await db
|
|
2549
|
+
.select()
|
|
2550
|
+
.from(modelSchemas)
|
|
2551
|
+
.where(and(eq(modelSchemas.modelId, missingModel[0].id), eq(modelSchemas.schemaId, schemaRecord.id)))
|
|
2552
|
+
.limit(1);
|
|
2553
|
+
if (existingJoin.length === 0) {
|
|
2554
|
+
await linkModelToSchema(db, missingModel[0].id, schemaRecord.id);
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
else {
|
|
2558
|
+
console.warn(`[importJsonSchema] Could not find model "${missingModelName}" by name in database`);
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
catch (error) {
|
|
2563
|
+
console.error(`[importJsonSchema] Error linking missing model "${missingModelName}":`, error?.message || String(error));
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
// Skip expensive verifyPropertiesPersisted - addModelsToDb already persisted; verification was for browser quirks
|
|
2569
|
+
// Skip "After properties" block for internal schema: addSchemaToDb/addModelsToDb already stored
|
|
2570
|
+
// correct schemaData. The per-model ID sync (6 model + 6 property selects + schema update) is
|
|
2571
|
+
// redundant during init and causes slow OPFS writes. Ad-hoc loadSchemaFromFile uses the same
|
|
2572
|
+
// applySchemaToDb path but runs when DB is warm.
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
catch (dbError) {
|
|
2577
|
+
// Database not available - log warning but continue
|
|
2578
|
+
logger$2('Database not available, skipping database operations:', dbError instanceof Error ? dbError.message : String(dbError));
|
|
2579
|
+
}
|
|
2580
|
+
// Models are now Model instances, no registration needed
|
|
2581
|
+
// They should be created via Model.create() and are accessible via Model static methods
|
|
2582
|
+
for (const [modelName] of Object.entries(modelDefinitions)) {
|
|
2583
|
+
logger$2('loadSchemaFromFile (internal) - model available:', modelName);
|
|
2584
|
+
}
|
|
2585
|
+
// Schema is now saved to database
|
|
2586
|
+
// useSchemas hook will pick it up via database queries
|
|
2587
|
+
return ''; // Return empty string to indicate no file was created
|
|
2588
|
+
}
|
|
2589
|
+
const workingDir = BaseFileManager.getWorkingDir();
|
|
2590
|
+
const path = BaseFileManager.getPathModule();
|
|
2591
|
+
// Ensure schema ID is defined (should always be set by this point, but check for safety)
|
|
2592
|
+
if (!schemaFile.id) {
|
|
2593
|
+
schemaFile.id = generateId();
|
|
2594
|
+
logger$2('Generated schema ID before file path creation:', schemaFile.id);
|
|
2595
|
+
}
|
|
2596
|
+
// Get the target file path using ID-based naming (preferred)
|
|
2597
|
+
const filePath = getSchemaFilePath(workingDir, schemaFile.metadata.name, version, schemaFile.id);
|
|
2598
|
+
// Check if schema already exists
|
|
2599
|
+
const exists = await BaseFileManager.pathExists(filePath);
|
|
2600
|
+
if (exists) {
|
|
2601
|
+
// If we have a file path and the import file is the same as the target file,
|
|
2602
|
+
// and it's already a complete schema, just load it without writing
|
|
2603
|
+
if (importFilePath) {
|
|
2604
|
+
const normalizedImportPath = path.resolve(importFilePath);
|
|
2605
|
+
const normalizedTargetPath = path.resolve(filePath);
|
|
2606
|
+
if (normalizedImportPath === normalizedTargetPath) {
|
|
2607
|
+
// Check if the file is already a complete schema
|
|
2608
|
+
try {
|
|
2609
|
+
const content = await BaseFileManager.readFileAsString(importFilePath);
|
|
2610
|
+
const data = JSON.parse(content);
|
|
2611
|
+
if (data.$schema) {
|
|
2612
|
+
// File is already a complete schema, just load it
|
|
2613
|
+
logger$2(`Schema file ${filePath} already exists and is complete, loading it`);
|
|
2614
|
+
return await loadSchemaFromFile(filePath);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
catch (error) {
|
|
2618
|
+
// If we can't read the file, proceed with the error
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
// File exists - check if it matches the schema we're trying to import
|
|
2623
|
+
try {
|
|
2624
|
+
const existingContent = await BaseFileManager.readFileAsString(filePath);
|
|
2625
|
+
const existingSchema = JSON.parse(existingContent);
|
|
2626
|
+
// Verify it's a complete schema file
|
|
2627
|
+
if (existingSchema.$schema) {
|
|
2628
|
+
// Compare schemas structurally
|
|
2629
|
+
if (compareSchemasStructurally(schemaFile, existingSchema)) {
|
|
2630
|
+
// Schemas match - just load the existing file
|
|
2631
|
+
logger$2(`Schema ${schemaFile.metadata.name} v${version} already exists with matching content, loading it`);
|
|
2632
|
+
return await loadSchemaFromFile(filePath);
|
|
2633
|
+
}
|
|
2634
|
+
else {
|
|
2635
|
+
// Schemas don't match - throw error to prevent data loss
|
|
2636
|
+
throw new Error(`Schema ${schemaFile.metadata.name} v${version} already exists with different content. ` +
|
|
2637
|
+
`To update the schema, delete the existing file first or use a different version.`);
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
else {
|
|
2641
|
+
// File exists but is not a complete schema - this shouldn't happen with ID-based naming
|
|
2642
|
+
// but we'll treat it as a conflict
|
|
2643
|
+
throw new Error(`Schema ${schemaFile.metadata.name} v${version} already exists but file is not a complete schema`);
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
catch (error) {
|
|
2647
|
+
// If error is already our custom error, re-throw it
|
|
2648
|
+
if (error instanceof Error && error.message.includes('already exists')) {
|
|
2649
|
+
throw error;
|
|
2650
|
+
}
|
|
2651
|
+
// If we can't read/parse the existing file, throw a generic error
|
|
2652
|
+
throw new Error(`Schema ${schemaFile.metadata.name} v${version} already exists but could not be read: ${error instanceof Error ? error.message : String(error)}`);
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
// Ensure working directory exists
|
|
2656
|
+
await BaseFileManager.createDirIfNotExists(workingDir);
|
|
2657
|
+
// Write schema to file
|
|
2658
|
+
const content = JSON.stringify(schemaFile, null, 2);
|
|
2659
|
+
await BaseFileManager.saveFile(filePath, content);
|
|
2660
|
+
// Wait for the file to be available and readable before trying to read it
|
|
2661
|
+
// This is necessary in browser environments where file writes may not be immediately readable
|
|
2662
|
+
await BaseFileManager.waitForFileWithContent(filePath, 100, 5000);
|
|
2663
|
+
// Load the schema file to process models and add them to the store
|
|
2664
|
+
// This handles all model creation, database operations, and store updates
|
|
2665
|
+
await loadSchemaFromFile(filePath);
|
|
2666
|
+
// Schema is now saved to database
|
|
2667
|
+
// useSchemas hook will pick it up via database queries
|
|
2668
|
+
return filePath;
|
|
2669
|
+
}
|
|
2670
|
+
/**
|
|
2671
|
+
* Apply schema and models to the database. Shared by loadSchemaFromFile and syncSchemaFromSource.
|
|
2672
|
+
* @internal
|
|
2673
|
+
*/
|
|
2674
|
+
async function applySchemaToDb(schemaFile, modelDefinitions, modelFileIds, propertyFileIds) {
|
|
2675
|
+
const schemaName = schemaFile.metadata?.name;
|
|
2676
|
+
const version = schemaFile.version;
|
|
2677
|
+
if (!schemaName) {
|
|
2678
|
+
throw new Error('Schema name is required in metadata.name');
|
|
2679
|
+
}
|
|
2680
|
+
const schemaInput = {
|
|
2681
|
+
name: schemaName,
|
|
2682
|
+
version,
|
|
2683
|
+
schemaFileId: schemaFile.id || null,
|
|
2684
|
+
createdAt: new Date(schemaFile.metadata.createdAt).getTime(),
|
|
2685
|
+
updatedAt: new Date(schemaFile.metadata.updatedAt).getTime(),
|
|
2686
|
+
};
|
|
2687
|
+
const _mod_6 = await import('./db-Lw0KVZmc.js');
|
|
2688
|
+
const _ns_6 = _mod_6.Z;
|
|
2689
|
+
const dbMod = _ns_6;
|
|
2690
|
+
const { addSchemaToDb, addModelsToDb } = dbMod;
|
|
2691
|
+
const db = BaseDb.getAppDb();
|
|
2692
|
+
if (!db) {
|
|
2693
|
+
throw new Error('Database not found');
|
|
2694
|
+
}
|
|
2695
|
+
const schemaData = JSON.stringify(schemaFile, null, 2);
|
|
2696
|
+
const schemaRecord = await addSchemaToDb(schemaInput, schemaFile.id, schemaData, false);
|
|
2697
|
+
if (Object.keys(modelDefinitions).length > 0) {
|
|
2698
|
+
await addModelsToDb(modelDefinitions, schemaRecord, undefined, {
|
|
2699
|
+
schemaFileId: schemaFile.id,
|
|
2700
|
+
modelFileIds,
|
|
2701
|
+
propertyFileIds,
|
|
2702
|
+
});
|
|
2703
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
2704
|
+
}
|
|
2705
|
+
return { schemaRecord };
|
|
2706
|
+
}
|
|
2707
|
+
/**
|
|
2708
|
+
* Sync a canonical schema from a file path or inlined object to the database.
|
|
2709
|
+
* Uses add/update semantics: no "already exists with different content" errors, no ID-based file copy.
|
|
2710
|
+
* Called automatically at init when config.schema is provided.
|
|
2711
|
+
*
|
|
2712
|
+
* @param source - File path (string) or complete SchemaFileFormat object
|
|
2713
|
+
*/
|
|
2714
|
+
async function syncSchemaFromSource(source) {
|
|
2715
|
+
if (typeof source === 'string') {
|
|
2716
|
+
const path = BaseFileManager.getPathModule();
|
|
2717
|
+
const resolvedPath = path.isAbsolute(source)
|
|
2718
|
+
? source
|
|
2719
|
+
: isNode()
|
|
2720
|
+
? path.resolve(process.cwd(), source)
|
|
2721
|
+
: path.join(BaseFileManager.getWorkingDir(), source);
|
|
2722
|
+
const content = await BaseFileManager.readFileAsString(resolvedPath);
|
|
2723
|
+
const parsed = JSON.parse(content);
|
|
2724
|
+
const version = parsed?.version ?? 1;
|
|
2725
|
+
await importJsonSchema({ contents: content }, version);
|
|
2726
|
+
}
|
|
2727
|
+
else {
|
|
2728
|
+
const version = source.version ?? 1;
|
|
2729
|
+
await importJsonSchema({ contents: JSON.stringify(source) }, version);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
/**
|
|
2733
|
+
* Load an existing complete schema file and process its models
|
|
2734
|
+
* This is used to load models from already-processed schema files into the store
|
|
2735
|
+
* @param schemaFilePath - Path to the complete schema file (must have $schema field)
|
|
2736
|
+
* @returns The file path of the schema
|
|
2737
|
+
* @throws Error if file cannot be read, parsed, or is not a complete schema
|
|
2738
|
+
*/
|
|
2739
|
+
const loadSchemaFromFile = async (schemaFilePath) => {
|
|
2740
|
+
try {
|
|
2741
|
+
// Read and parse the schema file
|
|
2742
|
+
const content = await BaseFileManager.readFileAsString(schemaFilePath);
|
|
2743
|
+
const schemaFile = JSON.parse(content);
|
|
2744
|
+
// Normalize models if provided as array (some tools output array format)
|
|
2745
|
+
if (schemaFile.models) {
|
|
2746
|
+
schemaFile.models = normalizeSchemaModels(schemaFile.models);
|
|
2747
|
+
}
|
|
2748
|
+
// Verify it's a complete schema file
|
|
2749
|
+
if (!schemaFile.$schema) {
|
|
2750
|
+
throw new Error(`File ${schemaFilePath} is not a complete schema file (missing $schema field). Use importJsonSchema() for minimal format files.`);
|
|
2751
|
+
}
|
|
2752
|
+
// Extract schema name and version
|
|
2753
|
+
const schemaName = schemaFile.metadata?.name;
|
|
2754
|
+
const version = schemaFile.version;
|
|
2755
|
+
if (!schemaName) {
|
|
2756
|
+
throw new Error('Schema name is required in metadata.name');
|
|
2757
|
+
}
|
|
2758
|
+
// Convert to JsonImportSchema format for processing
|
|
2759
|
+
// Remove id fields for JsonImportSchema format (they're not part of the import format)
|
|
2760
|
+
const importData = {
|
|
2761
|
+
name: schemaName,
|
|
2762
|
+
models: Object.fromEntries(Object.entries(schemaFile.models || {}).map(([modelName, model]) => [
|
|
2763
|
+
modelName,
|
|
2764
|
+
{
|
|
2765
|
+
...model,
|
|
2766
|
+
// Remove id field for import format
|
|
2767
|
+
id: undefined,
|
|
2768
|
+
properties: Object.fromEntries(Object.entries(model.properties).map(([propName, prop]) => {
|
|
2769
|
+
const schemaProp = prop;
|
|
2770
|
+
const jsonProp = {
|
|
2771
|
+
type: schemaProp.dataType || schemaProp.type,
|
|
2772
|
+
// Remove id field for import format
|
|
2773
|
+
id: undefined,
|
|
2774
|
+
};
|
|
2775
|
+
// Handle Relation type - convert ref to model (not for List)
|
|
2776
|
+
const rawDataType = (schemaProp.dataType ?? schemaProp.type ?? '').toString().toLowerCase();
|
|
2777
|
+
if (rawDataType !== 'list' && (schemaProp.ref || schemaProp.refModelName)) {
|
|
2778
|
+
jsonProp.model = schemaProp.refModelName || schemaProp.ref;
|
|
2779
|
+
}
|
|
2780
|
+
// Handle List type (support both refValueType and legacy items, case-insensitive dataType)
|
|
2781
|
+
if (rawDataType === 'list') {
|
|
2782
|
+
let refValueType = getRefValueType(schemaProp);
|
|
2783
|
+
// When ref is present but refValueType is not, derive refValueType as "Relation" (list of Relations)
|
|
2784
|
+
if (!refValueType && (schemaProp.ref ?? schemaProp.refModelName ?? schemaProp.items?.model)) {
|
|
2785
|
+
refValueType = 'Relation';
|
|
2786
|
+
}
|
|
2787
|
+
if (refValueType) {
|
|
2788
|
+
jsonProp.refValueType = refValueType;
|
|
2789
|
+
const ref = schemaProp.ref ?? schemaProp.refModelName ?? schemaProp.items?.model;
|
|
2790
|
+
if (ref)
|
|
2791
|
+
jsonProp.ref = ref;
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
// Copy other properties (storage, etc.)
|
|
2795
|
+
Object.keys(schemaProp).forEach(key => {
|
|
2796
|
+
if (key !== 'id' && key !== 'dataType' && key !== 'type' && key !== 'ref' && key !== 'refModelName' && key !== 'refValueType') {
|
|
2797
|
+
jsonProp[key] = schemaProp[key];
|
|
2798
|
+
}
|
|
2799
|
+
});
|
|
2800
|
+
return [propName, jsonProp];
|
|
2801
|
+
})),
|
|
2802
|
+
},
|
|
2803
|
+
])),
|
|
2804
|
+
};
|
|
2805
|
+
// Generate schema ID if missing (before creating schemaInput)
|
|
2806
|
+
if (!schemaFile.id) {
|
|
2807
|
+
schemaFile.id = generateId();
|
|
2808
|
+
logger$2('Generated schema ID for schema file:', schemaFile.id);
|
|
2809
|
+
}
|
|
2810
|
+
// Extract schemaFileIds from JSON file and generate missing ones BEFORE creating models
|
|
2811
|
+
// This ensures Model instances are created with correct IDs
|
|
2812
|
+
const modelFileIds = new Map();
|
|
2813
|
+
const propertyFileIds = new Map();
|
|
2814
|
+
for (const [modelName, model] of Object.entries(schemaFile.models || {})) {
|
|
2815
|
+
// Generate model ID if missing
|
|
2816
|
+
if (!model.id) {
|
|
2817
|
+
model.id = generateId();
|
|
2818
|
+
logger$2(`Generated model ID for ${modelName}:`, model.id);
|
|
2819
|
+
// Update the schemaFile object so it's included in schemaData
|
|
2820
|
+
schemaFile.models[modelName].id = model.id;
|
|
2821
|
+
}
|
|
2822
|
+
modelFileIds.set(modelName, model.id);
|
|
2823
|
+
const propIds = new Map();
|
|
2824
|
+
for (const [propName, prop] of Object.entries(model.properties)) {
|
|
2825
|
+
// Generate property ID if missing
|
|
2826
|
+
if (!prop.id) {
|
|
2827
|
+
prop.id = generateId();
|
|
2828
|
+
logger$2(`Generated property ID for ${modelName}.${propName}:`, prop.id);
|
|
2829
|
+
// Update the schemaFile object so it's included in schemaData
|
|
2830
|
+
schemaFile.models[modelName].properties[propName].id = prop.id;
|
|
2831
|
+
}
|
|
2832
|
+
propIds.set(propName, prop.id);
|
|
2833
|
+
}
|
|
2834
|
+
if (propIds.size > 0) {
|
|
2835
|
+
propertyFileIds.set(modelName, propIds);
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
// Convert JSON models to Model classes, passing modelFileIds and propertyFileIds so Model instances use correct IDs
|
|
2839
|
+
const modelDefinitions = await createModelsFromJson(importData, modelFileIds, propertyFileIds);
|
|
2840
|
+
logger$2('loadSchemaFromFile - modelDefinitions length:', Object.keys(modelDefinitions).length);
|
|
2841
|
+
// Try to add schema and models to database if database is available
|
|
2842
|
+
try {
|
|
2843
|
+
const db = BaseDb.getAppDb();
|
|
2844
|
+
if (db) {
|
|
2845
|
+
const { schemaRecord } = await applySchemaToDb(schemaFile, modelDefinitions, modelFileIds, propertyFileIds);
|
|
2846
|
+
// Query the database to get model IDs that were just created
|
|
2847
|
+
const _mod_7 = await import('./BasePathResolver-DRT09oVv.js');
|
|
2848
|
+
const _ns_7 = _mod_7.R;
|
|
2849
|
+
const modelSchemaSchemaMod = _ns_7;
|
|
2850
|
+
const { modelSchemas } = modelSchemaSchemaMod;
|
|
2851
|
+
const _mod_8 = await import('./BasePathResolver-DRT09oVv.js');
|
|
2852
|
+
const _ns_8 = _mod_8.N;
|
|
2853
|
+
const modelSchemaMod = _ns_8;
|
|
2854
|
+
const { models: modelsTable } = modelSchemaMod;
|
|
2855
|
+
const drizzleMod = await import('drizzle-orm');
|
|
2856
|
+
const { eq } = drizzleMod;
|
|
2857
|
+
// Try to find models directly by schemaFileId first (more reliable than join table)
|
|
2858
|
+
const seedModelId = modelFileIds.get('Seed');
|
|
2859
|
+
let seedModel = [];
|
|
2860
|
+
if (seedModelId) {
|
|
2861
|
+
// Retry querying for the model until it's visible
|
|
2862
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
2863
|
+
seedModel = await db
|
|
2864
|
+
.select()
|
|
2865
|
+
.from(modelsTable)
|
|
2866
|
+
.where(eq(modelsTable.schemaFileId, seedModelId))
|
|
2867
|
+
.limit(1);
|
|
2868
|
+
if (seedModel.length > 0 && seedModel[0].id) {
|
|
2869
|
+
break;
|
|
2870
|
+
}
|
|
2871
|
+
if (attempt < 9) {
|
|
2872
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
if (seedModel.length > 0 && seedModel[0].id) {
|
|
2876
|
+
await verifyPropertiesPersisted(db, seedModel[0].id, 'Seed', 10, 100);
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
// Also verify via join table if schemaRecord.id is available (for completeness)
|
|
2880
|
+
if (schemaRecord.id) {
|
|
2881
|
+
// Retry querying join table until entries are visible
|
|
2882
|
+
let modelLinks = [];
|
|
2883
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
2884
|
+
modelLinks = await db
|
|
2885
|
+
.select({
|
|
2886
|
+
modelId: modelSchemas.modelId,
|
|
2887
|
+
modelName: modelsTable.name,
|
|
2888
|
+
})
|
|
2889
|
+
.from(modelSchemas)
|
|
2890
|
+
.innerJoin(modelsTable, eq(modelSchemas.modelId, modelsTable.id))
|
|
2891
|
+
.where(eq(modelSchemas.schemaId, schemaRecord.id));
|
|
2892
|
+
if (modelLinks.length > 0) {
|
|
2893
|
+
break;
|
|
2894
|
+
}
|
|
2895
|
+
if (attempt < 9) {
|
|
2896
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
logger$2(`Verifying properties: found ${modelLinks.length} model links for schema ${schemaName} (id: ${schemaRecord.id})`);
|
|
2900
|
+
// If we didn't verify via direct lookup, verify via join table
|
|
2901
|
+
if (!seedModelId || (seedModel.length === 0 || !seedModel[0].id)) {
|
|
2902
|
+
const seedModelLink = modelLinks.find((link) => link.modelName === 'Seed');
|
|
2903
|
+
if (seedModelLink && seedModelLink.modelId) {
|
|
2904
|
+
logger$2(`Verifying properties for Seed model via join table (modelId: ${seedModelLink.modelId})`);
|
|
2905
|
+
await verifyPropertiesPersisted(db, seedModelLink.modelId, 'Seed', 10, 100);
|
|
2906
|
+
}
|
|
2907
|
+
else if (modelLinks.length > 0 && modelLinks[0].modelId) {
|
|
2908
|
+
// Fallback to first model if Seed not found
|
|
2909
|
+
logger$2(`Verifying properties for ${modelLinks[0].modelName} model via join table (modelId: ${modelLinks[0].modelId})`);
|
|
2910
|
+
await verifyPropertiesPersisted(db, modelLinks[0].modelId, modelLinks[0].modelName || 'unknown', 10, 100);
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
// After properties are created (file path), ensure schemaFile has the correct IDs from database
|
|
2915
|
+
// Query the database to get the actual schemaFileId values that were used
|
|
2916
|
+
// This ensures schemaData matches what's actually in the database
|
|
2917
|
+
const _mod_9 = await import('./BasePathResolver-DRT09oVv.js');
|
|
2918
|
+
const _ns_9 = _mod_9.N;
|
|
2919
|
+
const modelSchemaMod2 = _ns_9;
|
|
2920
|
+
const { properties: propertiesTable } = modelSchemaMod2;
|
|
2921
|
+
let schemaFileUpdated = false;
|
|
2922
|
+
for (const [modelName, modelFileId] of modelFileIds.entries()) {
|
|
2923
|
+
// Get model record to find modelId
|
|
2924
|
+
const modelRecords = await db
|
|
2925
|
+
.select()
|
|
2926
|
+
.from(modelsTable)
|
|
2927
|
+
.where(eq(modelsTable.schemaFileId, modelFileId))
|
|
2928
|
+
.limit(1);
|
|
2929
|
+
if (modelRecords.length > 0) {
|
|
2930
|
+
const modelId = modelRecords[0].id;
|
|
2931
|
+
// Get all properties for this model from database
|
|
2932
|
+
const propertyRecords = await db
|
|
2933
|
+
.select()
|
|
2934
|
+
.from(propertiesTable)
|
|
2935
|
+
.where(eq(propertiesTable.modelId, modelId));
|
|
2936
|
+
// Update schemaFile with actual database IDs (only if they differ)
|
|
2937
|
+
for (const propRecord of propertyRecords) {
|
|
2938
|
+
if (propRecord.schemaFileId && schemaFile.models[modelName]?.properties[propRecord.name]) {
|
|
2939
|
+
const currentId = schemaFile.models[modelName].properties[propRecord.name].id;
|
|
2940
|
+
if (currentId !== propRecord.schemaFileId) {
|
|
2941
|
+
schemaFile.models[modelName].properties[propRecord.name].id = propRecord.schemaFileId;
|
|
2942
|
+
logger$2(`Updated schemaFile property ID for ${modelName}.${propRecord.name} from ${currentId} to ${propRecord.schemaFileId}`);
|
|
2943
|
+
schemaFileUpdated = true;
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
// Always update schemaData to ensure it matches the current schemaFile state
|
|
2950
|
+
// This is important even if nothing changed, to ensure consistency
|
|
2951
|
+
const updatedSchemaData = JSON.stringify(schemaFile, null, 2);
|
|
2952
|
+
// Update the schema record in the database with current schemaData
|
|
2953
|
+
const _mod_10 = await import('./BasePathResolver-DRT09oVv.js');
|
|
2954
|
+
const _ns_10 = _mod_10.Q;
|
|
2955
|
+
const schemaSchemaMod = _ns_10;
|
|
2956
|
+
const { schemas: schemasTable } = schemaSchemaMod;
|
|
2957
|
+
await db
|
|
2958
|
+
.update(schemasTable)
|
|
2959
|
+
.set({ schemaData: updatedSchemaData })
|
|
2960
|
+
.where(eq(schemasTable.id, schemaRecord.id));
|
|
2961
|
+
if (schemaFileUpdated) {
|
|
2962
|
+
logger$2(`Updated schemaData in database with actual property IDs for schema "${schemaName}"`);
|
|
2963
|
+
}
|
|
2964
|
+
else {
|
|
2965
|
+
logger$2(`Verified schemaData in database matches schemaFile for schema "${schemaName}"`);
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
catch (dbError) {
|
|
2970
|
+
// Database not available - log warning but continue
|
|
2971
|
+
logger$2('Database not available, skipping database operations:', dbError instanceof Error ? dbError.message : String(dbError));
|
|
2972
|
+
}
|
|
2973
|
+
// Models are now Model instances, no registration needed
|
|
2974
|
+
// They should be created via Model.create() and are accessible via Model static methods
|
|
2975
|
+
for (const [modelName] of Object.entries(modelDefinitions)) {
|
|
2976
|
+
logger$2('loadSchemaFromFile - model available:', modelName);
|
|
2977
|
+
}
|
|
2978
|
+
return schemaFilePath;
|
|
2979
|
+
}
|
|
2980
|
+
catch (error) {
|
|
2981
|
+
if (error instanceof SyntaxError) {
|
|
2982
|
+
throw new Error(`Invalid JSON in file ${schemaFilePath}: ${error.message}`);
|
|
2983
|
+
}
|
|
2984
|
+
throw new Error(`Failed to load schema from file ${schemaFilePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2985
|
+
}
|
|
2986
|
+
};
|
|
2987
|
+
/**
|
|
2988
|
+
* Create a Model class from a JSON model definition
|
|
2989
|
+
* @param modelName - The name of the model
|
|
2990
|
+
* @param modelDef - The JSON model definition
|
|
2991
|
+
* @param schemaName - The name of the schema this model belongs to
|
|
2992
|
+
* @returns A Model class that can be used with the SDK
|
|
2993
|
+
*/
|
|
2994
|
+
const createModelFromJson = async (modelName, modelDef, schemaName, modelFileId, // Optional modelFileId from JSON file
|
|
2995
|
+
propertyFileIds) => {
|
|
2996
|
+
const _mod_11 = await import('./index-Y-yYyRBf.js');
|
|
2997
|
+
const _ns_11 = _mod_11.f;
|
|
2998
|
+
const modelMod = _ns_11;
|
|
2999
|
+
const { Model } = modelMod;
|
|
3000
|
+
// Convert JSON properties to schema format
|
|
3001
|
+
const convertedProperties = {};
|
|
3002
|
+
if (modelDef.properties) {
|
|
3003
|
+
for (const [propName, propDef] of Object.entries(modelDef.properties)) {
|
|
3004
|
+
// JSON import format: { type, model, items, storage: { type, path, extension } }
|
|
3005
|
+
// Schema format: { dataType, ref, refValueType, storageType, localStorageDir, filenameSuffix }
|
|
3006
|
+
const jsonProp = propDef;
|
|
3007
|
+
const rawType = jsonProp.type ?? jsonProp.dataType;
|
|
3008
|
+
const schemaProp = {
|
|
3009
|
+
dataType: normalizeDataType(rawType),
|
|
3010
|
+
};
|
|
3011
|
+
// Include property ID if available from propertyFileIds map
|
|
3012
|
+
// This ensures the property uses the correct ID from the schema file
|
|
3013
|
+
const propertyFileId = propertyFileIds?.get(propName);
|
|
3014
|
+
if (propertyFileId) {
|
|
3015
|
+
schemaProp.id = propertyFileId;
|
|
3016
|
+
}
|
|
3017
|
+
// Handle Relation type
|
|
3018
|
+
if (jsonProp.model && (rawType || '').toLowerCase() !== 'list') {
|
|
3019
|
+
schemaProp.ref = jsonProp.model;
|
|
3020
|
+
schemaProp.refModelName = jsonProp.model;
|
|
3021
|
+
}
|
|
3022
|
+
// Handle List type (case-insensitive): requires refValueType (or legacy items.type); ref when Relation
|
|
3023
|
+
if ((rawType || '').toLowerCase() === 'list') {
|
|
3024
|
+
let elementType = getRefValueType(jsonProp);
|
|
3025
|
+
// When ref is present but refValueType is not, derive refValueType as "Relation" (list of Relations)
|
|
3026
|
+
if ((!elementType || String(elementType).trim() === '') && (jsonProp.ref ?? jsonProp.items?.model ?? jsonProp.model)) {
|
|
3027
|
+
elementType = 'Relation';
|
|
3028
|
+
}
|
|
3029
|
+
if (!elementType || String(elementType).trim() === '') {
|
|
3030
|
+
const propKeys = Object.keys(jsonProp).join(', ');
|
|
3031
|
+
const propPreview = JSON.stringify(jsonProp).slice(0, 200);
|
|
3032
|
+
throw new Error(`List property "${propName}" in model "${modelName}" requires "refValueType" (e.g. refValueType: "Text" or refValueType: "Relation" with ref: "Tag"). ` +
|
|
3033
|
+
`Property has keys: [${propKeys}]. Raw property: ${propPreview}${propPreview.length >= 200 ? '...' : ''}`);
|
|
3034
|
+
}
|
|
3035
|
+
schemaProp.refValueType = normalizeDataType(elementType);
|
|
3036
|
+
const modelRef = jsonProp.ref ?? jsonProp.items?.model ?? jsonProp.model;
|
|
3037
|
+
if (modelRef) {
|
|
3038
|
+
schemaProp.ref = modelRef;
|
|
3039
|
+
schemaProp.refModelName = modelRef;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
// Handle storage configuration
|
|
3043
|
+
if (jsonProp.storage) {
|
|
3044
|
+
schemaProp.storageType = jsonProp.storage.type === 'ItemStorage' ? 'ItemStorage' : 'PropertyStorage';
|
|
3045
|
+
schemaProp.localStorageDir = jsonProp.storage.path;
|
|
3046
|
+
schemaProp.filenameSuffix = jsonProp.storage.extension;
|
|
3047
|
+
}
|
|
3048
|
+
// Handle required (for Relation/Image properties)
|
|
3049
|
+
if (jsonProp.required !== undefined) {
|
|
3050
|
+
schemaProp.required = !!jsonProp.required;
|
|
3051
|
+
}
|
|
3052
|
+
// Preserve validation rules (enum, pattern, minLength, maxLength)
|
|
3053
|
+
if (jsonProp.validation && typeof jsonProp.validation === 'object') {
|
|
3054
|
+
schemaProp.validation = jsonProp.validation;
|
|
3055
|
+
}
|
|
3056
|
+
convertedProperties[propName] = schemaProp;
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
// Create Model instance with the definition
|
|
3060
|
+
// Pass id (schemaFileId) if provided to ensure Model instance uses correct ID
|
|
3061
|
+
// Note: indexes and description are ignored - they remain in JSON for schema file format compliance but are not used by Model instances
|
|
3062
|
+
if (modelFileId) {
|
|
3063
|
+
logger$2(`Creating schema model "${modelName}" with modelFileId: ${modelFileId}`);
|
|
3064
|
+
}
|
|
3065
|
+
else {
|
|
3066
|
+
logger$2(`Warning: Creating schema model "${modelName}" without modelFileId - it will be renamed if duplicates exist`);
|
|
3067
|
+
}
|
|
3068
|
+
const modelInstance = Model.create(modelName, schemaName, {
|
|
3069
|
+
modelFileId: modelFileId, // Pass modelFileId (preferred) as id (schemaFileId) from JSON file
|
|
3070
|
+
id: modelFileId, // Also pass as id for backward compatibility
|
|
3071
|
+
properties: convertedProperties,
|
|
3072
|
+
waitForReady: false,
|
|
3073
|
+
});
|
|
3074
|
+
return modelInstance;
|
|
3075
|
+
};
|
|
3076
|
+
/**
|
|
3077
|
+
* Convert JSON import schema to ModelDefinitions
|
|
3078
|
+
* @param importData - The JSON import data
|
|
3079
|
+
* @param modelFileIds - Optional map of model names to their file IDs from the JSON file
|
|
3080
|
+
* @param propertyFileIds - Optional map of model names to maps of property names to their file IDs from the JSON file
|
|
3081
|
+
* @returns ModelDefinitions object with Model classes
|
|
3082
|
+
*/
|
|
3083
|
+
const createModelsFromJson = async (importData, modelFileIds, propertyFileIds) => {
|
|
3084
|
+
const modelDefinitions = {};
|
|
3085
|
+
const schemaName = importData.name;
|
|
3086
|
+
for (const [modelName, modelDef] of Object.entries(importData.models)) {
|
|
3087
|
+
// Get modelFileId from map if available
|
|
3088
|
+
const modelFileId = modelFileIds?.get(modelName);
|
|
3089
|
+
// Use modelFileId from map (no fallback to modelDef.id as JsonImportSchema doesn't have id property)
|
|
3090
|
+
const finalModelFileId = modelFileId;
|
|
3091
|
+
if (!finalModelFileId) {
|
|
3092
|
+
logger$2(`Warning: No modelFileId found for model "${modelName}" - model will be created without ID`);
|
|
3093
|
+
}
|
|
3094
|
+
// Get propertyFileIds for this model if available
|
|
3095
|
+
const modelPropertyFileIds = propertyFileIds?.get(modelName);
|
|
3096
|
+
const ModelClass = await createModelFromJson(modelName, modelDef, schemaName, finalModelFileId, modelPropertyFileIds);
|
|
3097
|
+
modelDefinitions[modelName] = ModelClass;
|
|
3098
|
+
}
|
|
3099
|
+
return modelDefinitions;
|
|
3100
|
+
};
|
|
3101
|
+
/**
|
|
3102
|
+
* Read a JSON import file and create Model classes from it.
|
|
3103
|
+
* For complete schema files ($schema), extracts model/property IDs so Model instances
|
|
3104
|
+
* use correct schemaFileIds - preventing duplicate models (Publication 1, etc.) when
|
|
3105
|
+
* processSchemaFiles runs multiple times.
|
|
3106
|
+
* @param filePath - Path to the JSON import file
|
|
3107
|
+
* @returns ModelDefinitions with Model classes created from the JSON schema
|
|
3108
|
+
*/
|
|
3109
|
+
const createModelsFromJsonFile = async (filePath) => {
|
|
3110
|
+
const workingDir = BaseFileManager.getWorkingDir();
|
|
3111
|
+
const internalFilePath = filePath.startsWith(workingDir) ? filePath : `${workingDir}/${filePath}`;
|
|
3112
|
+
const content = await BaseFileManager.readFileAsString(internalFilePath);
|
|
3113
|
+
const parsed = JSON.parse(content);
|
|
3114
|
+
const importData = parseJsonImportContent(content);
|
|
3115
|
+
// Extract model and property IDs from complete schema format so createModelsFromJson
|
|
3116
|
+
// can pass them - without these, Model instances get new random IDs and addModelsToDb
|
|
3117
|
+
// creates duplicates (Publication 1, Identity 1, etc.)
|
|
3118
|
+
let modelFileIds;
|
|
3119
|
+
let propertyFileIds;
|
|
3120
|
+
if (parsed?.$schema && parsed?.models) {
|
|
3121
|
+
const models = normalizeSchemaModels(parsed.models);
|
|
3122
|
+
modelFileIds = new Map();
|
|
3123
|
+
propertyFileIds = new Map();
|
|
3124
|
+
for (const [modelName, model] of Object.entries(models)) {
|
|
3125
|
+
if (model?.id)
|
|
3126
|
+
modelFileIds.set(modelName, model.id);
|
|
3127
|
+
if (model?.properties) {
|
|
3128
|
+
const propIds = new Map();
|
|
3129
|
+
for (const [propName, prop] of Object.entries(model.properties)) {
|
|
3130
|
+
if (prop?.id)
|
|
3131
|
+
propIds.set(propName, prop.id);
|
|
3132
|
+
}
|
|
3133
|
+
if (propIds.size > 0)
|
|
3134
|
+
propertyFileIds.set(modelName, propIds);
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
return createModelsFromJson(importData, modelFileIds, propertyFileIds);
|
|
3139
|
+
};
|
|
3140
|
+
|
|
3141
|
+
var $schema = "https://seedprotocol.org/schemas/data-model/v1";
|
|
3142
|
+
var version = 1;
|
|
3143
|
+
var id = "SEEDPROTOCOL";
|
|
3144
|
+
var metadata = {
|
|
3145
|
+
name: "Seed Protocol",
|
|
3146
|
+
description: "Internal Seed Protocol schema defining core models (Seed, Version, Metadata, Image, File, Html)",
|
|
3147
|
+
createdAt: "2025-01-01T00:00:00Z",
|
|
3148
|
+
updatedAt: "2025-01-01T00:00:00Z"
|
|
3149
|
+
};
|
|
3150
|
+
var models = {
|
|
3151
|
+
Seed: {
|
|
3152
|
+
id: "SEEDPROTOCOL_SEED",
|
|
3153
|
+
description: "Core seed model representing the base entity in Seed Protocol",
|
|
3154
|
+
properties: {
|
|
3155
|
+
uid: {
|
|
3156
|
+
type: "Text",
|
|
3157
|
+
id: "SEEDPROTOCOL_SEED_uid"
|
|
3158
|
+
},
|
|
3159
|
+
type: {
|
|
3160
|
+
type: "Text",
|
|
3161
|
+
id: "SEEDPROTOCOL_SEED_type"
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
},
|
|
3165
|
+
Version: {
|
|
3166
|
+
id: "SEEDPROTOCOL_VERSION",
|
|
3167
|
+
description: "Version model representing versions of seeds",
|
|
3168
|
+
properties: {
|
|
3169
|
+
seedUid: {
|
|
3170
|
+
type: "Text",
|
|
3171
|
+
id: "SEEDPROTOCOL_VERSION_seedUid"
|
|
3172
|
+
},
|
|
3173
|
+
note: {
|
|
3174
|
+
type: "Text",
|
|
3175
|
+
id: "SEEDPROTOCOL_VERSION_note"
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
},
|
|
3179
|
+
Metadata: {
|
|
3180
|
+
id: "SEEDPROTOCOL_METADATA",
|
|
3181
|
+
description: "Metadata model for storing key-value pairs",
|
|
3182
|
+
properties: {
|
|
3183
|
+
key: {
|
|
3184
|
+
type: "Text",
|
|
3185
|
+
id: "SEEDPROTOCOL_METADATA_key"
|
|
3186
|
+
},
|
|
3187
|
+
value: {
|
|
3188
|
+
type: "Text",
|
|
3189
|
+
id: "SEEDPROTOCOL_METADATA_value"
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
},
|
|
3193
|
+
Image: {
|
|
3194
|
+
id: "SEEDPROTOCOL_IMAGE",
|
|
3195
|
+
description: "Image model for storing image data and metadata",
|
|
3196
|
+
properties: {
|
|
3197
|
+
storageTransactionId: {
|
|
3198
|
+
type: "Text",
|
|
3199
|
+
id: "SEEDPROTOCOL_IMAGE_storageTransactionId"
|
|
3200
|
+
},
|
|
3201
|
+
uri: {
|
|
3202
|
+
type: "Text",
|
|
3203
|
+
id: "SEEDPROTOCOL_IMAGE_uri"
|
|
3204
|
+
},
|
|
3205
|
+
alt: {
|
|
3206
|
+
type: "Text",
|
|
3207
|
+
id: "SEEDPROTOCOL_IMAGE_alt"
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
},
|
|
3211
|
+
File: {
|
|
3212
|
+
id: "SEEDPROTOCOL_FILE",
|
|
3213
|
+
description: "File model for storing file data and metadata",
|
|
3214
|
+
properties: {
|
|
3215
|
+
storageTransactionId: {
|
|
3216
|
+
type: "Text",
|
|
3217
|
+
id: "SEEDPROTOCOL_FILE_storageTransactionId"
|
|
3218
|
+
},
|
|
3219
|
+
uri: {
|
|
3220
|
+
type: "Text",
|
|
3221
|
+
id: "SEEDPROTOCOL_FILE_uri"
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
},
|
|
3225
|
+
Html: {
|
|
3226
|
+
id: "SEEDPROTOCOL_HTML",
|
|
3227
|
+
description: "Html model for storing HTML content and metadata",
|
|
3228
|
+
properties: {
|
|
3229
|
+
storageTransactionId: {
|
|
3230
|
+
type: "Text",
|
|
3231
|
+
id: "SEEDPROTOCOL_HTML_storageTransactionId"
|
|
3232
|
+
},
|
|
3233
|
+
uri: {
|
|
3234
|
+
type: "Text",
|
|
3235
|
+
id: "SEEDPROTOCOL_HTML_uri"
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
};
|
|
3240
|
+
var enums = {
|
|
3241
|
+
};
|
|
3242
|
+
var migrations = [
|
|
3243
|
+
{
|
|
3244
|
+
version: 1,
|
|
3245
|
+
timestamp: "2025-01-01T00:00:00Z",
|
|
3246
|
+
description: "Initial seed-protocol schema with Seed, Version, Metadata, and Image models",
|
|
3247
|
+
changes: [
|
|
3248
|
+
]
|
|
3249
|
+
},
|
|
3250
|
+
{
|
|
3251
|
+
version: 2,
|
|
3252
|
+
timestamp: "2025-02-14T00:00:00Z",
|
|
3253
|
+
description: "Add File and Html models for storage seed types",
|
|
3254
|
+
changes: [
|
|
3255
|
+
"Add File model with storageTransactionId and uri",
|
|
3256
|
+
"Add Html model with storageTransactionId and uri"
|
|
3257
|
+
]
|
|
3258
|
+
}
|
|
3259
|
+
];
|
|
3260
|
+
var internalSchema = {
|
|
3261
|
+
$schema: $schema,
|
|
3262
|
+
version: version,
|
|
3263
|
+
id: id,
|
|
3264
|
+
metadata: metadata,
|
|
3265
|
+
models: models,
|
|
3266
|
+
enums: enums,
|
|
3267
|
+
migrations: migrations
|
|
3268
|
+
};
|
|
3269
|
+
|
|
3270
|
+
var SEEDPROTOCOL_Seed_Protocol_v1 = /*#__PURE__*/Object.freeze({
|
|
3271
|
+
__proto__: null,
|
|
3272
|
+
$schema: $schema,
|
|
3273
|
+
default: internalSchema,
|
|
3274
|
+
enums: enums,
|
|
3275
|
+
id: id,
|
|
3276
|
+
metadata: metadata,
|
|
3277
|
+
migrations: migrations,
|
|
3278
|
+
models: models,
|
|
3279
|
+
version: version
|
|
3280
|
+
});
|
|
3281
|
+
|
|
3282
|
+
const logger$1 = debug('seedSdk:client:actors:processSchemaFiles');
|
|
3283
|
+
let processSchemaFilesChain = Promise.resolve();
|
|
3284
|
+
function resolveSchemaFilePath(schemaFile) {
|
|
3285
|
+
const path = BaseFileManager.getPathModule();
|
|
3286
|
+
if (path.isAbsolute(schemaFile)) {
|
|
3287
|
+
return schemaFile;
|
|
3288
|
+
}
|
|
3289
|
+
if (isNode()) {
|
|
3290
|
+
return path.resolve(process.cwd(), schemaFile);
|
|
3291
|
+
}
|
|
3292
|
+
const workingDir = BaseFileManager.getWorkingDir();
|
|
3293
|
+
return path.join(workingDir, schemaFile);
|
|
3294
|
+
}
|
|
3295
|
+
/** Resolve schema display name from an inlined object or on-disk JSON path. */
|
|
3296
|
+
async function resolveSchemaNameFromSource(source) {
|
|
3297
|
+
if (typeof source === 'object' && source?.metadata?.name) {
|
|
3298
|
+
return String(source.metadata.name);
|
|
3299
|
+
}
|
|
3300
|
+
if (typeof source === 'string') {
|
|
3301
|
+
try {
|
|
3302
|
+
const resolvedPath = resolveSchemaFilePath(source);
|
|
3303
|
+
const content = await BaseFileManager.readFileAsString(resolvedPath);
|
|
3304
|
+
const parsed = JSON.parse(content);
|
|
3305
|
+
if (parsed?.metadata?.name) {
|
|
3306
|
+
return String(parsed.metadata.name);
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
catch (error) {
|
|
3310
|
+
logger$1('Could not resolve schema name from path:', source, error);
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
return null;
|
|
3314
|
+
}
|
|
3315
|
+
// Timeout for the entire schema processing operation (120 seconds)
|
|
3316
|
+
// Schema processing can be slow with syncSchemaFromSource, loadAllSchemasFromDb, and OPFS file I/O
|
|
3317
|
+
const PROCESS_SCHEMA_FILES_TIMEOUT_MS = 120000;
|
|
3318
|
+
const processSchemaFiles = fromCallback(({ sendBack, input: { context } }) => {
|
|
3319
|
+
logger$1('processSchemaFiles started');
|
|
3320
|
+
let hasResponded = false;
|
|
3321
|
+
let timeoutId = null;
|
|
3322
|
+
const reportError = (error) => {
|
|
3323
|
+
if (!hasResponded) {
|
|
3324
|
+
hasResponded = true;
|
|
3325
|
+
if (timeoutId) {
|
|
3326
|
+
clearTimeout(timeoutId);
|
|
3327
|
+
timeoutId = null;
|
|
3328
|
+
}
|
|
3329
|
+
logger$1('processSchemaFiles error:', error);
|
|
3330
|
+
sendBack({
|
|
3331
|
+
type: 'error',
|
|
3332
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
3333
|
+
});
|
|
3334
|
+
}
|
|
3335
|
+
};
|
|
3336
|
+
const reportSuccess = () => {
|
|
3337
|
+
if (!hasResponded) {
|
|
3338
|
+
hasResponded = true;
|
|
3339
|
+
if (timeoutId) {
|
|
3340
|
+
clearTimeout(timeoutId);
|
|
3341
|
+
timeoutId = null;
|
|
3342
|
+
}
|
|
3343
|
+
logger$1('processSchemaFiles completed');
|
|
3344
|
+
sendBack({ type: ClientManagerEvents.PROCESS_SCHEMA_FILES_SUCCESS });
|
|
3345
|
+
}
|
|
3346
|
+
};
|
|
3347
|
+
// Set up a single timeout for the entire operation
|
|
3348
|
+
timeoutId = setTimeout(() => {
|
|
3349
|
+
reportError(new Error(`processSchemaFiles timed out after ${PROCESS_SCHEMA_FILES_TIMEOUT_MS}ms`));
|
|
3350
|
+
}, PROCESS_SCHEMA_FILES_TIMEOUT_MS);
|
|
3351
|
+
const _processSchemaFiles = async () => {
|
|
3352
|
+
// Schemas written (or confirmed present) this run — skip redundant loadSchemaFromFile later
|
|
3353
|
+
const schemasWrittenThisRun = new Set();
|
|
3354
|
+
// First, load the internal seed-protocol schema
|
|
3355
|
+
logger$1('Loading internal seed-protocol schema');
|
|
3356
|
+
const internalSchemaData = internalSchema;
|
|
3357
|
+
const db = BaseDb.getAppDb();
|
|
3358
|
+
const internalSchemaExists = db
|
|
3359
|
+
? (await db.select().from(schemas).where(eq(schemas.schemaFileId, INTERNAL_SCHEMA_IDS[0])).limit(1)).length > 0
|
|
3360
|
+
: false;
|
|
3361
|
+
// Always mark internal schema as written so the DB/file loop does not re-apply it
|
|
3362
|
+
schemasWrittenThisRun.add(SEED_PROTOCOL_SCHEMA_NAME);
|
|
3363
|
+
if (internalSchemaExists) {
|
|
3364
|
+
logger$1('Internal seed-protocol schema already in DB, skipping import');
|
|
3365
|
+
}
|
|
3366
|
+
else {
|
|
3367
|
+
try {
|
|
3368
|
+
// Import the internal schema to database (will create models and add to store)
|
|
3369
|
+
await importJsonSchema({ contents: JSON.stringify(internalSchemaData) }, internalSchemaData.version);
|
|
3370
|
+
logger$1(`Loaded internal seed-protocol schema v${internalSchemaData.version}`);
|
|
3371
|
+
}
|
|
3372
|
+
catch (error) {
|
|
3373
|
+
// If schema already exists, that's fine - it means it was already loaded
|
|
3374
|
+
if (error?.message?.includes('already exists')) {
|
|
3375
|
+
logger$1('Internal seed-protocol schema already loaded');
|
|
3376
|
+
}
|
|
3377
|
+
else {
|
|
3378
|
+
logger$1('Error loading internal seed-protocol schema:', error);
|
|
3379
|
+
// Don't fail if internal schema can't be loaded - it might already be in DB
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
// If schema (canonical) is provided, sync it - takes precedence over schemaFile
|
|
3384
|
+
if (context.schema) {
|
|
3385
|
+
// Skip sync when schema is internal (already loaded above)
|
|
3386
|
+
const schemaName = typeof context.schema === 'object' && context.schema?.metadata?.name
|
|
3387
|
+
? String(context.schema.metadata.name)
|
|
3388
|
+
: '';
|
|
3389
|
+
const schemaId = typeof context.schema === 'object' && context.schema?.id ? String(context.schema.id) : undefined;
|
|
3390
|
+
if (isInternalSchema(schemaName, schemaId)) {
|
|
3391
|
+
logger$1('Schema is internal (Seed Protocol), already loaded - skipping syncSchemaFromSource');
|
|
3392
|
+
}
|
|
3393
|
+
else {
|
|
3394
|
+
try {
|
|
3395
|
+
const syncedName = await resolveSchemaNameFromSource(context.schema);
|
|
3396
|
+
await syncSchemaFromSource(context.schema);
|
|
3397
|
+
if (syncedName) {
|
|
3398
|
+
schemasWrittenThisRun.add(syncedName);
|
|
3399
|
+
}
|
|
3400
|
+
logger$1(`Synced canonical schema from ${typeof context.schema === 'string' ? context.schema : 'object'}`);
|
|
3401
|
+
}
|
|
3402
|
+
catch (error) {
|
|
3403
|
+
logger$1(`Error syncing canonical schema:`, error);
|
|
3404
|
+
// Don't fail init - schema might already be in DB
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
else if (context.schemaFile) {
|
|
3409
|
+
// Legacy: schemaFile - load via importJsonSchema
|
|
3410
|
+
try {
|
|
3411
|
+
const resolvedPath = resolveSchemaFilePath(context.schemaFile);
|
|
3412
|
+
const content = await BaseFileManager.readFileAsString(resolvedPath);
|
|
3413
|
+
const parsed = JSON.parse(content);
|
|
3414
|
+
await importJsonSchema({ contents: content });
|
|
3415
|
+
if (parsed?.metadata?.name) {
|
|
3416
|
+
schemasWrittenThisRun.add(String(parsed.metadata.name));
|
|
3417
|
+
}
|
|
3418
|
+
logger$1(`Loaded schema from ${resolvedPath}`);
|
|
3419
|
+
}
|
|
3420
|
+
catch (error) {
|
|
3421
|
+
logger$1(`Error loading schema file ${context.schemaFile}:`, error);
|
|
3422
|
+
// Don't fail init - schema might already be in DB
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
// Process minimal import files (files without $schema) - skip when canonical schema is set
|
|
3426
|
+
if (!context.schema) {
|
|
3427
|
+
logger$1('Listing schema files');
|
|
3428
|
+
let schemaFiles = [];
|
|
3429
|
+
try {
|
|
3430
|
+
schemaFiles = await listSchemaFiles();
|
|
3431
|
+
logger$1(`Found ${schemaFiles.length} schema files to process`);
|
|
3432
|
+
}
|
|
3433
|
+
catch (error) {
|
|
3434
|
+
logger$1('Error listing schema files (continuing anyway):', error);
|
|
3435
|
+
schemaFiles = [];
|
|
3436
|
+
}
|
|
3437
|
+
for (const schemaFile of schemaFiles) {
|
|
3438
|
+
try {
|
|
3439
|
+
await importJsonSchema(schemaFile.filePath);
|
|
3440
|
+
if (schemaFile.name) {
|
|
3441
|
+
schemasWrittenThisRun.add(schemaFile.name);
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
catch (error) {
|
|
3445
|
+
logger$1(`Error importing schema file ${schemaFile.filePath}:`, error);
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
// Then, load all schemas using the unified database-first approach
|
|
3450
|
+
logger$1('Loading schemas from database and files');
|
|
3451
|
+
let allSchemasData = [];
|
|
3452
|
+
try {
|
|
3453
|
+
allSchemasData = await loadAllSchemasFromDb();
|
|
3454
|
+
logger$1(`Loaded ${allSchemasData.length} schemas (${allSchemasData.filter(s => s.isDraft).length} drafts, ${allSchemasData.filter(s => !s.isDraft).length} published)`);
|
|
3455
|
+
}
|
|
3456
|
+
catch (error) {
|
|
3457
|
+
logger$1('Error loading schemas from database (continuing anyway):', error);
|
|
3458
|
+
// Continue with empty array - this is not critical for initialization
|
|
3459
|
+
// Schemas can be loaded on-demand later
|
|
3460
|
+
allSchemasData = [];
|
|
3461
|
+
}
|
|
3462
|
+
// Collect models to add to context (schemas are now loaded on-demand via database queries)
|
|
3463
|
+
const allModels = { ...(context.models || {}) };
|
|
3464
|
+
// Linked model count per schema row, to tell whether an earlier load already applied a file's models
|
|
3465
|
+
const linkedModelCounts = new Map();
|
|
3466
|
+
if (db) {
|
|
3467
|
+
try {
|
|
3468
|
+
const rows = await db
|
|
3469
|
+
.select({ schemaId: modelSchemas.schemaId, count: countDistinct(modelSchemas.modelId) })
|
|
3470
|
+
.from(modelSchemas)
|
|
3471
|
+
.groupBy(modelSchemas.schemaId);
|
|
3472
|
+
for (const row of rows) {
|
|
3473
|
+
if (row.schemaId != null)
|
|
3474
|
+
linkedModelCounts.set(row.schemaId, Number(row.count));
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
catch (error) {
|
|
3478
|
+
logger$1('Error counting linked models (re-applying all schema files):', error);
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
for (const schemaData of allSchemasData) {
|
|
3482
|
+
const schema = schemaData.schema;
|
|
3483
|
+
const schemaName = schema.metadata?.name || 'unknown';
|
|
3484
|
+
logger$1(`Processing schema: ${schemaName} v${schema.version} (${schemaData.isDraft ? 'draft' : 'published'}, source: ${schemaData.source})`);
|
|
3485
|
+
// For published schemas (not drafts), load models from file if available
|
|
3486
|
+
if (!schemaData.isDraft && schema.id) {
|
|
3487
|
+
// loadAllSchemasFromDb already picked the latest file for this schema (and read it)
|
|
3488
|
+
const latestFilePath = schemaData.filePath;
|
|
3489
|
+
if (latestFilePath) {
|
|
3490
|
+
try {
|
|
3491
|
+
// Skip loadSchemaFromFile when this schema was already written this run (or is
|
|
3492
|
+
// internal) — re-applying creates UNIQUE constraint races on properties.
|
|
3493
|
+
// Use createModelsFromJsonFile only to get model definitions for context.
|
|
3494
|
+
const alreadyWritten = schemasWrittenThisRun.has(schemaName) ||
|
|
3495
|
+
isInternalSchema(schemaName, schema.id);
|
|
3496
|
+
// Also skip when an earlier init already applied this file: the DB row matches it and
|
|
3497
|
+
// all its models are linked. Re-applying every published schema on every init made
|
|
3498
|
+
// init time grow with the number of schema files.
|
|
3499
|
+
const expectedModelCount = Object.keys(schema.models || {}).length;
|
|
3500
|
+
const alreadyApplied = schemaData.dbMatchesFile === true &&
|
|
3501
|
+
schemaData.schemaRecordId != null &&
|
|
3502
|
+
(linkedModelCounts.get(schemaData.schemaRecordId) ?? 0) >= expectedModelCount;
|
|
3503
|
+
if (alreadyWritten || alreadyApplied) {
|
|
3504
|
+
const modelDefinitions = await createModelsFromJsonFile(latestFilePath);
|
|
3505
|
+
Object.assign(allModels, modelDefinitions);
|
|
3506
|
+
}
|
|
3507
|
+
else {
|
|
3508
|
+
await loadSchemaFromFile(latestFilePath);
|
|
3509
|
+
const modelDefinitions = await createModelsFromJsonFile(latestFilePath);
|
|
3510
|
+
Object.assign(allModels, modelDefinitions);
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
catch (error) {
|
|
3514
|
+
logger$1(`Error loading schema file for ${schemaName}:`, error);
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
else {
|
|
3518
|
+
// File doesn't exist (e.g. schema passed via config.schema) - use schema data from loadAllSchemasFromDb
|
|
3519
|
+
try {
|
|
3520
|
+
const schemaModels = schema.models;
|
|
3521
|
+
if (schemaModels && Object.keys(schemaModels).length > 0) {
|
|
3522
|
+
const modelFileIds = new Map();
|
|
3523
|
+
const propertyFileIds = new Map();
|
|
3524
|
+
for (const [modelName, model] of Object.entries(schemaModels)) {
|
|
3525
|
+
const modelId = model.id;
|
|
3526
|
+
if (modelId)
|
|
3527
|
+
modelFileIds.set(modelName, modelId);
|
|
3528
|
+
if (model.properties) {
|
|
3529
|
+
const propIds = new Map();
|
|
3530
|
+
for (const [propName, prop] of Object.entries(model.properties)) {
|
|
3531
|
+
const propId = prop.id;
|
|
3532
|
+
if (propId)
|
|
3533
|
+
propIds.set(propName, propId);
|
|
3534
|
+
}
|
|
3535
|
+
if (propIds.size > 0)
|
|
3536
|
+
propertyFileIds.set(modelName, propIds);
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
const importData = {
|
|
3540
|
+
name: schemaName,
|
|
3541
|
+
models: Object.fromEntries(Object.entries(schemaModels).map(([modelName, model]) => [
|
|
3542
|
+
modelName,
|
|
3543
|
+
{
|
|
3544
|
+
description: model.description,
|
|
3545
|
+
properties: Object.fromEntries(Object.entries(model.properties || {}).map(([propName, prop]) => {
|
|
3546
|
+
const p = prop;
|
|
3547
|
+
return [
|
|
3548
|
+
propName,
|
|
3549
|
+
{
|
|
3550
|
+
type: p.dataType || p.type,
|
|
3551
|
+
model: p.refModelName || p.ref,
|
|
3552
|
+
refValueType: p.refValueType,
|
|
3553
|
+
ref: p.ref,
|
|
3554
|
+
...p,
|
|
3555
|
+
},
|
|
3556
|
+
];
|
|
3557
|
+
})),
|
|
3558
|
+
},
|
|
3559
|
+
])),
|
|
3560
|
+
};
|
|
3561
|
+
const modelDefinitions = await createModelsFromJson(importData, modelFileIds, propertyFileIds);
|
|
3562
|
+
Object.assign(allModels, modelDefinitions);
|
|
3563
|
+
logger$1(`Loaded ${Object.keys(modelDefinitions).length} models from schema data for ${schemaName}`);
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
catch (error) {
|
|
3567
|
+
logger$1(`Error creating models from schema data for ${schemaName}:`, error);
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
}
|
|
3571
|
+
else if (schemaData.isDraft) {
|
|
3572
|
+
// For drafts, create models from the schema data
|
|
3573
|
+
try {
|
|
3574
|
+
const schemaModels = schema.models;
|
|
3575
|
+
if (schemaModels && Object.keys(schemaModels).length > 0) {
|
|
3576
|
+
const modelFileIds = new Map();
|
|
3577
|
+
const propertyFileIds = new Map();
|
|
3578
|
+
for (const [modelName, model] of Object.entries(schemaModels)) {
|
|
3579
|
+
const modelId = model.id;
|
|
3580
|
+
if (modelId)
|
|
3581
|
+
modelFileIds.set(modelName, modelId);
|
|
3582
|
+
if (model.properties) {
|
|
3583
|
+
const propIds = new Map();
|
|
3584
|
+
for (const [propName, prop] of Object.entries(model.properties)) {
|
|
3585
|
+
const propId = prop.id;
|
|
3586
|
+
if (propId)
|
|
3587
|
+
propIds.set(propName, propId);
|
|
3588
|
+
}
|
|
3589
|
+
if (propIds.size > 0)
|
|
3590
|
+
propertyFileIds.set(modelName, propIds);
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
const importData = {
|
|
3594
|
+
name: schemaName,
|
|
3595
|
+
models: Object.fromEntries(Object.entries(schemaModels).map(([modelName, model]) => [
|
|
3596
|
+
modelName,
|
|
3597
|
+
{
|
|
3598
|
+
description: model.description,
|
|
3599
|
+
properties: Object.fromEntries(Object.entries(model.properties || {}).map(([propName, prop]) => {
|
|
3600
|
+
const p = prop;
|
|
3601
|
+
return [
|
|
3602
|
+
propName,
|
|
3603
|
+
{
|
|
3604
|
+
type: p.dataType || p.type,
|
|
3605
|
+
model: p.refModelName || p.ref,
|
|
3606
|
+
refValueType: p.refValueType,
|
|
3607
|
+
ref: p.ref,
|
|
3608
|
+
...p,
|
|
3609
|
+
},
|
|
3610
|
+
];
|
|
3611
|
+
})),
|
|
3612
|
+
},
|
|
3613
|
+
])),
|
|
3614
|
+
};
|
|
3615
|
+
const modelDefinitions = await createModelsFromJson(importData, modelFileIds, propertyFileIds);
|
|
3616
|
+
Object.assign(allModels, modelDefinitions);
|
|
3617
|
+
logger$1(`Loaded ${Object.keys(modelDefinitions).length} models from draft schema ${schemaName}`);
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
catch (error) {
|
|
3621
|
+
logger$1(`Error creating models from draft schema ${schemaName}:`, error);
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
// Schemas are now loaded on-demand via database queries (useSchemas hook uses loadAllSchemasFromDb)
|
|
3625
|
+
// No need to populate context.schemas
|
|
3626
|
+
}
|
|
3627
|
+
// Update context with models only (schemas are loaded on-demand via database queries)
|
|
3628
|
+
sendBack({
|
|
3629
|
+
type: ClientManagerEvents.UPDATE_CONTEXT,
|
|
3630
|
+
context: {
|
|
3631
|
+
models: allModels,
|
|
3632
|
+
}
|
|
3633
|
+
});
|
|
3634
|
+
};
|
|
3635
|
+
processSchemaFilesChain = processSchemaFilesChain
|
|
3636
|
+
// Its schema and model inserts check for rows first, so tabs take turns (docs/MULTI_TAB.md).
|
|
3637
|
+
.then(() => withSeedDbLock('init', context.filesDir, _processSchemaFiles))
|
|
3638
|
+
.catch((err) => {
|
|
3639
|
+
logger$1('processSchemaFiles chain error:', err);
|
|
3640
|
+
throw err;
|
|
3641
|
+
});
|
|
3642
|
+
processSchemaFilesChain.then(reportSuccess).catch((error) => {
|
|
3643
|
+
reportError(error instanceof Error ? error : new Error(String(error)));
|
|
3644
|
+
});
|
|
3645
|
+
// Cleanup function to clear timeout if actor is stopped
|
|
3646
|
+
return () => {
|
|
3647
|
+
if (timeoutId) {
|
|
3648
|
+
clearTimeout(timeoutId);
|
|
3649
|
+
timeoutId = null;
|
|
3650
|
+
}
|
|
3651
|
+
};
|
|
3652
|
+
});
|
|
3653
|
+
|
|
3654
|
+
const { UNINITIALIZED, PLATFORM_CLASSES_INIT, FILE_SYSTEM_INIT, DB_INIT, SAVE_CONFIG, PROCESS_SCHEMA_FILES, ADD_MODELS_TO_STORE, ADD_MODELS_TO_DB, IDLE, INIT_FAILED, } = ClientManagerState;
|
|
3655
|
+
const { UPDATE_CONTEXT, PLATFORM_CLASSES_READY, FILE_SYSTEM_READY, DB_READY, SAVE_CONFIG_SUCCESS, SAVE_APP_STATE_SUCCESS, SAVE_APP_STATE_ERROR, SET_ADDRESSES, ADD_MODELS_TO_STORE_SUCCESS, ADD_MODELS_TO_DB_SUCCESS, PROCESS_SCHEMA_FILES_SUCCESS, } = ClientManagerEvents;
|
|
3656
|
+
const logSyncAfterAddressSave = debug("seedSdk:client:clientManagerMachine:syncAfterAddressSave");
|
|
3657
|
+
function addressesFromPersistedAddressValue(value) {
|
|
3658
|
+
if (!value || typeof value !== "object")
|
|
3659
|
+
return [];
|
|
3660
|
+
const v = value;
|
|
3661
|
+
const owned = Array.isArray(v.owned)
|
|
3662
|
+
? v.owned.filter((a) => typeof a === "string")
|
|
3663
|
+
: [];
|
|
3664
|
+
const watched = Array.isArray(v.watched)
|
|
3665
|
+
? v.watched.filter((a) => typeof a === "string")
|
|
3666
|
+
: [];
|
|
3667
|
+
const seen = new Set();
|
|
3668
|
+
const out = [];
|
|
3669
|
+
for (const a of [...owned, ...watched]) {
|
|
3670
|
+
const k = a.toLowerCase();
|
|
3671
|
+
if (!seen.has(k)) {
|
|
3672
|
+
seen.add(k);
|
|
3673
|
+
out.push(a);
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
return out;
|
|
3677
|
+
}
|
|
3678
|
+
function maybeRunSyncFromEasAfterAddressSave({ context, event, }) {
|
|
3679
|
+
const merged = addressesFromPersistedAddressValue(event.value);
|
|
3680
|
+
if (!context.syncFromEasOnAddressChange)
|
|
3681
|
+
return;
|
|
3682
|
+
if (event.key !== "addresses")
|
|
3683
|
+
return;
|
|
3684
|
+
if (merged.length === 0)
|
|
3685
|
+
return;
|
|
3686
|
+
try {
|
|
3687
|
+
requestEasSyncFromAddressChange(merged);
|
|
3688
|
+
}
|
|
3689
|
+
catch (err) {
|
|
3690
|
+
logSyncAfterAddressSave("requestEasSyncFromAddressChange after address save failed", err);
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
function emitAddressesPersistedIfAddressesKey(event) {
|
|
3694
|
+
if (event.key !== "addresses")
|
|
3695
|
+
return;
|
|
3696
|
+
const payload = parseAddressesPersistedPayload(event.value);
|
|
3697
|
+
emitAcrossTabs(ADDRESSES_PERSISTED_EVENT, payload);
|
|
3698
|
+
}
|
|
3699
|
+
function errorFromEvent(event, fallbackMessage) {
|
|
3700
|
+
const error = event?.error;
|
|
3701
|
+
if (error instanceof Error) {
|
|
3702
|
+
return error;
|
|
3703
|
+
}
|
|
3704
|
+
return new Error(String(error ?? fallbackMessage));
|
|
3705
|
+
}
|
|
3706
|
+
const failInit = (fallbackMessage) => ({
|
|
3707
|
+
target: INIT_FAILED,
|
|
3708
|
+
actions: {
|
|
3709
|
+
type: "assignInitError",
|
|
3710
|
+
params: { fallbackMessage },
|
|
3711
|
+
},
|
|
3712
|
+
});
|
|
3713
|
+
const clientManagerMachine = setup({
|
|
3714
|
+
types: {
|
|
3715
|
+
context: {},
|
|
3716
|
+
input: {},
|
|
3717
|
+
},
|
|
3718
|
+
actors: {
|
|
3719
|
+
platformClassesInit,
|
|
3720
|
+
fileSystemInit,
|
|
3721
|
+
dbInit,
|
|
3722
|
+
saveConfig,
|
|
3723
|
+
saveAppState,
|
|
3724
|
+
addModelsToStore,
|
|
3725
|
+
addModelsToDb,
|
|
3726
|
+
processSchemaFiles,
|
|
3727
|
+
},
|
|
3728
|
+
actions: {
|
|
3729
|
+
assignInitError: assign(({ event }, params) => ({
|
|
3730
|
+
initError: errorFromEvent(event, params.fallbackMessage),
|
|
3731
|
+
isInitialized: false,
|
|
3732
|
+
})),
|
|
3733
|
+
},
|
|
3734
|
+
}).createMachine({
|
|
3735
|
+
id: MachineIds.CLIENT_MANAGER,
|
|
3736
|
+
initial: UNINITIALIZED,
|
|
3737
|
+
context: ({ input }) => input,
|
|
3738
|
+
on: {
|
|
3739
|
+
[UPDATE_CONTEXT]: {
|
|
3740
|
+
actions: assign(({ event, context }) => {
|
|
3741
|
+
return {
|
|
3742
|
+
...context,
|
|
3743
|
+
...event.context,
|
|
3744
|
+
};
|
|
3745
|
+
}),
|
|
3746
|
+
},
|
|
3747
|
+
init: {
|
|
3748
|
+
target: `.${PLATFORM_CLASSES_INIT}`,
|
|
3749
|
+
actions: assign({
|
|
3750
|
+
isInitialized: false,
|
|
3751
|
+
initError: undefined,
|
|
3752
|
+
saveError: undefined,
|
|
3753
|
+
}),
|
|
3754
|
+
},
|
|
3755
|
+
},
|
|
3756
|
+
states: {
|
|
3757
|
+
[UNINITIALIZED]: {
|
|
3758
|
+
on: {
|
|
3759
|
+
init: {
|
|
3760
|
+
target: PLATFORM_CLASSES_INIT,
|
|
3761
|
+
},
|
|
3762
|
+
},
|
|
3763
|
+
},
|
|
3764
|
+
[PLATFORM_CLASSES_INIT]: {
|
|
3765
|
+
on: {
|
|
3766
|
+
[PLATFORM_CLASSES_READY]: {
|
|
3767
|
+
target: FILE_SYSTEM_INIT,
|
|
3768
|
+
},
|
|
3769
|
+
error: failInit('Platform classes initialization failed'),
|
|
3770
|
+
},
|
|
3771
|
+
invoke: {
|
|
3772
|
+
src: 'platformClassesInit',
|
|
3773
|
+
input: ({ event, context }) => ({
|
|
3774
|
+
event: event,
|
|
3775
|
+
context
|
|
3776
|
+
}),
|
|
3777
|
+
onError: failInit('Platform classes initialization failed'),
|
|
3778
|
+
},
|
|
3779
|
+
},
|
|
3780
|
+
[FILE_SYSTEM_INIT]: {
|
|
3781
|
+
on: {
|
|
3782
|
+
[FILE_SYSTEM_READY]: {
|
|
3783
|
+
target: DB_INIT,
|
|
3784
|
+
},
|
|
3785
|
+
error: failInit('File system initialization failed'),
|
|
3786
|
+
},
|
|
3787
|
+
invoke: {
|
|
3788
|
+
src: 'fileSystemInit',
|
|
3789
|
+
input: ({ context }) => ({ context }),
|
|
3790
|
+
onError: failInit('File system initialization failed'),
|
|
3791
|
+
},
|
|
3792
|
+
},
|
|
3793
|
+
[DB_INIT]: {
|
|
3794
|
+
on: {
|
|
3795
|
+
[DB_READY]: {
|
|
3796
|
+
target: SAVE_CONFIG,
|
|
3797
|
+
},
|
|
3798
|
+
error: failInit('Database initialization failed'),
|
|
3799
|
+
},
|
|
3800
|
+
invoke: {
|
|
3801
|
+
src: 'dbInit',
|
|
3802
|
+
input: ({ context }) => ({ context }),
|
|
3803
|
+
onError: failInit('Database initialization failed'),
|
|
3804
|
+
},
|
|
3805
|
+
},
|
|
3806
|
+
[SAVE_CONFIG]: {
|
|
3807
|
+
on: {
|
|
3808
|
+
[SAVE_CONFIG_SUCCESS]: {
|
|
3809
|
+
target: PROCESS_SCHEMA_FILES,
|
|
3810
|
+
},
|
|
3811
|
+
error: failInit('Saving config failed'),
|
|
3812
|
+
},
|
|
3813
|
+
invoke: {
|
|
3814
|
+
src: 'saveConfig',
|
|
3815
|
+
input: ({ context }) => ({ context }),
|
|
3816
|
+
onError: failInit('Saving config failed'),
|
|
3817
|
+
},
|
|
3818
|
+
},
|
|
3819
|
+
[PROCESS_SCHEMA_FILES]: {
|
|
3820
|
+
on: {
|
|
3821
|
+
[PROCESS_SCHEMA_FILES_SUCCESS]: {
|
|
3822
|
+
target: ADD_MODELS_TO_STORE,
|
|
3823
|
+
},
|
|
3824
|
+
error: failInit('Schema processing failed'),
|
|
3825
|
+
},
|
|
3826
|
+
invoke: {
|
|
3827
|
+
src: 'processSchemaFiles',
|
|
3828
|
+
input: ({ context }) => ({ context }),
|
|
3829
|
+
onError: failInit('Schema processing failed'),
|
|
3830
|
+
},
|
|
3831
|
+
},
|
|
3832
|
+
[ADD_MODELS_TO_STORE]: {
|
|
3833
|
+
on: {
|
|
3834
|
+
[ADD_MODELS_TO_STORE_SUCCESS]: {
|
|
3835
|
+
target: ADD_MODELS_TO_DB,
|
|
3836
|
+
},
|
|
3837
|
+
error: failInit('Adding models to store failed'),
|
|
3838
|
+
},
|
|
3839
|
+
invoke: {
|
|
3840
|
+
src: 'addModelsToStore',
|
|
3841
|
+
input: ({ context }) => ({ context }),
|
|
3842
|
+
onError: failInit('Adding models to store failed'),
|
|
3843
|
+
},
|
|
3844
|
+
},
|
|
3845
|
+
[ADD_MODELS_TO_DB]: {
|
|
3846
|
+
on: {
|
|
3847
|
+
[ADD_MODELS_TO_DB_SUCCESS]: {
|
|
3848
|
+
target: IDLE,
|
|
3849
|
+
},
|
|
3850
|
+
error: failInit('Adding models to database failed'),
|
|
3851
|
+
},
|
|
3852
|
+
invoke: {
|
|
3853
|
+
src: 'addModelsToDb',
|
|
3854
|
+
input: ({ context }) => ({ context }),
|
|
3855
|
+
onError: failInit('Adding models to database failed'),
|
|
3856
|
+
},
|
|
3857
|
+
},
|
|
3858
|
+
[INIT_FAILED]: {
|
|
3859
|
+
entry: assign({
|
|
3860
|
+
isInitialized: false,
|
|
3861
|
+
}),
|
|
3862
|
+
on: {
|
|
3863
|
+
init: {
|
|
3864
|
+
target: PLATFORM_CLASSES_INIT,
|
|
3865
|
+
actions: assign({
|
|
3866
|
+
isInitialized: false,
|
|
3867
|
+
initError: undefined,
|
|
3868
|
+
saveError: undefined,
|
|
3869
|
+
}),
|
|
3870
|
+
},
|
|
3871
|
+
},
|
|
3872
|
+
},
|
|
3873
|
+
[IDLE]: {
|
|
3874
|
+
entry: assign({
|
|
3875
|
+
isInitialized: true,
|
|
3876
|
+
}),
|
|
3877
|
+
on: {
|
|
3878
|
+
[SAVE_APP_STATE_SUCCESS]: {
|
|
3879
|
+
actions: [
|
|
3880
|
+
assign(() => {
|
|
3881
|
+
return {
|
|
3882
|
+
isSaving: false,
|
|
3883
|
+
saveError: undefined,
|
|
3884
|
+
};
|
|
3885
|
+
}),
|
|
3886
|
+
({ context, event }) => maybeRunSyncFromEasAfterAddressSave({
|
|
3887
|
+
context,
|
|
3888
|
+
event: event,
|
|
3889
|
+
}),
|
|
3890
|
+
({ event }) => emitAddressesPersistedIfAddressesKey(event),
|
|
3891
|
+
],
|
|
3892
|
+
},
|
|
3893
|
+
[SAVE_APP_STATE_ERROR]: {
|
|
3894
|
+
actions: assign(({ event }) => ({
|
|
3895
|
+
isSaving: false,
|
|
3896
|
+
saveError: errorFromEvent(event, 'Failed to save app state'),
|
|
3897
|
+
})),
|
|
3898
|
+
},
|
|
3899
|
+
[SET_ADDRESSES]: {
|
|
3900
|
+
actions: [
|
|
3901
|
+
assign(({ event, spawn }) => {
|
|
3902
|
+
const { addresses } = event;
|
|
3903
|
+
const normalized = normalizeAddressConfig(addresses);
|
|
3904
|
+
spawn('saveAppState', {
|
|
3905
|
+
input: {
|
|
3906
|
+
key: 'addresses',
|
|
3907
|
+
value: { owned: normalized.owned, watched: normalized.watched },
|
|
3908
|
+
},
|
|
3909
|
+
});
|
|
3910
|
+
return {
|
|
3911
|
+
addresses: normalized.owned,
|
|
3912
|
+
ownedAddresses: normalized.owned,
|
|
3913
|
+
watchedAddresses: normalized.watched,
|
|
3914
|
+
isSaving: true,
|
|
3915
|
+
saveError: undefined,
|
|
3916
|
+
};
|
|
3917
|
+
})
|
|
3918
|
+
],
|
|
3919
|
+
},
|
|
3920
|
+
init: {
|
|
3921
|
+
target: PLATFORM_CLASSES_INIT,
|
|
3922
|
+
actions: assign({
|
|
3923
|
+
isInitialized: false,
|
|
3924
|
+
initError: undefined,
|
|
3925
|
+
saveError: undefined,
|
|
3926
|
+
}),
|
|
3927
|
+
},
|
|
3928
|
+
},
|
|
3929
|
+
},
|
|
3930
|
+
},
|
|
3931
|
+
});
|
|
3932
|
+
|
|
3933
|
+
/**
|
|
3934
|
+
* Coalesce overlapping async init calls onto one in-flight promise.
|
|
3935
|
+
* Skip when already successfully ready. Always clear in-flight on settle
|
|
3936
|
+
* so a failed init can be retried.
|
|
3937
|
+
*/
|
|
3938
|
+
function createCoalescedAsync(run, shouldSkip) {
|
|
3939
|
+
let inFlight = null;
|
|
3940
|
+
return (arg) => {
|
|
3941
|
+
if (inFlight) {
|
|
3942
|
+
return inFlight;
|
|
3943
|
+
}
|
|
3944
|
+
if (shouldSkip(arg)) {
|
|
3945
|
+
return Promise.resolve();
|
|
3946
|
+
}
|
|
3947
|
+
const pending = run(arg).finally(() => {
|
|
3948
|
+
if (inFlight === pending) {
|
|
3949
|
+
inFlight = null;
|
|
3950
|
+
}
|
|
3951
|
+
});
|
|
3952
|
+
inFlight = pending;
|
|
3953
|
+
return pending;
|
|
3954
|
+
};
|
|
3955
|
+
}
|
|
3956
|
+
|
|
3957
|
+
function msToUnixSeconds(ms) {
|
|
3958
|
+
if (ms == null || !Number.isFinite(ms))
|
|
3959
|
+
return 0;
|
|
3960
|
+
// Heuristic: values already in seconds are small; ms are large
|
|
3961
|
+
if (ms > 1e12)
|
|
3962
|
+
return Math.floor(ms / 1000);
|
|
3963
|
+
if (ms > 1e10)
|
|
3964
|
+
return Math.floor(ms / 1000);
|
|
3965
|
+
return Math.floor(ms);
|
|
3966
|
+
}
|
|
3967
|
+
function toSnakeCaseName(name) {
|
|
3968
|
+
return name
|
|
3969
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
3970
|
+
.replace(/([A-Z])([A-Z][a-z])/g, '$1_$2')
|
|
3971
|
+
.toLowerCase();
|
|
3972
|
+
}
|
|
3973
|
+
function parsePropertyValue(raw) {
|
|
3974
|
+
if (raw == null)
|
|
3975
|
+
return '';
|
|
3976
|
+
const t = raw.trim();
|
|
3977
|
+
if ((t.startsWith('[') && t.endsWith(']')) ||
|
|
3978
|
+
(t.startsWith('{') && t.endsWith('}'))) {
|
|
3979
|
+
try {
|
|
3980
|
+
return JSON.parse(t);
|
|
3981
|
+
}
|
|
3982
|
+
catch {
|
|
3983
|
+
return raw;
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
return raw;
|
|
3987
|
+
}
|
|
3988
|
+
/**
|
|
3989
|
+
* Revocation as this database knows it. `revoked_at` (unix seconds) is the local truth: sync keeps
|
|
3990
|
+
* EAS's revocationTime there and unpublish stamps it before EAS's index catches up, while
|
|
3991
|
+
* `attestation_raw` only records what EAS said when the row was fetched.
|
|
3992
|
+
*/
|
|
3993
|
+
function isRevokedLocally(revokedAt) {
|
|
3994
|
+
return revokedAt != null && revokedAt !== 0;
|
|
3995
|
+
}
|
|
3996
|
+
/** Overrides the fetch-time `revoked`/`revocationTime` of a stored attestation with `revoked_at`. */
|
|
3997
|
+
function withLocalRevocation(att, revokedAt) {
|
|
3998
|
+
const revoked = isRevokedLocally(revokedAt);
|
|
3999
|
+
return { ...att, revoked, revocationTime: revoked ? revokedAt : 0 };
|
|
4000
|
+
}
|
|
4001
|
+
function attestationFromRaw(raw) {
|
|
4002
|
+
if (!raw || !raw.trim())
|
|
4003
|
+
return null;
|
|
4004
|
+
try {
|
|
4005
|
+
const parsed = JSON.parse(raw);
|
|
4006
|
+
if (parsed && typeof parsed.id === 'string')
|
|
4007
|
+
return parsed;
|
|
4008
|
+
}
|
|
4009
|
+
catch {
|
|
4010
|
+
// fall through
|
|
4011
|
+
}
|
|
4012
|
+
return null;
|
|
4013
|
+
}
|
|
4014
|
+
function seedRowToAttestation(row) {
|
|
4015
|
+
if (!row.uid || !isValidEasAttestationUid(row.uid))
|
|
4016
|
+
return null;
|
|
4017
|
+
const fromRaw = attestationFromRaw(row.attestationRaw);
|
|
4018
|
+
if (fromRaw) {
|
|
4019
|
+
const schemaName = row.schemaName ??
|
|
4020
|
+
fromRaw.schema?.schemaNames?.[0]?.name ??
|
|
4021
|
+
row.type ??
|
|
4022
|
+
'unknown';
|
|
4023
|
+
return withLocalRevocation({
|
|
4024
|
+
...fromRaw,
|
|
4025
|
+
id: row.uid,
|
|
4026
|
+
schema: {
|
|
4027
|
+
schemaNames: [{ name: schemaName }],
|
|
4028
|
+
},
|
|
4029
|
+
timeCreated: fromRaw.timeCreated ||
|
|
4030
|
+
msToUnixSeconds(row.attestationCreatedAt),
|
|
4031
|
+
attester: fromRaw.attester ?? row.publisher ?? undefined,
|
|
4032
|
+
schemaId: fromRaw.schemaId || row.schemaUid || '',
|
|
4033
|
+
}, row.revokedAt);
|
|
4034
|
+
}
|
|
4035
|
+
const schemaName = row.schemaName ?? row.type ?? 'unknown';
|
|
4036
|
+
return withLocalRevocation({
|
|
4037
|
+
id: row.uid,
|
|
4038
|
+
decodedDataJson: '',
|
|
4039
|
+
refUID: '0x0000000000000000000000000000000000000000000000000000000000000000',
|
|
4040
|
+
schemaId: row.schemaUid || '',
|
|
4041
|
+
timeCreated: msToUnixSeconds(row.attestationCreatedAt),
|
|
4042
|
+
attester: row.publisher ?? undefined,
|
|
4043
|
+
schema: { schemaNames: [{ name: schemaName }] },
|
|
4044
|
+
}, row.revokedAt);
|
|
4045
|
+
}
|
|
4046
|
+
function versionRowToAttestation(row) {
|
|
4047
|
+
if (!row.uid || !isValidEasAttestationUid(row.uid))
|
|
4048
|
+
return null;
|
|
4049
|
+
if (!row.seedUid)
|
|
4050
|
+
return null;
|
|
4051
|
+
const fromRaw = attestationFromRaw(row.attestationRaw);
|
|
4052
|
+
if (fromRaw) {
|
|
4053
|
+
return withLocalRevocation({
|
|
4054
|
+
...fromRaw,
|
|
4055
|
+
id: row.uid,
|
|
4056
|
+
refUID: row.seedUid,
|
|
4057
|
+
timeCreated: fromRaw.timeCreated ||
|
|
4058
|
+
msToUnixSeconds(row.attestationCreatedAt),
|
|
4059
|
+
attester: fromRaw.attester ?? row.publisher ?? undefined,
|
|
4060
|
+
}, row.revokedAt);
|
|
4061
|
+
}
|
|
4062
|
+
return withLocalRevocation({
|
|
4063
|
+
id: row.uid,
|
|
4064
|
+
decodedDataJson: '',
|
|
4065
|
+
refUID: row.seedUid,
|
|
4066
|
+
schemaId: '',
|
|
4067
|
+
timeCreated: msToUnixSeconds(row.attestationCreatedAt),
|
|
4068
|
+
attester: row.publisher ?? undefined,
|
|
4069
|
+
}, row.revokedAt);
|
|
4070
|
+
}
|
|
4071
|
+
function metadataRowToAttestation(row) {
|
|
4072
|
+
if (!row.uid || !isValidEasAttestationUid(row.uid))
|
|
4073
|
+
return null;
|
|
4074
|
+
if (!row.versionUid)
|
|
4075
|
+
return null;
|
|
4076
|
+
const fromRaw = attestationFromRaw(row.attestationRaw);
|
|
4077
|
+
if (fromRaw) {
|
|
4078
|
+
return withLocalRevocation({
|
|
4079
|
+
...fromRaw,
|
|
4080
|
+
id: row.uid,
|
|
4081
|
+
refUID: row.versionUid,
|
|
4082
|
+
schemaId: fromRaw.schemaId || row.schemaUid || '',
|
|
4083
|
+
timeCreated: fromRaw.timeCreated ||
|
|
4084
|
+
msToUnixSeconds(row.attestationCreatedAt),
|
|
4085
|
+
attester: fromRaw.attester ?? row.publisher ?? undefined,
|
|
4086
|
+
}, row.revokedAt);
|
|
4087
|
+
}
|
|
4088
|
+
const name = row.propertyName
|
|
4089
|
+
? toSnakeCaseName(row.propertyName)
|
|
4090
|
+
: 'unknown';
|
|
4091
|
+
const value = parsePropertyValue(row.propertyValue);
|
|
4092
|
+
const type = row.easDataType || 'string';
|
|
4093
|
+
return withLocalRevocation({
|
|
4094
|
+
id: row.uid,
|
|
4095
|
+
decodedDataJson: JSON.stringify([{ value: { name, value, type } }]),
|
|
4096
|
+
refUID: row.versionUid,
|
|
4097
|
+
schemaId: row.schemaUid || '',
|
|
4098
|
+
timeCreated: msToUnixSeconds(row.attestationCreatedAt),
|
|
4099
|
+
attester: row.publisher ?? undefined,
|
|
4100
|
+
}, row.revokedAt);
|
|
4101
|
+
}
|
|
4102
|
+
async function resolveSchemaNameForSeedType(appDb, seedType,
|
|
4103
|
+
/** seeds.model_file_id: the seed's own model (model names are only unique per schema). */
|
|
4104
|
+
modelFileId) {
|
|
4105
|
+
if (!seedType)
|
|
4106
|
+
return null;
|
|
4107
|
+
const normalized = startCase(seedType);
|
|
4108
|
+
try {
|
|
4109
|
+
const schemaRows = await appDb
|
|
4110
|
+
.select({ schemaName: schemas.name })
|
|
4111
|
+
.from(models$1)
|
|
4112
|
+
.innerJoin(modelSchemas, eq(models$1.id, modelSchemas.modelId))
|
|
4113
|
+
.innerJoin(schemas, eq(modelSchemas.schemaId, schemas.id))
|
|
4114
|
+
.where(modelFileId ? eq(models$1.schemaFileId, modelFileId) : eq(models$1.name, normalized))
|
|
4115
|
+
.orderBy(desc(models$1.id))
|
|
4116
|
+
.limit(1);
|
|
4117
|
+
if (schemaRows[0]?.schemaName)
|
|
4118
|
+
return schemaRows[0].schemaName;
|
|
4119
|
+
}
|
|
4120
|
+
catch {
|
|
4121
|
+
// ignore
|
|
4122
|
+
}
|
|
4123
|
+
return seedType;
|
|
4124
|
+
}
|
|
4125
|
+
function monthBoundsUnix(year, month) {
|
|
4126
|
+
const startDate = new Date(year, month - 1, 1);
|
|
4127
|
+
const endDate = new Date(year, month, 0, 23, 59, 59);
|
|
4128
|
+
const startTs = Math.floor(startDate.getTime() / 1000);
|
|
4129
|
+
const endTs = Math.floor(endDate.getTime() / 1000) + 1;
|
|
4130
|
+
return { startTs, endTs };
|
|
4131
|
+
}
|
|
4132
|
+
async function listPublishedSeedRows(schemaName, opts) {
|
|
4133
|
+
const appDb = BaseDb.getAppDb();
|
|
4134
|
+
if (!appDb)
|
|
4135
|
+
return [];
|
|
4136
|
+
const snakeType = schemaName.includes('_')
|
|
4137
|
+
? schemaName
|
|
4138
|
+
: schemaName.replace(/([A-Z])/g, (m, i) => i === 0 ? m.toLowerCase() : `_${m.toLowerCase()}`);
|
|
4139
|
+
// Prefer exact type match (EAS schema names are usually lowercase singular)
|
|
4140
|
+
const typeCandidates = Array.from(new Set([schemaName, schemaName.toLowerCase(), snakeType.toLowerCase()]));
|
|
4141
|
+
const rows = await appDb
|
|
4142
|
+
.select({
|
|
4143
|
+
uid: seeds.uid,
|
|
4144
|
+
schemaUid: seeds.schemaUid,
|
|
4145
|
+
type: seeds.type,
|
|
4146
|
+
modelFileId: seeds.modelFileId,
|
|
4147
|
+
publisher: seeds.publisher,
|
|
4148
|
+
attestationRaw: seeds.attestationRaw,
|
|
4149
|
+
attestationCreatedAt: seeds.attestationCreatedAt,
|
|
4150
|
+
revokedAt: seeds.revokedAt,
|
|
4151
|
+
markedForDeletion: seeds._markedForDeletion,
|
|
4152
|
+
})
|
|
4153
|
+
.from(seeds)
|
|
4154
|
+
.where(and(inArray(seeds.type, typeCandidates), isNotNull(seeds.uid), or(isNull(seeds.revokedAt), eq(seeds.revokedAt, 0)), or(isNull(seeds._markedForDeletion), eq(seeds._markedForDeletion, 0))))
|
|
4155
|
+
.orderBy(desc(seeds.attestationCreatedAt));
|
|
4156
|
+
const out = [];
|
|
4157
|
+
for (const row of rows) {
|
|
4158
|
+
if (!row.uid || !isValidEasAttestationUid(row.uid))
|
|
4159
|
+
continue;
|
|
4160
|
+
// Must have at least one published version that is not revoked: the SDK's latest published
|
|
4161
|
+
// version skips revoked ones, so a seed whose versions were all revoked has none.
|
|
4162
|
+
const versionRows = await appDb
|
|
4163
|
+
.select({
|
|
4164
|
+
uid: versions.uid,
|
|
4165
|
+
revokedAt: versions.revokedAt,
|
|
4166
|
+
})
|
|
4167
|
+
.from(versions)
|
|
4168
|
+
.where(eq(versions.seedUid, row.uid))
|
|
4169
|
+
.orderBy(desc(versions.createdAt));
|
|
4170
|
+
const hasPublished = versionRows.some((v) => v.uid && isValidEasAttestationUid(v.uid) && !isRevokedLocally(v.revokedAt));
|
|
4171
|
+
if (!hasPublished)
|
|
4172
|
+
continue;
|
|
4173
|
+
const schemaResolved = await resolveSchemaNameForSeedType(appDb, row.type, row.modelFileId);
|
|
4174
|
+
const att = seedRowToAttestation({
|
|
4175
|
+
...row,
|
|
4176
|
+
schemaName: schemaResolved === schemaName ? schemaName : (schemaResolved ?? schemaName),
|
|
4177
|
+
});
|
|
4178
|
+
if (!att)
|
|
4179
|
+
continue;
|
|
4180
|
+
// Force requested schema name so assembleSeeds filters correctly
|
|
4181
|
+
att.schema = { schemaNames: [{ name: schemaName }] };
|
|
4182
|
+
if (opts?.startTs != null || opts?.endTs != null) {
|
|
4183
|
+
const t = att.timeCreated;
|
|
4184
|
+
if (opts.startTs != null && t < opts.startTs)
|
|
4185
|
+
continue;
|
|
4186
|
+
if (opts.endTs != null && t >= opts.endTs)
|
|
4187
|
+
continue;
|
|
4188
|
+
}
|
|
4189
|
+
out.push(att);
|
|
4190
|
+
}
|
|
4191
|
+
return out;
|
|
4192
|
+
}
|
|
4193
|
+
/**
|
|
4194
|
+
* SDK local QueryDataSource: published Seeds/Versions/metadata from SQLite.
|
|
4195
|
+
*
|
|
4196
|
+
* Revocation follows the local `revoked_at` columns, not the stored `attestation_raw`: rows with
|
|
4197
|
+
* `revoked_at` set are left out (as the remote source leaves out revoked attestations) and returned
|
|
4198
|
+
* attestations carry `revoked`/`revocationTime` from those columns.
|
|
4199
|
+
*/
|
|
4200
|
+
function createLocalQueryDataSource() {
|
|
4201
|
+
return {
|
|
4202
|
+
kind: 'local',
|
|
4203
|
+
async getSeedByUid(seedUid) {
|
|
4204
|
+
const appDb = BaseDb.getAppDb();
|
|
4205
|
+
if (!appDb || !seedUid)
|
|
4206
|
+
return null;
|
|
4207
|
+
const rows = await appDb
|
|
4208
|
+
.select({
|
|
4209
|
+
uid: seeds.uid,
|
|
4210
|
+
schemaUid: seeds.schemaUid,
|
|
4211
|
+
type: seeds.type,
|
|
4212
|
+
modelFileId: seeds.modelFileId,
|
|
4213
|
+
publisher: seeds.publisher,
|
|
4214
|
+
attestationRaw: seeds.attestationRaw,
|
|
4215
|
+
attestationCreatedAt: seeds.attestationCreatedAt,
|
|
4216
|
+
revokedAt: seeds.revokedAt,
|
|
4217
|
+
})
|
|
4218
|
+
.from(seeds)
|
|
4219
|
+
.where(eq(seeds.uid, seedUid))
|
|
4220
|
+
.limit(1);
|
|
4221
|
+
const row = rows[0];
|
|
4222
|
+
if (!row?.uid || !isValidEasAttestationUid(row.uid))
|
|
4223
|
+
return null;
|
|
4224
|
+
if (isRevokedLocally(row.revokedAt))
|
|
4225
|
+
return null;
|
|
4226
|
+
const schemaName = await resolveSchemaNameForSeedType(appDb, row.type, row.modelFileId);
|
|
4227
|
+
return seedRowToAttestation({ ...row, schemaName });
|
|
4228
|
+
},
|
|
4229
|
+
async listSeedsBySchemaName(schemaName, opts) {
|
|
4230
|
+
const all = await listPublishedSeedRows(schemaName);
|
|
4231
|
+
return all.slice(opts.skip, opts.skip + opts.limit);
|
|
4232
|
+
},
|
|
4233
|
+
async listSeedsBySchemaNameForMonth(schemaName, year, month) {
|
|
4234
|
+
const { startTs, endTs } = monthBoundsUnix(year, month);
|
|
4235
|
+
return listPublishedSeedRows(schemaName, { startTs, endTs });
|
|
4236
|
+
},
|
|
4237
|
+
async getVersionsForSeed(seedUid) {
|
|
4238
|
+
return this.getVersionsForSeeds([seedUid]);
|
|
4239
|
+
},
|
|
4240
|
+
async getVersionsForSeeds(seedUids, opts) {
|
|
4241
|
+
const appDb = BaseDb.getAppDb();
|
|
4242
|
+
if (!appDb || seedUids.length === 0)
|
|
4243
|
+
return [];
|
|
4244
|
+
const rows = await appDb
|
|
4245
|
+
.select({
|
|
4246
|
+
uid: versions.uid,
|
|
4247
|
+
seedUid: versions.seedUid,
|
|
4248
|
+
publisher: versions.publisher,
|
|
4249
|
+
attestationRaw: versions.attestationRaw,
|
|
4250
|
+
attestationCreatedAt: versions.attestationCreatedAt,
|
|
4251
|
+
revokedAt: versions.revokedAt,
|
|
4252
|
+
})
|
|
4253
|
+
.from(versions)
|
|
4254
|
+
.where(inArray(versions.seedUid, seedUids));
|
|
4255
|
+
// Like the remote source (EAS queries exclude revoked attestations by default; with
|
|
4256
|
+
// includeRevoked they come back marked `revoked`).
|
|
4257
|
+
const out = [];
|
|
4258
|
+
for (const row of rows) {
|
|
4259
|
+
if (isRevokedLocally(row.revokedAt) && !opts?.includeRevoked)
|
|
4260
|
+
continue;
|
|
4261
|
+
const att = versionRowToAttestation(row);
|
|
4262
|
+
if (att)
|
|
4263
|
+
out.push(att);
|
|
4264
|
+
}
|
|
4265
|
+
return out;
|
|
4266
|
+
},
|
|
4267
|
+
async getPropertiesForVersionUids(versionUids) {
|
|
4268
|
+
const appDb = BaseDb.getAppDb();
|
|
4269
|
+
if (!appDb || versionUids.length === 0)
|
|
4270
|
+
return [];
|
|
4271
|
+
const rows = await appDb
|
|
4272
|
+
.select({
|
|
4273
|
+
uid: metadata$1.uid,
|
|
4274
|
+
schemaUid: metadata$1.schemaUid,
|
|
4275
|
+
propertyName: metadata$1.propertyName,
|
|
4276
|
+
propertyValue: metadata$1.propertyValue,
|
|
4277
|
+
easDataType: metadata$1.easDataType,
|
|
4278
|
+
versionUid: metadata$1.versionUid,
|
|
4279
|
+
publisher: metadata$1.publisher,
|
|
4280
|
+
attestationRaw: metadata$1.attestationRaw,
|
|
4281
|
+
attestationCreatedAt: metadata$1.attestationCreatedAt,
|
|
4282
|
+
revokedAt: metadata$1.revokedAt,
|
|
4283
|
+
})
|
|
4284
|
+
.from(metadata$1)
|
|
4285
|
+
.where(inArray(metadata$1.versionUid, versionUids));
|
|
4286
|
+
// Like the remote source, revoked property attestations are left out, so the query layer's
|
|
4287
|
+
// canonical pick (newest live per version and schema) matches the SDK's live-first readers.
|
|
4288
|
+
const out = [];
|
|
4289
|
+
for (const row of rows) {
|
|
4290
|
+
if (isRevokedLocally(row.revokedAt))
|
|
4291
|
+
continue;
|
|
4292
|
+
const att = metadataRowToAttestation(row);
|
|
4293
|
+
if (att)
|
|
4294
|
+
out.push(att);
|
|
4295
|
+
}
|
|
4296
|
+
return out;
|
|
4297
|
+
},
|
|
4298
|
+
async getSeedsByUids(uids) {
|
|
4299
|
+
const appDb = BaseDb.getAppDb();
|
|
4300
|
+
if (!appDb || uids.length === 0)
|
|
4301
|
+
return [];
|
|
4302
|
+
const rows = await appDb
|
|
4303
|
+
.select({
|
|
4304
|
+
uid: seeds.uid,
|
|
4305
|
+
schemaUid: seeds.schemaUid,
|
|
4306
|
+
type: seeds.type,
|
|
4307
|
+
modelFileId: seeds.modelFileId,
|
|
4308
|
+
publisher: seeds.publisher,
|
|
4309
|
+
attestationRaw: seeds.attestationRaw,
|
|
4310
|
+
attestationCreatedAt: seeds.attestationCreatedAt,
|
|
4311
|
+
revokedAt: seeds.revokedAt,
|
|
4312
|
+
})
|
|
4313
|
+
.from(seeds)
|
|
4314
|
+
.where(inArray(seeds.uid, uids));
|
|
4315
|
+
const out = [];
|
|
4316
|
+
for (const row of rows) {
|
|
4317
|
+
if (isRevokedLocally(row.revokedAt))
|
|
4318
|
+
continue;
|
|
4319
|
+
const schemaName = await resolveSchemaNameForSeedType(appDb, row.type, row.modelFileId);
|
|
4320
|
+
const att = seedRowToAttestation({ ...row, schemaName });
|
|
4321
|
+
if (att)
|
|
4322
|
+
out.push(att);
|
|
4323
|
+
}
|
|
4324
|
+
return out;
|
|
4325
|
+
},
|
|
4326
|
+
async readStorageBody(ref) {
|
|
4327
|
+
try {
|
|
4328
|
+
if (ref.localPathHint) {
|
|
4329
|
+
const text = await BaseFileManager.readFileAsString(ref.localPathHint);
|
|
4330
|
+
if (text)
|
|
4331
|
+
return text;
|
|
4332
|
+
}
|
|
4333
|
+
// Common layout: html/{name}.html under filesDir
|
|
4334
|
+
if (typeof ref.value === 'string') {
|
|
4335
|
+
const v = ref.value.trim();
|
|
4336
|
+
// Absolute or relative file path already on disk
|
|
4337
|
+
if (v.includes('/') && !v.startsWith('http')) {
|
|
4338
|
+
try {
|
|
4339
|
+
const text = await BaseFileManager.readFileAsString(v);
|
|
4340
|
+
if (text)
|
|
4341
|
+
return text;
|
|
4342
|
+
}
|
|
4343
|
+
catch {
|
|
4344
|
+
// try under html/
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
const htmlPath = BaseFileManager.getFilesPath('html', v.endsWith('.html') ? v : `${v}.html`);
|
|
4348
|
+
try {
|
|
4349
|
+
const text = await BaseFileManager.readFileAsString(htmlPath);
|
|
4350
|
+
if (text)
|
|
4351
|
+
return text;
|
|
4352
|
+
}
|
|
4353
|
+
catch {
|
|
4354
|
+
// fall through
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4358
|
+
catch {
|
|
4359
|
+
return null;
|
|
4360
|
+
}
|
|
4361
|
+
return null;
|
|
4362
|
+
},
|
|
4363
|
+
};
|
|
4364
|
+
}
|
|
4365
|
+
|
|
4366
|
+
let registered = false;
|
|
4367
|
+
/**
|
|
4368
|
+
* Register the SDK SQLite/files adapter with `@seedprotocol/query`.
|
|
4369
|
+
* Safe to call multiple times; re-registers with a fresh adapter instance.
|
|
4370
|
+
* No-op when the app DB is not ready yet (unless `force`).
|
|
4371
|
+
*/
|
|
4372
|
+
function registerSeedQueryLocalSource(options) {
|
|
4373
|
+
if (!options?.force && !BaseDb.isAppDbReady()) {
|
|
4374
|
+
return false;
|
|
4375
|
+
}
|
|
4376
|
+
try {
|
|
4377
|
+
registerLocalQuerySource(createLocalQueryDataSource());
|
|
4378
|
+
registered = true;
|
|
4379
|
+
return true;
|
|
4380
|
+
}
|
|
4381
|
+
catch (err) {
|
|
4382
|
+
console.warn('[sdk/query] registerSeedQueryLocalSource failed:', err);
|
|
4383
|
+
return false;
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
/** Unregister local query source (tests / client unload). */
|
|
4387
|
+
function unregisterSeedQueryLocalSource() {
|
|
4388
|
+
clearLocalQuerySource();
|
|
4389
|
+
registered = false;
|
|
4390
|
+
}
|
|
4391
|
+
function isSeedQueryLocalSourceRegistered() {
|
|
4392
|
+
return registered;
|
|
4393
|
+
}
|
|
4394
|
+
/**
|
|
4395
|
+
* Thin published-JSON helper: `getSeed` via the local (or auto) query source.
|
|
4396
|
+
* Does not touch Item / drafts / liveQuery.
|
|
4397
|
+
*
|
|
4398
|
+
* Prefer this (or `@seedprotocol/query` `getSeed` / `queryBySchema`) for
|
|
4399
|
+
* canonical published Seed JSON. Use `getItemData` / Item for draft-head authoring.
|
|
4400
|
+
*/
|
|
4401
|
+
async function getPublishedSeedRecord(seedUid, options) {
|
|
4402
|
+
if (!registered) {
|
|
4403
|
+
registerSeedQueryLocalSource();
|
|
4404
|
+
}
|
|
4405
|
+
const source = options?.source ?? 'local';
|
|
4406
|
+
return getSeed(seedUid, { ...options, source });
|
|
4407
|
+
}
|
|
4408
|
+
|
|
4409
|
+
const emptyResult$1 = () => ({
|
|
4410
|
+
removedSeedLocalIds: [],
|
|
4411
|
+
removedSeedUids: [],
|
|
4412
|
+
});
|
|
4413
|
+
function normalizeAddresses(addresses) {
|
|
4414
|
+
return [
|
|
4415
|
+
...new Set(addresses
|
|
4416
|
+
.filter((a) => typeof a === 'string' && a.length > 0)
|
|
4417
|
+
.map((a) => a.toLowerCase())),
|
|
4418
|
+
];
|
|
4419
|
+
}
|
|
4420
|
+
/**
|
|
4421
|
+
* Find local on-chain seed copies for the given publisher addresses (no delete).
|
|
4422
|
+
* Drafts (no uid and no attestationRaw) are excluded.
|
|
4423
|
+
*/
|
|
4424
|
+
async function findLocalOnchainCopiesForAddresses(addresses) {
|
|
4425
|
+
const normalized = normalizeAddresses(addresses);
|
|
4426
|
+
if (normalized.length === 0) {
|
|
4427
|
+
return emptyResult$1();
|
|
4428
|
+
}
|
|
4429
|
+
const appDb = BaseDb.getAppDb();
|
|
4430
|
+
if (!appDb) {
|
|
4431
|
+
return emptyResult$1();
|
|
4432
|
+
}
|
|
4433
|
+
const addressSet = new Set(normalized);
|
|
4434
|
+
const candidateRows = (await appDb
|
|
4435
|
+
.select({
|
|
4436
|
+
localId: seeds.localId,
|
|
4437
|
+
uid: seeds.uid,
|
|
4438
|
+
publisher: seeds.publisher,
|
|
4439
|
+
attestationRaw: seeds.attestationRaw,
|
|
4440
|
+
})
|
|
4441
|
+
.from(seeds)
|
|
4442
|
+
.where(or(isNotNull(seeds.uid), isNotNull(seeds.attestationRaw))));
|
|
4443
|
+
const matched = candidateRows.filter((row) => {
|
|
4444
|
+
if (!row.localId)
|
|
4445
|
+
return false;
|
|
4446
|
+
const publisher = resolvePublisherFromSeedRow(row);
|
|
4447
|
+
return publisher != null && addressSet.has(publisher.toLowerCase());
|
|
4448
|
+
});
|
|
4449
|
+
return {
|
|
4450
|
+
removedSeedLocalIds: matched.map((r) => r.localId).filter(Boolean),
|
|
4451
|
+
removedSeedUids: matched
|
|
4452
|
+
.map((r) => r.uid)
|
|
4453
|
+
.filter((u) => typeof u === 'string' && u.length > 0),
|
|
4454
|
+
};
|
|
4455
|
+
}
|
|
4456
|
+
/**
|
|
4457
|
+
* Hard-delete the given seed local ids and related local rows (cascade).
|
|
4458
|
+
* Does not touch schemas, app_state, or chain.
|
|
4459
|
+
*/
|
|
4460
|
+
async function hardDeleteLocalSeedsByLocalIds(seedLocalIds) {
|
|
4461
|
+
if (seedLocalIds.length === 0)
|
|
4462
|
+
return;
|
|
4463
|
+
const appDb = BaseDb.getAppDb();
|
|
4464
|
+
if (!appDb)
|
|
4465
|
+
return;
|
|
4466
|
+
await appDb
|
|
4467
|
+
.delete(htmlEmbeddedImageCoPublish)
|
|
4468
|
+
.where(or(inArray(htmlEmbeddedImageCoPublish.parentSeedLocalId, seedLocalIds), inArray(htmlEmbeddedImageCoPublish.htmlSeedLocalId, seedLocalIds), inArray(htmlEmbeddedImageCoPublish.imageSeedLocalId, seedLocalIds)));
|
|
4469
|
+
await appDb
|
|
4470
|
+
.delete(arweaveL1FinalizeJobs)
|
|
4471
|
+
.where(inArray(arweaveL1FinalizeJobs.seedLocalId, seedLocalIds));
|
|
4472
|
+
await appDb
|
|
4473
|
+
.delete(publishProcesses)
|
|
4474
|
+
.where(inArray(publishProcesses.seedLocalId, seedLocalIds));
|
|
4475
|
+
await appDb.delete(metadata$1).where(inArray(metadata$1.seedLocalId, seedLocalIds));
|
|
4476
|
+
await appDb.delete(versions).where(inArray(versions.seedLocalId, seedLocalIds));
|
|
4477
|
+
await appDb.delete(seeds).where(inArray(seeds.localId, seedLocalIds));
|
|
4478
|
+
}
|
|
4479
|
+
|
|
4480
|
+
const emptyResult = () => ({
|
|
4481
|
+
seeds: 0,
|
|
4482
|
+
versions: 0,
|
|
4483
|
+
metadata: 0,
|
|
4484
|
+
});
|
|
4485
|
+
/**
|
|
4486
|
+
* Stamp local seeds/versions/metadata that have no publisher and no real uid.
|
|
4487
|
+
* Does not touch sealed rows (real EAS uid or attestationRaw).
|
|
4488
|
+
*/
|
|
4489
|
+
async function claimUnpublishedDrafts(publisher) {
|
|
4490
|
+
const normalized = normalizePublisher(publisher);
|
|
4491
|
+
if (!normalized)
|
|
4492
|
+
return emptyResult();
|
|
4493
|
+
const appDb = BaseDb.getAppDb();
|
|
4494
|
+
if (!appDb)
|
|
4495
|
+
return emptyResult();
|
|
4496
|
+
const now = Date.now();
|
|
4497
|
+
const claimTable = async (table) => {
|
|
4498
|
+
const rows = (await appDb
|
|
4499
|
+
.select({
|
|
4500
|
+
localId: table.localId,
|
|
4501
|
+
uid: table.uid,
|
|
4502
|
+
publisher: table.publisher,
|
|
4503
|
+
attestationRaw: table.attestationRaw,
|
|
4504
|
+
})
|
|
4505
|
+
.from(table));
|
|
4506
|
+
const ids = rows
|
|
4507
|
+
.filter((row) => typeof row.localId === 'string' &&
|
|
4508
|
+
row.localId !== '' &&
|
|
4509
|
+
isLocalUnsealedDraft(row))
|
|
4510
|
+
.map((row) => row.localId);
|
|
4511
|
+
if (ids.length === 0)
|
|
4512
|
+
return 0;
|
|
4513
|
+
await appDb
|
|
4514
|
+
.update(table)
|
|
4515
|
+
.set({ publisher: normalized, updatedAt: now })
|
|
4516
|
+
.where(inArray(table.localId, ids));
|
|
4517
|
+
return ids.length;
|
|
4518
|
+
};
|
|
4519
|
+
const [seedCount, versionCount, metadataCount] = await Promise.all([
|
|
4520
|
+
claimTable(seeds),
|
|
4521
|
+
claimTable(versions),
|
|
4522
|
+
claimTable(metadata$1),
|
|
4523
|
+
]);
|
|
4524
|
+
return {
|
|
4525
|
+
seeds: seedCount,
|
|
4526
|
+
versions: versionCount,
|
|
4527
|
+
metadata: metadataCount,
|
|
4528
|
+
};
|
|
4529
|
+
}
|
|
4530
|
+
|
|
4531
|
+
const logger = debug('seedSdk:client');
|
|
4532
|
+
const clientManager = createActor(clientManagerMachine, {
|
|
4533
|
+
input: {
|
|
4534
|
+
isInitialized: false,
|
|
4535
|
+
addressesSet: false,
|
|
4536
|
+
isSaving: false,
|
|
4537
|
+
syncFromEasOnAddressChange: false,
|
|
4538
|
+
}
|
|
4539
|
+
});
|
|
4540
|
+
const subscription = clientManager.subscribe((snapshot) => {
|
|
4541
|
+
logger('ClientManager snapshot.value:', snapshot.value);
|
|
4542
|
+
logger('ClientManager snapshot.context.isInitialized:', snapshot.context.isInitialized);
|
|
4543
|
+
});
|
|
4544
|
+
clientManager.start();
|
|
4545
|
+
const ensureInitialized = () => {
|
|
4546
|
+
if (!clientManager.getSnapshot().context.isInitialized) {
|
|
4547
|
+
throw new Error(CLIENT_NOT_INITIALIZED);
|
|
4548
|
+
}
|
|
4549
|
+
};
|
|
4550
|
+
// Singleton instance - created once at module load time
|
|
4551
|
+
// ES modules cache exports, ensuring all imports get the same instance
|
|
4552
|
+
const clientInstance = {
|
|
4553
|
+
isInitialized: () => {
|
|
4554
|
+
return clientManager.getSnapshot().context.isInitialized;
|
|
4555
|
+
},
|
|
4556
|
+
getService: () => {
|
|
4557
|
+
// ensureInitialized();
|
|
4558
|
+
return clientManager;
|
|
4559
|
+
},
|
|
4560
|
+
init: createCoalescedAsync(async (options) => {
|
|
4561
|
+
// If the actor is stopped (e.g., from a previous failed test), restart it
|
|
4562
|
+
const snapshot = clientManager.getSnapshot();
|
|
4563
|
+
const currentState = snapshot.value;
|
|
4564
|
+
const isStopped = snapshot.status === 'stopped';
|
|
4565
|
+
if (isStopped) {
|
|
4566
|
+
logger('Client manager was stopped, restarting...');
|
|
4567
|
+
clientManager.start();
|
|
4568
|
+
// Wait for the actor to be ready
|
|
4569
|
+
try {
|
|
4570
|
+
await waitFor(clientManager, (snapshot) => snapshot.status !== 'stopped', { timeout: 1000 });
|
|
4571
|
+
}
|
|
4572
|
+
catch (e) {
|
|
4573
|
+
logger('Timeout waiting for actor to restart, continuing anyway...');
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4576
|
+
// Clear any stale context from previous failed initializations
|
|
4577
|
+
// This ensures we start with a clean slate before the new init updates it
|
|
4578
|
+
if (currentState !== 'uninitialized') {
|
|
4579
|
+
logger(`Client manager is in state ${currentState}, clearing stale context before re-init...`);
|
|
4580
|
+
clientManager.send({
|
|
4581
|
+
type: 'updateContext',
|
|
4582
|
+
context: {
|
|
4583
|
+
isInitialized: false,
|
|
4584
|
+
addressesSet: false,
|
|
4585
|
+
isSaving: false,
|
|
4586
|
+
syncFromEasOnAddressChange: false,
|
|
4587
|
+
endpoints: undefined,
|
|
4588
|
+
addresses: undefined,
|
|
4589
|
+
ownedAddresses: undefined,
|
|
4590
|
+
watchedAddresses: undefined,
|
|
4591
|
+
models: undefined,
|
|
4592
|
+
arweaveDomain: undefined,
|
|
4593
|
+
filesDir: undefined,
|
|
4594
|
+
dbConfig: undefined,
|
|
4595
|
+
initError: undefined,
|
|
4596
|
+
saveError: undefined,
|
|
4597
|
+
}
|
|
4598
|
+
});
|
|
4599
|
+
// Wait a moment for the context update to be processed
|
|
4600
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
4601
|
+
}
|
|
4602
|
+
// Send init event - the root-level handler will transition to PLATFORM_CLASSES_INIT
|
|
4603
|
+
// which will stop any running actors and start fresh with the new options
|
|
4604
|
+
// The platformClassesInit actor will update the context with the correct endpoints from options
|
|
4605
|
+
clientManager.send({ type: 'init', options });
|
|
4606
|
+
// Wait for the state machine to transition to PLATFORM_CLASSES_INIT to ensure
|
|
4607
|
+
// any old actors are stopped and the new initialization flow begins
|
|
4608
|
+
try {
|
|
4609
|
+
await waitFor(clientManager, (snapshot) => {
|
|
4610
|
+
return snapshot.value === 'platformClassesInit' || snapshot.context.isInitialized;
|
|
4611
|
+
}, { timeout: 1000 });
|
|
4612
|
+
}
|
|
4613
|
+
catch (e) {
|
|
4614
|
+
// If we can't transition, continue anyway - the waitFor below will catch any errors
|
|
4615
|
+
logger('Timeout waiting for state transition, continuing...');
|
|
4616
|
+
}
|
|
4617
|
+
try {
|
|
4618
|
+
await waitFor(clientManager, (snapshot) => {
|
|
4619
|
+
return snapshot.context.isInitialized || snapshot.context.initError != null;
|
|
4620
|
+
}, { timeout: 120000 });
|
|
4621
|
+
const initError = clientManager.getSnapshot().context.initError;
|
|
4622
|
+
if (initError) {
|
|
4623
|
+
throw initError instanceof Error ? initError : new Error(String(initError));
|
|
4624
|
+
}
|
|
4625
|
+
}
|
|
4626
|
+
catch (error) {
|
|
4627
|
+
// Ensure we never throw undefined
|
|
4628
|
+
if (error === undefined || error === null) {
|
|
4629
|
+
throw new Error('Initialization failed with undefined error');
|
|
4630
|
+
}
|
|
4631
|
+
throw error;
|
|
4632
|
+
}
|
|
4633
|
+
// Wire `@seedprotocol/query` local source once DB is ready
|
|
4634
|
+
registerSeedQueryLocalSource({ force: true });
|
|
4635
|
+
}, () => {
|
|
4636
|
+
const snapshot = clientManager.getSnapshot();
|
|
4637
|
+
return (snapshot.value === ClientManagerState.IDLE &&
|
|
4638
|
+
snapshot.context.isInitialized === true &&
|
|
4639
|
+
!snapshot.context.initError);
|
|
4640
|
+
}),
|
|
4641
|
+
setAddresses: async (addresses) => {
|
|
4642
|
+
ensureInitialized();
|
|
4643
|
+
logger('setAddresses', addresses);
|
|
4644
|
+
clientManager.send({ type: 'setAddresses', addresses });
|
|
4645
|
+
await waitFor(clientManager, (snapshot) => !snapshot.context.isSaving, { timeout: 10000 });
|
|
4646
|
+
const saveError = clientManager.getSnapshot().context.saveError;
|
|
4647
|
+
if (saveError) {
|
|
4648
|
+
throw saveError instanceof Error ? saveError : new Error(String(saveError));
|
|
4649
|
+
}
|
|
4650
|
+
logger('setAddresses success', addresses);
|
|
4651
|
+
},
|
|
4652
|
+
getAddresses: async () => {
|
|
4653
|
+
ensureInitialized();
|
|
4654
|
+
const owned = await getOwnedAddressesFromDb();
|
|
4655
|
+
const watched = await getWatchedAddressesFromDb();
|
|
4656
|
+
return { owned, watched };
|
|
4657
|
+
},
|
|
4658
|
+
getOwnedAddresses: async () => {
|
|
4659
|
+
ensureInitialized();
|
|
4660
|
+
return getOwnedAddressesFromDb();
|
|
4661
|
+
},
|
|
4662
|
+
getWatchedAddresses: async () => {
|
|
4663
|
+
ensureInitialized();
|
|
4664
|
+
return getWatchedAddressesFromDb();
|
|
4665
|
+
},
|
|
4666
|
+
/**
|
|
4667
|
+
* Stamp local seeds/versions/metadata that have no publisher and no real uid.
|
|
4668
|
+
* Sealed rows (real EAS uid or attestationRaw) are left unchanged.
|
|
4669
|
+
* Use after `setAddresses` on connect; `setPublishWallet` does this automatically.
|
|
4670
|
+
*/
|
|
4671
|
+
claimUnpublishedDrafts: async (publisher) => {
|
|
4672
|
+
ensureInitialized();
|
|
4673
|
+
return claimUnpublishedDrafts(publisher);
|
|
4674
|
+
},
|
|
4675
|
+
/**
|
|
4676
|
+
* Hard-delete local on-chain seed copies for the given publisher addresses.
|
|
4677
|
+
* Drafts (no uid / attestationRaw) are kept. Chain is untouched; reconnect can rehydrate via syncFromEas.
|
|
4678
|
+
*/
|
|
4679
|
+
removeLocalCopiesForAddresses: async (addresses) => {
|
|
4680
|
+
ensureInitialized();
|
|
4681
|
+
const list = Array.isArray(addresses) ? addresses : [];
|
|
4682
|
+
const matched = await findLocalOnchainCopiesForAddresses(list);
|
|
4683
|
+
const cacheIds = [
|
|
4684
|
+
...matched.removedSeedLocalIds,
|
|
4685
|
+
...matched.removedSeedUids,
|
|
4686
|
+
];
|
|
4687
|
+
if (cacheIds.length > 0) {
|
|
4688
|
+
Item.dropCachedInstancesForSeedIds(cacheIds);
|
|
4689
|
+
}
|
|
4690
|
+
await hardDeleteLocalSeedsByLocalIds(matched.removedSeedLocalIds);
|
|
4691
|
+
// Other tabs drop their cached instances of the removed seeds too.
|
|
4692
|
+
emitAcrossTabs(LOCAL_COPIES_REMOVED_EVENT, {
|
|
4693
|
+
addresses: list,
|
|
4694
|
+
removedSeedLocalIds: matched.removedSeedLocalIds,
|
|
4695
|
+
removedSeedUids: matched.removedSeedUids,
|
|
4696
|
+
});
|
|
4697
|
+
return matched;
|
|
4698
|
+
},
|
|
4699
|
+
/**
|
|
4700
|
+
* Syncs item attestations from EAS for the configured models and given addresses.
|
|
4701
|
+
* Uses owned + watched addresses from DB when addresses are not provided.
|
|
4702
|
+
*/
|
|
4703
|
+
syncFromEas: async (options) => {
|
|
4704
|
+
ensureInitialized();
|
|
4705
|
+
startEasSyncActor();
|
|
4706
|
+
const correlationId = typeof globalThis.crypto?.randomUUID === 'function'
|
|
4707
|
+
? globalThis.crypto.randomUUID()
|
|
4708
|
+
: `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
4709
|
+
sendEasSyncClientRequest(correlationId, options);
|
|
4710
|
+
await waitFor(easSyncActor, (snapshot) => snapshot.context.lastFinishedCorrelationIds?.includes(correlationId) ?? false);
|
|
4711
|
+
const snap = easSyncActor.getSnapshot();
|
|
4712
|
+
if (snap.context.lastFinishedStatus === 'failed') {
|
|
4713
|
+
throw new Error(snap.context.lastFinishedError ?? 'EAS sync failed');
|
|
4714
|
+
}
|
|
4715
|
+
},
|
|
4716
|
+
addModel: async (modelDef) => {
|
|
4717
|
+
const db = await BaseDb.getAppDb();
|
|
4718
|
+
const existingModels = await db.select().from(models$1).where(eq(models$1.name, modelDef.name));
|
|
4719
|
+
if (existingModels.length > 0) {
|
|
4720
|
+
return;
|
|
4721
|
+
}
|
|
4722
|
+
await db.insert(models$1).values({
|
|
4723
|
+
name: modelDef.name
|
|
4724
|
+
});
|
|
4725
|
+
// Global service removed - model registration is now handled by ClientManager
|
|
4726
|
+
// Models are added to DB during the ADD_MODELS_TO_DB state
|
|
4727
|
+
},
|
|
4728
|
+
onReady: (callback) => {
|
|
4729
|
+
const subscription = clientManager.subscribe((snapshot) => {
|
|
4730
|
+
if (snapshot.context.isInitialized) {
|
|
4731
|
+
subscription.unsubscribe();
|
|
4732
|
+
callback();
|
|
4733
|
+
}
|
|
4734
|
+
});
|
|
4735
|
+
},
|
|
4736
|
+
stop: () => {
|
|
4737
|
+
ensureInitialized();
|
|
4738
|
+
unregisterSeedQueryLocalSource();
|
|
4739
|
+
clientManager.stop();
|
|
4740
|
+
},
|
|
4741
|
+
unload: () => {
|
|
4742
|
+
ensureInitialized();
|
|
4743
|
+
unregisterSeedQueryLocalSource();
|
|
4744
|
+
clientManager.stop();
|
|
4745
|
+
subscription.unsubscribe();
|
|
4746
|
+
},
|
|
4747
|
+
};
|
|
4748
|
+
// Export the singleton instance
|
|
4749
|
+
// This ensures all imports across different files get the same instance
|
|
4750
|
+
const ClientManager = clientInstance;
|
|
4751
|
+
const getClient = () => {
|
|
4752
|
+
return clientInstance;
|
|
4753
|
+
};
|
|
4754
|
+
|
|
4755
|
+
var ClientManager$1 = /*#__PURE__*/Object.freeze({
|
|
4756
|
+
__proto__: null,
|
|
4757
|
+
ClientManager: ClientManager,
|
|
4758
|
+
clientManager: clientManager,
|
|
4759
|
+
getClient: getClient
|
|
4760
|
+
});
|
|
4761
|
+
|
|
4762
|
+
export { startEasSyncActor as A, unregisterSeedQueryLocalSource as B, ClientManager as C, createModelsFromJson as D, EAS_CHAIN_ID_APP_STATE_KEY as E, getLatestSchemaVersion as F, schema as G, ClientManager$1 as H, SEEDPROTOCOL_Seed_Protocol_v1 as S, assertLocalDbChain as a, getRefValueType as b, claimUnpublishedDrafts as c, createLocalQueryDataSource as d, createSchema as e, easSyncActor as f, getContentHash as g, getClient as h, importJsonSchema as i, getPublishedSeedRecord as j, getSchemaNameFromId as k, loadSchemaFromFile as l, isSeedQueryLocalSourceRegistered as m, listCompleteSchemaFiles as n, loadAllSchemasFromDb as o, mergeEasSyncRequestIntents as p, readSchema as q, readJsonImportFile as r, syncSchemaFromSource as s, transformImportToSchemaFile as t, registerSeedQueryLocalSource as u, requestEasSyncFromAddressChange as v, waitForEasReadChain as w, requestEasSyncFromEventBus as x, requestEasSyncFromModelsInit as y, sendEasSyncClientRequest as z };
|
|
4763
|
+
//# sourceMappingURL=ClientManager-DngaZdw6.js.map
|