@seedprotocol/sdk 0.7.10 → 0.7.11-next.1396.g883ef47
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/README.md +1 -1
- package/dist/{BaseFileManager-DLou1TZS.js → BaseFileManager-D-pLXQAp.js} +2 -14
- package/dist/BaseFileManager-D-pLXQAp.js.map +1 -0
- package/dist/{Item-oPnmIehk.js → Item-BU9wIT7l.js} +2053 -412
- package/dist/Item-BU9wIT7l.js.map +1 -0
- package/dist/{ModelProperty-D2vJozmW.js → ModelProperty-Bv7KzP97.js} +40 -38
- package/dist/ModelProperty-Bv7KzP97.js.map +1 -0
- package/dist/{Schema-BMXRF4j9.js → Schema-C_4wv0RP.js} +22 -22
- package/dist/{Schema-BMXRF4j9.js.map → Schema-C_4wv0RP.js.map} +1 -1
- package/dist/{SchemaValidationService-CieGrj5c.js → SchemaValidationService-Dc-lpFQX.js} +9 -7
- package/dist/{SchemaValidationService-CieGrj5c.js.map → SchemaValidationService-Dc-lpFQX.js.map} +1 -1
- package/dist/arweaveContentTags-iW_HcWJA.js +1456 -0
- package/dist/arweaveContentTags-iW_HcWJA.js.map +1 -0
- package/dist/{configurePlatform-WkmvqeTO.js → configurePlatform-BH1YOZpg.js} +2 -2
- package/dist/{configurePlatform-WkmvqeTO.js.map → configurePlatform-BH1YOZpg.js.map} +1 -1
- package/dist/db/drizzle/0030_drop_property_uids.sql +1 -0
- package/dist/db/drizzle/meta/0030_snapshot.json +1638 -0
- package/dist/db/drizzle/meta/_journal.json +8 -1
- package/dist/{detachRowsBeforeDelete-BZr_0iZU.js → detachRowsBeforeDelete-Cie55SoG.js} +4 -6
- package/dist/detachRowsBeforeDelete-Cie55SoG.js.map +1 -0
- package/dist/dist/{BaseFileManager-DLou1TZS.d.ts → BaseFileManager-D-pLXQAp.d.ts} +432 -495
- package/dist/dist/BaseFileManager-D-pLXQAp.d.ts.map +1 -0
- package/dist/dist/{configurePlatform-WkmvqeTO.d.ts → configurePlatform-BH1YOZpg.d.ts} +1 -1
- package/dist/dist/configurePlatform-BH1YOZpg.d.ts.map +1 -0
- package/dist/dist/{index-BjWk0x4-.d.ts → index-2PaIhvOU.d.ts} +1 -64
- package/dist/dist/{index-BjWk0x4-.d.ts.map → index-2PaIhvOU.d.ts.map} +1 -1
- package/dist/dist/{index.node-BfiBriEq.d.ts → index.node-C3rI1upv.d.ts} +25 -281
- package/dist/dist/{index.node-BfiBriEq.d.ts.map → index.node-C3rI1upv.d.ts.map} +1 -1
- package/dist/dist/minimumUpgradeVersion-Dg2MguFf.d.ts +20 -0
- package/dist/dist/minimumUpgradeVersion-Dg2MguFf.d.ts.map +1 -0
- package/dist/dist/platform.node.d.ts +2 -2
- package/dist/download-DHWUPLlK.js +393 -0
- package/dist/download-DHWUPLlK.js.map +1 -0
- package/dist/{eas-FZPPqxqe.js → eas-MXEdKKVc.js} +12 -10
- package/dist/{eas-FZPPqxqe.js.map → eas-MXEdKKVc.js.map} +1 -1
- package/dist/ensureImageLocal-B6suz6FC.js +175 -0
- package/dist/ensureImageLocal-B6suz6FC.js.map +1 -0
- package/dist/{getPublishPayload-DKuEaq96.js → getPublishPayload-Dd3HCYeI.js} +8 -49
- package/dist/getPublishPayload-Dd3HCYeI.js.map +1 -0
- package/dist/{getPublishPendingDiff-CH5xLMjU.js → getPublishPendingDiff-kcTP52Kj.js} +13 -12
- package/dist/{getPublishPendingDiff-CH5xLMjU.js.map → getPublishPendingDiff-kcTP52Kj.js.map} +1 -1
- package/dist/{index-BjWk0x4-.js → index-2PaIhvOU.js} +2 -4
- package/dist/index-2PaIhvOU.js.map +1 -0
- package/dist/index-BXTyvnKb.js +28 -0
- package/dist/index-BXTyvnKb.js.map +1 -0
- package/dist/{index-DXRhFg1N.js → index-CmNqUKQN.js} +10 -8
- package/dist/index-CmNqUKQN.js.map +1 -0
- package/dist/{index-Gh42_47r.js → index-Xs_EDi-_.js} +18 -401
- package/dist/index-Xs_EDi-_.js.map +1 -0
- package/dist/{index.browser-DQstVvRV.js → index.browser-BkcOQGhw.js} +59 -102
- package/dist/index.browser-BkcOQGhw.js.map +1 -0
- package/dist/{index.node-BfiBriEq.js → index.node-C3rI1upv.js} +14 -36
- package/dist/index.node-C3rI1upv.js.map +1 -0
- package/dist/main.js +19 -19
- package/dist/{minimumUpgradeVersion-BDhHvHtr.js → minimumUpgradeVersion-Dg2MguFf.js} +38 -9
- package/dist/minimumUpgradeVersion-Dg2MguFf.js.map +1 -0
- package/dist/{modelPropertyRecordSchemas-DEe-lYhn.js → modelPropertyRecordSchemas-CEac3FB8.js} +10 -10
- package/dist/{modelPropertyRecordSchemas-DEe-lYhn.js.map → modelPropertyRecordSchemas-CEac3FB8.js.map} +1 -1
- package/dist/node.js +14 -12
- package/dist/node.js.map +1 -1
- package/dist/platform.browser.js +10 -12
- package/dist/platform.browser.js.map +1 -1
- package/dist/platform.node.js +5 -5
- package/dist/{property-iNW1N3vm.js → property-4jSqHieb.js} +9 -9
- package/dist/{property-iNW1N3vm.js.map → property-4jSqHieb.js.map} +1 -1
- package/dist/{errors-CnT0fSMK.js → publishErrors-DZXDX3B6.js} +1830 -245
- package/dist/publishErrors-DZXDX3B6.js.map +1 -0
- package/dist/{readHtmlBodyForStorageSeed-B7HgPmq6.js → readHtmlBodyForStorageSeed-Cva1DPzE.js} +10 -12
- package/dist/{readHtmlBodyForStorageSeed-B7HgPmq6.js.map → readHtmlBodyForStorageSeed-Cva1DPzE.js.map} +1 -1
- package/dist/seedSchema/index.ts +0 -1
- package/dist/src/Item/Item.d.ts.map +1 -1
- package/dist/src/ItemProperty/ItemProperty.d.ts +1 -0
- package/dist/src/ItemProperty/ItemProperty.d.ts.map +1 -1
- package/dist/src/ItemProperty/service/actors/initialize.d.ts.map +1 -1
- package/dist/src/Model/index.d.ts +0 -2
- package/dist/src/Model/index.d.ts.map +1 -1
- package/dist/src/browser/db/Db.d.ts.map +1 -1
- package/dist/src/browser/db/drizzleFiles.d.ts +3 -2
- package/dist/src/browser/db/drizzleFiles.d.ts.map +1 -1
- package/dist/src/browser/helpers/FileManager.d.ts +1 -3
- package/dist/src/browser/helpers/FileManager.d.ts.map +1 -1
- package/dist/src/browser/workers/FileDownloader.d.ts +1 -0
- package/dist/src/browser/workers/FileDownloader.d.ts.map +1 -1
- package/dist/src/browser/workers/ImageResizer.d.ts +1 -0
- package/dist/src/browser/workers/ImageResizer.d.ts.map +1 -1
- package/dist/src/client/actors/addModelsToDb.d.ts.map +1 -1
- package/dist/src/client/actors/saveAppState.d.ts.map +1 -1
- package/dist/src/client/actors/saveConfig.d.ts.map +1 -1
- package/dist/src/db/minimumUpgradeVersion.d.ts +20 -5
- package/dist/src/db/minimumUpgradeVersion.d.ts.map +1 -1
- package/dist/src/db/read/metadataProperty.d.ts +1 -1
- package/dist/src/db/read/metadataProperty.d.ts.map +1 -1
- package/dist/src/db/read/resolveModelRecord.d.ts +0 -20
- package/dist/src/db/read/resolveModelRecord.d.ts.map +1 -1
- package/dist/src/db/write/createMetadata.d.ts +1 -8
- package/dist/src/db/write/createMetadata.d.ts.map +1 -1
- package/dist/src/db/write/removeLocalCopiesForAddresses.d.ts +0 -5
- package/dist/src/db/write/removeLocalCopiesForAddresses.d.ts.map +1 -1
- package/dist/src/db/write/updateItemPropertyValue.d.ts.map +1 -1
- package/dist/src/events/files/download.d.ts.map +1 -1
- package/dist/src/events/item/syncDbWithEas.d.ts.map +1 -1
- package/dist/src/helpers/easPropertyCanonical.d.ts +1 -1
- package/dist/src/helpers/easPropertyCanonical.d.ts.map +1 -1
- package/dist/src/helpers/entity/detachRowsBeforeDelete.d.ts +2 -3
- package/dist/src/helpers/entity/detachRowsBeforeDelete.d.ts.map +1 -1
- package/dist/src/helpers/entity/entityLifecycle.d.ts +0 -2
- package/dist/src/helpers/entity/entityLifecycle.d.ts.map +1 -1
- package/dist/src/helpers/environment.d.ts +0 -13
- package/dist/src/helpers/environment.d.ts.map +1 -1
- package/dist/src/helpers/index.d.ts +0 -4
- package/dist/src/helpers/index.d.ts.map +1 -1
- package/dist/src/helpers/itemStorageFile.d.ts +0 -11
- package/dist/src/helpers/itemStorageFile.d.ts.map +1 -1
- package/dist/src/helpers/propertySchemaUid.d.ts +7 -5
- package/dist/src/helpers/propertySchemaUid.d.ts.map +1 -1
- package/dist/src/helpers/storageDir.d.ts +0 -2
- package/dist/src/helpers/storageDir.d.ts.map +1 -1
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/node/db/Db.d.ts +0 -2
- package/dist/src/node/db/Db.d.ts.map +1 -1
- package/dist/src/node/helpers/FileManager.d.ts +0 -2
- package/dist/src/node/helpers/FileManager.d.ts.map +1 -1
- package/dist/src/node/index.d.ts +11 -3
- package/dist/src/node/index.d.ts.map +1 -1
- package/dist/src/outbox/index.d.ts +5 -5
- package/dist/src/outbox/index.d.ts.map +1 -1
- package/dist/src/seedSchema/index.d.ts +0 -1
- package/dist/src/seedSchema/index.d.ts.map +1 -1
- package/dist/{syncDbWithEas-FuyqoEKk.js → syncDbWithEas-V_9gIyHp.js} +25 -23
- package/dist/syncDbWithEas-V_9gIyHp.js.map +1 -0
- package/dist/{tolerantWebAccess-zEseJ2zT.js → tolerantWebAccess-D6hFxREe.js} +11 -13
- package/dist/{tolerantWebAccess-zEseJ2zT.js.map → tolerantWebAccess-D6hFxREe.js.map} +1 -1
- package/dist/{updateSeedRevokedAt-B9JRwr5T.js → updateSeedRevokedAt-CsAoUEEt.js} +4 -3
- package/dist/{updateSeedRevokedAt-B9JRwr5T.js.map → updateSeedRevokedAt-CsAoUEEt.js.map} +1 -1
- package/package.json +7 -12
- package/dist/BaseFileManager-DLou1TZS.js.map +0 -1
- package/dist/Item-oPnmIehk.js.map +0 -1
- package/dist/ModelProperty-D2vJozmW.js.map +0 -1
- package/dist/detachRowsBeforeDelete-BZr_0iZU.js.map +0 -1
- package/dist/dist/BaseFileManager-DLou1TZS.d.ts.map +0 -1
- package/dist/dist/configurePlatform-WkmvqeTO.d.ts.map +0 -1
- package/dist/dist/minimumUpgradeVersion-BDhHvHtr.d.ts +0 -9
- package/dist/dist/minimumUpgradeVersion-BDhHvHtr.d.ts.map +0 -1
- package/dist/errors-CnT0fSMK.js.map +0 -1
- package/dist/getPublishPayload-DKuEaq96.js.map +0 -1
- package/dist/getPublishUploads-DivYpgNw.js +0 -783
- package/dist/getPublishUploads-DivYpgNw.js.map +0 -1
- package/dist/index-BjWk0x4-.js.map +0 -1
- package/dist/index-CZMWKXLM.js +0 -27
- package/dist/index-CZMWKXLM.js.map +0 -1
- package/dist/index-DXRhFg1N.js.map +0 -1
- package/dist/index-Gh42_47r.js.map +0 -1
- package/dist/index.browser-DQstVvRV.js.map +0 -1
- package/dist/index.node-BfiBriEq.js.map +0 -1
- package/dist/minimumUpgradeVersion-BDhHvHtr.js.map +0 -1
- package/dist/publishGraph-CRn_N3NT.js +0 -311
- package/dist/publishGraph-CRn_N3NT.js.map +0 -1
- package/dist/seedSchema/PropertyUidSchema.ts +0 -16
- package/dist/src/db/read/getItemProperty.d.ts +0 -6
- package/dist/src/db/read/getItemProperty.d.ts.map +0 -1
- package/dist/src/db/read/isSeedRevoked.d.ts +0 -7
- package/dist/src/db/read/isSeedRevoked.d.ts.map +0 -1
- package/dist/src/db/write/saveMetadata.d.ts +0 -3
- package/dist/src/db/write/saveMetadata.d.ts.map +0 -1
- package/dist/src/db/write/updateMetadata.d.ts +0 -13
- package/dist/src/db/write/updateMetadata.d.ts.map +0 -1
- package/dist/src/seedSchema/PropertyUidSchema.d.ts +0 -64
- package/dist/src/seedSchema/PropertyUidSchema.d.ts.map +0 -1
- package/dist/src/types/fileManager.d.ts +0 -13
- package/dist/src/types/fileManager.d.ts.map +0 -1
- package/dist/syncDbWithEas-FuyqoEKk.js.map +0 -1
- package/dist/tabEvents-DT5ez6io.js +0 -3830
- package/dist/tabEvents-DT5ez6io.js.map +0 -1
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { fromCallback, setup, assign, createActor, waitFor, fromPromise } from 'xstate';
|
|
2
|
-
import {
|
|
2
|
+
import { as as isActorStopped, N as applyContextUpdate, P as isInternalUpdate, at as differsFromOriginals, bd as writeModelToDb, be as writePropertyToDb, U as isEntityTornDown, ax as onEntityTeardown, Q as evictionLog, V as sameEntity, W as DEFAULT_READY_TIMEOUT_MS, X as createReactiveProxy, Y as runEntityLoad, Z as waitForEntityReady, A as AmbiguousModelError, E as EntityUnloadedError, $ as entityStatus, I as Item, _ as evictEntities, a1 as releaseEntity, a2 as destroyEntity, bf as currentNameMatchLevels, bg as renameModelInDb, ag as modelPropertiesToObject, a6 as EntityCache, a3 as isClientReady, aM as MachineIds, a$ as normalizeAddressConfig, af as nameMatchLevels, aa as loadNameMatchLevels, a0 as inDefinitionsTransaction, bh as insertModelStubs, b0 as normalizeAddressList, bi as warnOnInvalidNameMatch, a4 as upsertSchemaRow, bj as refreshCachedProperties, ai as easPropertyNameForPublish, aJ as findSeedRow, a7 as normalizePublisher, aK as resolvePublisherFromSeedRow, an as matchMetadataProperty, ao as metadataRowsOfProperty, ap as METADATA_LATEST_FIRST_ORDER_SQL, bk as resolveMetadataProperty, aj as metadataStorageName, bl as lookupPropertySchemaUid, K as getItemStorageSubdir, bm as getItemStorageLocalFileName, bn as deleteStagedFile, bo as storageFileName, bp as STORAGE_SUBDIR, bq as resolveNewSeedFields, br as stageStorageContent, bs as requireStagedFile, r as registerOutboxApplier, k as outboxOpValue, bt as findSetConflict, bu as recordPropertyEdit, d as OutboxOpDeferredError, bv as startOutbox, e as OutboxOpFailedError, q as OwnershipError, u as isItemOwned, D as compareMetadataRowsLatestFirst, bw as createSeed, bx as storageValueRowOf, by as getLatestVersionRow, aI as parseListPropertyValueFromStorage, aL as ClientManagerState, bz as ClientManagerEvents, aY as isLocalUnsealedDraft, J as writeInBatches, ae as selectInBatches, bA as getWatchedAddressesFromDb, bB as getOwnedAddressesFromDb, bC as setEntityUnloadGraceMs, bD as stopOutbox, bE as evictAllEntities, bF as registerClientActor, aF as isVersionRevoked, bG as getPropertyDefinitions, aH as readModelMetadataProperties, bH as metadataPropertyNameForms, bI as currentPropertyRow, H as getLatestPublishedVersionRow, au as resolveModelRecord, bJ as resolveModelRecordByNameOrType, v as isPublishedSeedRef, bK as storageSeedTransactionId, bL as storageUploadPath, bM as storedValueKind, bN as resolveStorageDisplay } from './Item-BU9wIT7l.js';
|
|
3
3
|
import debug from 'debug';
|
|
4
|
-
import { B as BaseDb, l as properties, d as models$1, j as modelSchemas,
|
|
5
|
-
import {
|
|
6
|
-
import { seedGatewayConfigFromSeedConfig, setPreferredArweaveReadBaseUrls, resolveSeedGatewayEndpoints, setResolvedSeedGatewayEndpoints, BaseArweaveClient, ensureReadGatewaySelected, DEFAULT_ARWEAVE_HOST } from '@seedprotocol/arweave';
|
|
4
|
+
import { B as BaseDb, l as properties, d as models$1, j as modelSchemas, q as schemas, f as easSyncProcesses, a as appState, r as BaseFileManager, b as modelUids, s as seeds, v as versions, m as metadata$1, p as pendingWrites, h as htmlEmbeddedImageCoPublish, e as arweaveL1FinalizeJobs, o as publishProcesses } from './BaseFileManager-D-pLXQAp.js';
|
|
5
|
+
import { seedGatewayConfigFromSeedConfig, setPreferredArweaveReadBaseUrls, resolveSeedGatewayEndpoints, setResolvedSeedGatewayEndpoints, BaseArweaveClient, ensureReadGatewaySelected, DEFAULT_ARWEAVE_HOST, normalizeRelationPropertyValue, resolveSeedIdsFromRefString, getPublicArweaveRawUrl } from '@seedprotocol/arweave';
|
|
7
6
|
import 'pluralize';
|
|
8
|
-
import { localNamesInBestMatchTier, configureEasReadChain, matchEasModelSchemas, whenEasReadChainSettled, getEasReadChainId, DEFAULT_EAS_CHAIN_ID, isEasReadChainConfigured, easName, isValidEasAttestationUid } from '@seedprotocol/eas';
|
|
7
|
+
import { localNamesInBestMatchTier, configureEasReadChain, matchEasModelSchemas, whenEasReadChainSettled, getEasReadChainId, DEFAULT_EAS_CHAIN_ID, isEasReadChainConfigured, easName, isValidEasAttestationUid, getEasChainDeployment, getEasAttestationExplorerUrl } from '@seedprotocol/eas';
|
|
8
|
+
import { k as generateId, l as isInternalSchema, a as TProperty, Q as withDbTabLock, H as isLeaderTab, a2 as postTabMessage, a3 as onTabMessage, O as whenLeaderTab, a4 as listenForOtherTabsEvents, o as isBrowser, q as isNode, a5 as startTabCoordination, Y as withSeedDbLock, p as emitAcrossTabs, A as ADDRESSES_PERSISTED_EVENT, g as normalizePropertyRecordSchema, M as ModelPropertyDataTypes, n as normalizeDataType, a6 as tryWithTabLock, a7 as notifyFileSaved, I as ImageSize, J as parseAddressesPersistedPayload, L as LOCAL_COPIES_REMOVED_EVENT, a8 as CLIENT_NOT_INITIALIZED, a9 as stopTabCoordination, Z as ZERO_BYTES32, aa as getExtensionForMimeType, ab as INTERNAL_STORAGE_MODEL_FILE_IDS, i as isDataType, ac as resolveUploadMimeType, B as getContentSha256, y as buildArweaveContentTags } from './arweaveContentTags-iW_HcWJA.js';
|
|
9
9
|
import { eq, and, isNull, or, inArray, isNotNull, ne, desc, count, sql, like } from 'drizzle-orm';
|
|
10
10
|
import { Type } from '@sinclair/typebox';
|
|
11
11
|
import { isEqual } from 'lodash-es';
|
|
12
|
+
import { getSeed, registerLocalQuerySource, clearLocalQuerySource } from '@seedprotocol/query';
|
|
13
|
+
import { rewriteHtmlUrls, parseArchiveDocument, dublinCoreXml, premisXml, digestHex, createBag } from '@seedprotocol/archive';
|
|
14
|
+
import { parseFragment } from 'parse5';
|
|
12
15
|
import { sha3_256 } from '@noble/hashes/sha3';
|
|
13
16
|
import { bytesToHex, utf8ToBytes } from '@noble/hashes/utils';
|
|
14
|
-
import { getSeed, registerLocalQuerySource, clearLocalQuerySource } from '@seedprotocol/query';
|
|
15
17
|
|
|
16
|
-
const logger$
|
|
18
|
+
const logger$h = debug('seedSdk:model:actors:validateModel');
|
|
17
19
|
// Validation timeout in milliseconds (10 seconds)
|
|
18
20
|
const VALIDATION_TIMEOUT_MS = 10000;
|
|
19
21
|
const validateModel = fromCallback(({ sendBack, input: { context }, self }) => {
|
|
@@ -36,10 +38,10 @@ const validateModel = fromCallback(({ sendBack, input: { context }, self }) => {
|
|
|
36
38
|
const _validateModel = async () => {
|
|
37
39
|
try {
|
|
38
40
|
// Use dynamic imports to break circular dependencies
|
|
39
|
-
const validationServiceMod = await import('./SchemaValidationService-
|
|
41
|
+
const validationServiceMod = await import('./SchemaValidationService-Dc-lpFQX.js');
|
|
40
42
|
const { SchemaValidationService } = validationServiceMod;
|
|
41
43
|
const validationService = new SchemaValidationService();
|
|
42
|
-
const _mod_0 = await import('./Schema-
|
|
44
|
+
const _mod_0 = await import('./Schema-C_4wv0RP.js');
|
|
43
45
|
const _ns_0 = _mod_0.a;
|
|
44
46
|
const schemaMod = _ns_0;
|
|
45
47
|
const { Schema } = schemaMod;
|
|
@@ -85,12 +87,12 @@ const validateModel = fromCallback(({ sendBack, input: { context }, self }) => {
|
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
89
|
else {
|
|
88
|
-
logger$
|
|
90
|
+
logger$h(`Schema is in ${schemaStatus} state, skipping schema validation`);
|
|
89
91
|
// Continue with structure validation only
|
|
90
92
|
}
|
|
91
93
|
}
|
|
92
94
|
catch (error) {
|
|
93
|
-
logger$
|
|
95
|
+
logger$h('Error validating model against schema:', error);
|
|
94
96
|
// Continue with structure validation only
|
|
95
97
|
}
|
|
96
98
|
}
|
|
@@ -105,7 +107,7 @@ const validateModel = fromCallback(({ sendBack, input: { context }, self }) => {
|
|
|
105
107
|
if (!hasResponded) {
|
|
106
108
|
hasResponded = true;
|
|
107
109
|
clearTimeout(timeoutId);
|
|
108
|
-
logger$
|
|
110
|
+
logger$h('Error in validateModel:', error);
|
|
109
111
|
sendBack({
|
|
110
112
|
type: 'validationError',
|
|
111
113
|
errors: [{
|
|
@@ -122,7 +124,7 @@ const validateModel = fromCallback(({ sendBack, input: { context }, self }) => {
|
|
|
122
124
|
if (!hasResponded) {
|
|
123
125
|
hasResponded = true;
|
|
124
126
|
clearTimeout(timeoutId);
|
|
125
|
-
logger$
|
|
127
|
+
logger$h('Error in validateModel:', error);
|
|
126
128
|
sendBack({
|
|
127
129
|
type: 'validationError',
|
|
128
130
|
errors: [{
|
|
@@ -306,7 +308,7 @@ const modelMachine = setup({
|
|
|
306
308
|
},
|
|
307
309
|
});
|
|
308
310
|
|
|
309
|
-
const logger$
|
|
311
|
+
const logger$g = debug('seedSdk:model:createModelProperties');
|
|
310
312
|
/**
|
|
311
313
|
* Creates and caches the ModelProperty instances for the properties passed to Model.create, from
|
|
312
314
|
* the rows the model's write stored. Stops early when the model is stopped (unloaded, or evicted
|
|
@@ -319,8 +321,8 @@ async function createModelProperties(model, propertyDefinitions) {
|
|
|
319
321
|
if (!id || !_dbId || !modelName) {
|
|
320
322
|
throw new Error('Model ID, file ID, and name are required to create properties');
|
|
321
323
|
}
|
|
322
|
-
logger$
|
|
323
|
-
const { ModelProperty } = await import('./ModelProperty-
|
|
324
|
+
logger$g(`Creating ${Object.keys(propertyDefinitions).length} properties for model "${modelName}" (id: ${_dbId})`);
|
|
325
|
+
const { ModelProperty } = await import('./ModelProperty-Bv7KzP97.js');
|
|
324
326
|
const { and, eq } = await import('drizzle-orm');
|
|
325
327
|
const db = BaseDb.getAppDb();
|
|
326
328
|
for (const [propName, propData] of Object.entries(propertyDefinitions)) {
|
|
@@ -338,11 +340,11 @@ async function createModelProperties(model, propertyDefinitions) {
|
|
|
338
340
|
: undefined;
|
|
339
341
|
// Otherwise use provided ID or generate a random one
|
|
340
342
|
// IDs should be generated in the import process before creating properties
|
|
341
|
-
const _mod_1 = await import('./
|
|
342
|
-
const _ns_1 = _mod_1.
|
|
343
|
+
const _mod_1 = await import('./arweaveContentTags-iW_HcWJA.js');
|
|
344
|
+
const _ns_1 = _mod_1.ap;
|
|
343
345
|
const { generateId } = _ns_1;
|
|
344
346
|
const propertyFileId = propData.id || propertyRecord?.schemaFileId || generateId();
|
|
345
|
-
logger$
|
|
347
|
+
logger$g(`Creating property "${propName}" with fileId "${propertyFileId}"`);
|
|
346
348
|
// Query database to get refModelId if the property has ref/refModelName
|
|
347
349
|
// The property should already be in DB from addModelsToDb
|
|
348
350
|
let refModelId = propData.refModelId;
|
|
@@ -352,27 +354,27 @@ async function createModelProperties(model, propertyDefinitions) {
|
|
|
352
354
|
// First try to get refModelId from the property record in the database
|
|
353
355
|
if (propertyRecord?.refModelId) {
|
|
354
356
|
refModelId = propertyRecord.refModelId;
|
|
355
|
-
logger$
|
|
357
|
+
logger$g(`Found refModelId ${refModelId} from database for property "${propName}"`);
|
|
356
358
|
}
|
|
357
359
|
else if (refModelName) {
|
|
358
360
|
// Fallback: resolve the ref by name within this model's schema
|
|
359
|
-
const _mod_2 = await import('./
|
|
360
|
-
const _ns_2 = _mod_2.
|
|
361
|
+
const _mod_2 = await import('./Item-BU9wIT7l.js');
|
|
362
|
+
const _ns_2 = _mod_2.bP;
|
|
361
363
|
const { resolveModelRecord } = _ns_2;
|
|
362
364
|
const refModelRecord = await resolveModelRecord(refModelName, { schemaName }, db);
|
|
363
365
|
if (refModelRecord?.id) {
|
|
364
366
|
refModelId = refModelRecord.id;
|
|
365
|
-
logger$
|
|
367
|
+
logger$g(`Resolved refModelId ${refModelId} from model name "${refModelName}" for property "${propName}"`);
|
|
366
368
|
}
|
|
367
369
|
}
|
|
368
370
|
}
|
|
369
371
|
catch (error) {
|
|
370
|
-
logger$
|
|
372
|
+
logger$g(`Error fetching refModelId for property "${propName}":`, error);
|
|
371
373
|
// Continue without refModelId - it will be resolved later in _initializeOriginalValues
|
|
372
374
|
}
|
|
373
375
|
}
|
|
374
376
|
if (stopped()) {
|
|
375
|
-
logger$
|
|
377
|
+
logger$g(`Model "${modelName}" was stopped while creating its properties; not creating the rest`);
|
|
376
378
|
return;
|
|
377
379
|
}
|
|
378
380
|
// Create ModelProperty instance
|
|
@@ -409,10 +411,10 @@ async function createModelProperties(model, propertyDefinitions) {
|
|
|
409
411
|
// The model holds its properties (a child hold), released when the model is destroyed or evicted
|
|
410
412
|
ModelProperty.holdForModel(property, model);
|
|
411
413
|
}
|
|
412
|
-
logger$
|
|
414
|
+
logger$g(`Created all properties for model "${modelName}"`);
|
|
413
415
|
}
|
|
414
416
|
|
|
415
|
-
const logger$
|
|
417
|
+
const logger$f = debug('seedSdk:db:write:saveDefinition');
|
|
416
418
|
// Writes that have started. Evicting an instance can't cancel its write, so code that deletes rows
|
|
417
419
|
// under live instances (Schema.destroy, test cleanup) waits for these after evicting the instances.
|
|
418
420
|
const inFlightWrites = new Set();
|
|
@@ -444,7 +446,7 @@ function saveDefinition(input) {
|
|
|
444
446
|
}
|
|
445
447
|
const write = async (input) => {
|
|
446
448
|
if (input.type === 'model') {
|
|
447
|
-
logger$
|
|
449
|
+
logger$f(`Writing model "${input.modelName}" (${input.modelFileId}) to schema ${input.schemaId}`);
|
|
448
450
|
return writeModelToDb(input.modelFileId, {
|
|
449
451
|
modelName: input.modelName,
|
|
450
452
|
schemaId: input.schemaId,
|
|
@@ -452,13 +454,13 @@ const write = async (input) => {
|
|
|
452
454
|
});
|
|
453
455
|
}
|
|
454
456
|
// Imported here: the validation service imports the Schema barrel, which imports this module's callers
|
|
455
|
-
const { SchemaValidationService } = await import('./SchemaValidationService-
|
|
457
|
+
const { SchemaValidationService } = await import('./SchemaValidationService-Dc-lpFQX.js');
|
|
456
458
|
const data = input.data();
|
|
457
459
|
const { errors } = new SchemaValidationService().validatePropertyStructure(data);
|
|
458
460
|
if (errors.length > 0) {
|
|
459
461
|
throw new Error(`Property "${data.name}" is invalid: ${errors.map((error) => error.message).join(', ')}`);
|
|
460
462
|
}
|
|
461
|
-
logger$
|
|
463
|
+
logger$f(`Writing property "${data.name}" (${input.propertyFileId})`);
|
|
462
464
|
await writePropertyToDb(input.propertyFileId, data);
|
|
463
465
|
};
|
|
464
466
|
|
|
@@ -601,7 +603,7 @@ function setupEntityLiveQuery(instance, config) {
|
|
|
601
603
|
}
|
|
602
604
|
|
|
603
605
|
// Model and Schema are imported dynamically: Model.ts -> loadModel -> Schema.ts -> Model.ts
|
|
604
|
-
const logger$
|
|
606
|
+
const logger$e = debug('seedSdk:model:loadModel');
|
|
605
607
|
/**
|
|
606
608
|
* Create ModelProperty instances for all property IDs to ensure they're cached
|
|
607
609
|
* This ensures that ModelProperty.getById() in Model.properties getter will find the instances
|
|
@@ -613,10 +615,10 @@ const createPropertyInstances = async (propertyFileIds, since) => {
|
|
|
613
615
|
return;
|
|
614
616
|
}
|
|
615
617
|
try {
|
|
616
|
-
const mod = await import('./ModelProperty-
|
|
618
|
+
const mod = await import('./ModelProperty-Bv7KzP97.js');
|
|
617
619
|
const ModelProperty = mod?.ModelProperty ?? mod?.default;
|
|
618
620
|
if (!ModelProperty) {
|
|
619
|
-
logger$
|
|
621
|
+
logger$e('createPropertyInstances: ModelProperty not available from dynamic import');
|
|
620
622
|
return;
|
|
621
623
|
}
|
|
622
624
|
// Create instances for all property IDs in parallel
|
|
@@ -625,22 +627,22 @@ const createPropertyInstances = async (propertyFileIds, since) => {
|
|
|
625
627
|
try {
|
|
626
628
|
const property = await ModelProperty.createById(propertyFileId, { since });
|
|
627
629
|
if (property) {
|
|
628
|
-
logger$
|
|
630
|
+
logger$e(`Created/cached ModelProperty instance for propertyFileId "${propertyFileId}"`);
|
|
629
631
|
}
|
|
630
632
|
else {
|
|
631
|
-
logger$
|
|
633
|
+
logger$e(`ModelProperty.createById returned undefined for propertyFileId "${propertyFileId}" (may not exist in DB yet)`);
|
|
632
634
|
}
|
|
633
635
|
}
|
|
634
636
|
catch (error) {
|
|
635
|
-
logger$
|
|
637
|
+
logger$e(`Error creating ModelProperty instance for propertyFileId "${propertyFileId}": ${error}`);
|
|
636
638
|
// Don't throw - continue with other properties
|
|
637
639
|
}
|
|
638
640
|
});
|
|
639
641
|
await Promise.all(createPromises);
|
|
640
|
-
logger$
|
|
642
|
+
logger$e(`Finished creating/caching ${propertyFileIds.length} ModelProperty instances`);
|
|
641
643
|
}
|
|
642
644
|
catch (error) {
|
|
643
|
-
logger$
|
|
645
|
+
logger$e(`Error in createPropertyInstances: ${error}`);
|
|
644
646
|
// Don't throw - this is best-effort to pre-populate cache
|
|
645
647
|
}
|
|
646
648
|
};
|
|
@@ -675,7 +677,7 @@ async function loadModel(context, isStopped) {
|
|
|
675
677
|
if (dbModels.length > 0) {
|
|
676
678
|
modelRecord = dbModels[0];
|
|
677
679
|
foundBySchemaFileId = true;
|
|
678
|
-
logger$
|
|
680
|
+
logger$e(`Found model "${modelName}" in database by schemaFileId "${schemaFileId}"`);
|
|
679
681
|
}
|
|
680
682
|
}
|
|
681
683
|
// If not found by ID, try by name
|
|
@@ -684,11 +686,11 @@ async function loadModel(context, isStopped) {
|
|
|
684
686
|
if (!modelRecord) {
|
|
685
687
|
// Model names are only unique per schema: prefer this schema's row, and otherwise only
|
|
686
688
|
// consider same-name rows that no schema claims yet (never another schema's model).
|
|
687
|
-
const _mod_3 = await import('./BaseFileManager-
|
|
689
|
+
const _mod_3 = await import('./BaseFileManager-D-pLXQAp.js');
|
|
688
690
|
const _ns_3 = _mod_3.M;
|
|
689
691
|
const { modelSchemas } = _ns_3;
|
|
690
|
-
const _mod_4 = await import('./BaseFileManager-
|
|
691
|
-
const _ns_4 = _mod_4.
|
|
692
|
+
const _mod_4 = await import('./BaseFileManager-D-pLXQAp.js');
|
|
693
|
+
const _ns_4 = _mod_4.x;
|
|
692
694
|
const { schemas: schemasTable } = _ns_4;
|
|
693
695
|
let dbModels = await db
|
|
694
696
|
.select({
|
|
@@ -730,16 +732,16 @@ async function loadModel(context, isStopped) {
|
|
|
730
732
|
try {
|
|
731
733
|
const { Model } = await Promise.resolve().then(function () { return Model$1; });
|
|
732
734
|
if (Model.peek(dbSchemaFileId)) {
|
|
733
|
-
logger$
|
|
735
|
+
logger$e(`Model "${modelName}" found in database by name with schemaFileId "${dbSchemaFileId}", and a cached instance already exists. Updating current instance to use the same schemaFileId.`);
|
|
734
736
|
// Update the current instance's schemaFileId to match the database
|
|
735
737
|
// This ensures both instances point to the same cached instance
|
|
736
738
|
modelRecord = foundModel;
|
|
737
739
|
schemaFileId = dbSchemaFileId;
|
|
738
|
-
logger$
|
|
740
|
+
logger$e(`Using existing cached instance with schemaFileId "${schemaFileId}" for model "${modelName}"`);
|
|
739
741
|
}
|
|
740
742
|
}
|
|
741
743
|
catch (error) {
|
|
742
|
-
logger$
|
|
744
|
+
logger$e(`Error checking Model cache for schemaFileId "${dbSchemaFileId}": ${error}`);
|
|
743
745
|
// Fall through to normal processing
|
|
744
746
|
}
|
|
745
747
|
}
|
|
@@ -747,31 +749,31 @@ async function loadModel(context, isStopped) {
|
|
|
747
749
|
// this is a new model - don't use the existing model (will be renamed if duplicate)
|
|
748
750
|
// UNLESS we found a cached instance above (in which case modelRecord is already set)
|
|
749
751
|
if (!modelRecord && schemaFileId && dbSchemaFileId && schemaFileId !== dbSchemaFileId) {
|
|
750
|
-
logger$
|
|
752
|
+
logger$e(`Model "${modelName}" found by name but has different schemaFileId (provided: "${schemaFileId}", db: "${dbSchemaFileId}"). Creating new model (will check for duplicates).`);
|
|
751
753
|
// Don't set modelRecord - we'll create a new model with potentially renamed name
|
|
752
754
|
}
|
|
753
755
|
else if (!modelRecord && schemaFileId && !dbSchemaFileId) {
|
|
754
756
|
// We have a schemaFileId but the found model doesn't have one - this is a new model
|
|
755
757
|
// Don't reuse the existing model, create a new one (will be renamed if duplicate)
|
|
756
|
-
logger$
|
|
758
|
+
logger$e(`Model "${modelName}" found by name but found model has no schemaFileId. Creating new model with schemaFileId "${schemaFileId}" (will check for duplicates).`);
|
|
757
759
|
// Don't set modelRecord - we'll create a new model
|
|
758
760
|
}
|
|
759
761
|
else if (!modelRecord) {
|
|
760
762
|
// Only reuse existing model if:
|
|
761
763
|
// 1. No schemaFileId was provided (loading existing model), OR
|
|
762
764
|
// 2. schemaFileId matches (same model)
|
|
763
|
-
logger$
|
|
765
|
+
logger$e(`Model "${modelName}" found by name, reusing existing model (schemaFileId: provided="${schemaFileId}", db="${dbSchemaFileId}")`);
|
|
764
766
|
modelRecord = foundModel;
|
|
765
767
|
// Only use database schemaFileId if no id was explicitly provided
|
|
766
768
|
// If an id was provided, we should use it (it might be creating a new model with a specific ID)
|
|
767
769
|
if (!id && dbSchemaFileId) {
|
|
768
770
|
schemaFileId = dbSchemaFileId;
|
|
769
|
-
logger$
|
|
771
|
+
logger$e(`Using database schemaFileId "${schemaFileId}" for model "${modelName}" (no ID was provided)`);
|
|
770
772
|
}
|
|
771
773
|
else if (id) {
|
|
772
|
-
logger$
|
|
774
|
+
logger$e(`Preserving provided id (schemaFileId) "${schemaFileId}" for model "${modelName}" (ignoring database schemaFileId "${dbSchemaFileId}")`);
|
|
773
775
|
}
|
|
774
|
-
logger$
|
|
776
|
+
logger$e(`Found model "${modelName}" in database by name, schemaFileId: "${schemaFileId}"`);
|
|
775
777
|
}
|
|
776
778
|
}
|
|
777
779
|
}
|
|
@@ -796,7 +798,7 @@ async function loadModel(context, isStopped) {
|
|
|
796
798
|
// Generate schemaFileId if not set
|
|
797
799
|
if (!schemaFileId) {
|
|
798
800
|
schemaFileId = generateId();
|
|
799
|
-
logger$
|
|
801
|
+
logger$e(`Generated id (schemaFileId) "${schemaFileId}" for model "${modelName}"`);
|
|
800
802
|
}
|
|
801
803
|
// Include _dbId from database record so properties can be created if needed
|
|
802
804
|
// Even if we found an existing model, we may still need to create properties
|
|
@@ -820,14 +822,14 @@ async function loadModel(context, isStopped) {
|
|
|
820
822
|
}
|
|
821
823
|
}
|
|
822
824
|
catch (error) {
|
|
823
|
-
logger$
|
|
825
|
+
logger$e(`Error loading model from database: ${error}`);
|
|
824
826
|
// Fall through to create new model
|
|
825
827
|
}
|
|
826
828
|
}
|
|
827
829
|
// Step 2: Fallback to Schema context (only if database doesn't have the model)
|
|
828
830
|
// This handles the case where model exists in schema file but not yet in database
|
|
829
831
|
try {
|
|
830
|
-
const _mod_5 = await import('./Schema-
|
|
832
|
+
const _mod_5 = await import('./Schema-C_4wv0RP.js');
|
|
831
833
|
const _ns_5 = _mod_5.a;
|
|
832
834
|
const { Schema } = _ns_5;
|
|
833
835
|
// Stopped meanwhile (unloaded, or evicted with its schema): don't load the schema again
|
|
@@ -842,10 +844,10 @@ async function loadModel(context, isStopped) {
|
|
|
842
844
|
const byName = schemaContext.models[modelName];
|
|
843
845
|
const schemaModelId = byName?.id ?? null;
|
|
844
846
|
if (schemaFileId === schemaModelId) {
|
|
845
|
-
logger$
|
|
847
|
+
logger$e(`Found model "${modelName}" in Schema context (database fallback)`);
|
|
846
848
|
if (!schemaFileId) {
|
|
847
849
|
schemaFileId = generateId();
|
|
848
|
-
logger$
|
|
850
|
+
logger$e(`Generated id (schemaFileId) "${schemaFileId}" for model "${modelName}"`);
|
|
849
851
|
}
|
|
850
852
|
return { id: schemaFileId };
|
|
851
853
|
}
|
|
@@ -857,7 +859,7 @@ async function loadModel(context, isStopped) {
|
|
|
857
859
|
// Schema context may not have model by name yet (e.g. still loading). If our id appears in any schema model, we're the schema model.
|
|
858
860
|
for (const def of Object.values(schemaContext.models)) {
|
|
859
861
|
if (def?.id === schemaFileId) {
|
|
860
|
-
logger$
|
|
862
|
+
logger$e(`Found model in Schema context by id "${schemaFileId}" (database fallback)`);
|
|
861
863
|
return { id: schemaFileId };
|
|
862
864
|
}
|
|
863
865
|
}
|
|
@@ -865,7 +867,7 @@ async function loadModel(context, isStopped) {
|
|
|
865
867
|
}
|
|
866
868
|
}
|
|
867
869
|
catch (error) {
|
|
868
|
-
logger$
|
|
870
|
+
logger$e(`Error loading model from Schema context: ${error}`);
|
|
869
871
|
// Fall through to create new model
|
|
870
872
|
}
|
|
871
873
|
// Step 3: Check for duplicate names in database before creating new model
|
|
@@ -885,7 +887,7 @@ async function loadModel(context, isStopped) {
|
|
|
885
887
|
// 3. This is an internal schema (internal schemas should preserve original names from schema files), OR
|
|
886
888
|
// 4. This instance was created with modelFileId from schema (_idFromSchema) - schema models keep their name
|
|
887
889
|
const shouldSkipDuplicateCheck = modelRecord !== null || foundBySchemaFileId || isInternal || !!_idFromSchema;
|
|
888
|
-
logger$
|
|
890
|
+
logger$e(`Duplicate check: modelRecord=${modelRecord !== null}, foundBySchemaFileId=${foundBySchemaFileId}, isInternal=${isInternal}, _idFromSchema=${!!_idFromSchema}, schemaFileId=${schemaFileId}, shouldSkip=${shouldSkipDuplicateCheck}`);
|
|
889
891
|
if (!shouldSkipDuplicateCheck) {
|
|
890
892
|
try {
|
|
891
893
|
const lowerModelName = modelName.toLowerCase();
|
|
@@ -908,20 +910,20 @@ async function loadModel(context, isStopped) {
|
|
|
908
910
|
}
|
|
909
911
|
}
|
|
910
912
|
catch (error) {
|
|
911
|
-
logger$
|
|
913
|
+
logger$e(`Error checking Model cache for duplicates: ${error}`);
|
|
912
914
|
}
|
|
913
915
|
// Also check database for models (in case they're persisted but not in Schema context)
|
|
914
916
|
if (db) {
|
|
915
|
-
const _mod_6 = await import('./BaseFileManager-
|
|
917
|
+
const _mod_6 = await import('./BaseFileManager-D-pLXQAp.js');
|
|
916
918
|
const _ns_6 = _mod_6.M;
|
|
917
919
|
const modelSchemaSchemaMod = _ns_6;
|
|
918
920
|
const { modelSchemas } = modelSchemaSchemaMod;
|
|
919
|
-
const _mod_7 = await import('./BaseFileManager-
|
|
920
|
-
const _ns_7 = _mod_7.
|
|
921
|
+
const _mod_7 = await import('./BaseFileManager-D-pLXQAp.js');
|
|
922
|
+
const _ns_7 = _mod_7.x;
|
|
921
923
|
const schemaSchemaMod = _ns_7;
|
|
922
924
|
const { schemas: schemasTable } = schemaSchemaMod;
|
|
923
925
|
// Query all models for this schema to check for duplicates (case-insensitive)
|
|
924
|
-
logger$
|
|
926
|
+
logger$e(`Checking database for duplicate model names in schema "${schemaName}"`);
|
|
925
927
|
const allModelsForSchema = await db
|
|
926
928
|
.select({
|
|
927
929
|
name: models$1.name,
|
|
@@ -930,16 +932,16 @@ async function loadModel(context, isStopped) {
|
|
|
930
932
|
.innerJoin(modelSchemas, eq(models$1.id, modelSchemas.modelId))
|
|
931
933
|
.innerJoin(schemasTable, eq(modelSchemas.schemaId, schemasTable.id))
|
|
932
934
|
.where(eq(schemasTable.name, schemaName));
|
|
933
|
-
logger$
|
|
935
|
+
logger$e(`Found ${allModelsForSchema.length} models in database for schema "${schemaName}": ${allModelsForSchema.map((m) => m.name).join(', ')}`);
|
|
934
936
|
if (allModelsForSchema.length === 0 && modelName.includes('Shared')) {
|
|
935
|
-
logger$
|
|
937
|
+
logger$e(`[DEBUG] No models found in database for schema "${schemaName}" when checking for duplicate "${modelName}"`);
|
|
936
938
|
}
|
|
937
939
|
// Check for exact match (case-insensitive) or matches with number suffix
|
|
938
940
|
for (const dbModel of allModelsForSchema) {
|
|
939
941
|
const lowerDbName = dbModel.name.toLowerCase();
|
|
940
942
|
if (lowerDbName === lowerModelName) {
|
|
941
943
|
existingNumbers.add(0); // Base name exists
|
|
942
|
-
logger$
|
|
944
|
+
logger$e(`Found duplicate in database: "${dbModel.name}" matches "${modelName}"`);
|
|
943
945
|
}
|
|
944
946
|
else if (lowerDbName.startsWith(lowerModelName + ' ')) {
|
|
945
947
|
// Check if it's the base name followed by a space and a number
|
|
@@ -947,55 +949,55 @@ async function loadModel(context, isStopped) {
|
|
|
947
949
|
const number = parseInt(suffix, 10);
|
|
948
950
|
if (!isNaN(number) && suffix === number.toString()) {
|
|
949
951
|
existingNumbers.add(number);
|
|
950
|
-
logger$
|
|
952
|
+
logger$e(`Found numbered variant in database: "${dbModel.name}" (number: ${number})`);
|
|
951
953
|
}
|
|
952
954
|
}
|
|
953
955
|
}
|
|
954
956
|
}
|
|
955
957
|
// If duplicates found, generate unique name
|
|
956
|
-
logger$
|
|
958
|
+
logger$e(`Duplicate check results: existingNumbers=${Array.from(existingNumbers).join(', ')}, hasBaseName=${existingNumbers.has(0)}`);
|
|
957
959
|
if (existingNumbers.has(0)) {
|
|
958
960
|
let nextNumber = 1;
|
|
959
961
|
while (existingNumbers.has(nextNumber)) {
|
|
960
962
|
nextNumber++;
|
|
961
963
|
}
|
|
962
964
|
finalModelName = `${modelName} ${nextNumber}`;
|
|
963
|
-
logger$
|
|
965
|
+
logger$e(`Found duplicate model name "${modelName}" in schema "${schemaName}", using unique name "${finalModelName}"`);
|
|
964
966
|
}
|
|
965
967
|
else {
|
|
966
|
-
logger$
|
|
968
|
+
logger$e(`No duplicate found for model name "${modelName}" in schema "${schemaName}"`);
|
|
967
969
|
}
|
|
968
970
|
}
|
|
969
971
|
catch (error) {
|
|
970
|
-
logger$
|
|
972
|
+
logger$e(`Error checking for duplicate model names: ${error}`);
|
|
971
973
|
// Continue with original name if check fails
|
|
972
974
|
}
|
|
973
975
|
}
|
|
974
976
|
else {
|
|
975
977
|
if (foundBySchemaFileId) {
|
|
976
|
-
logger$
|
|
978
|
+
logger$e(`Preserving original model name "${modelName}" for model found by schemaFileId (schemaFileId: ${schemaFileId})`);
|
|
977
979
|
}
|
|
978
980
|
else if (modelRecord) {
|
|
979
|
-
logger$
|
|
981
|
+
logger$e(`Using existing model name "${modelName}" from database (modelRecord found)`);
|
|
980
982
|
}
|
|
981
983
|
else if (isInternal) {
|
|
982
|
-
logger$
|
|
984
|
+
logger$e(`Preserving original model name "${modelName}" for internal schema "${schemaName}" (skipping duplicate check)`);
|
|
983
985
|
}
|
|
984
986
|
}
|
|
985
987
|
}
|
|
986
988
|
// Step 4: Create new empty model (not found in database or Schema context)
|
|
987
|
-
logger$
|
|
989
|
+
logger$e(`Creating new model "${finalModelName}" in schema "${schemaName}"`);
|
|
988
990
|
// Generate schemaFileId for new model
|
|
989
991
|
if (!schemaFileId) {
|
|
990
992
|
schemaFileId = generateId();
|
|
991
|
-
logger$
|
|
993
|
+
logger$e(`Generated id (schemaFileId) "${schemaFileId}" for new model "${finalModelName}"`);
|
|
992
994
|
}
|
|
993
995
|
if (finalModelName !== modelName) {
|
|
994
|
-
logger$
|
|
996
|
+
logger$e(`Model name "${modelName}" is taken in schema "${schemaName}"; using "${finalModelName}"`);
|
|
995
997
|
}
|
|
996
998
|
// Mark schema as draft when a new model is created so saveNewVersion() can persist it
|
|
997
999
|
try {
|
|
998
|
-
const _mod_8 = await import('./Schema-
|
|
1000
|
+
const _mod_8 = await import('./Schema-C_4wv0RP.js');
|
|
999
1001
|
const _ns_8 = _mod_8.a;
|
|
1000
1002
|
const { Schema } = _ns_8;
|
|
1001
1003
|
if (!isStopped()) {
|
|
@@ -1004,7 +1006,7 @@ async function loadModel(context, isStopped) {
|
|
|
1004
1006
|
}
|
|
1005
1007
|
}
|
|
1006
1008
|
catch (err) {
|
|
1007
|
-
logger$
|
|
1009
|
+
logger$e(`Failed to mark schema as draft after creating model: ${err}`);
|
|
1008
1010
|
}
|
|
1009
1011
|
return {
|
|
1010
1012
|
id: schemaFileId,
|
|
@@ -1084,12 +1086,12 @@ function queueDraftSave(key, save) {
|
|
|
1084
1086
|
return current;
|
|
1085
1087
|
}
|
|
1086
1088
|
|
|
1087
|
-
const logger$
|
|
1089
|
+
const logger$d = debug('seedSdk:model:Model');
|
|
1088
1090
|
const saveDraftLogger = debug('seedSdk:model:saveDraftToDb');
|
|
1089
1091
|
// Lazy import cache for ModelProperty to avoid circular dependency
|
|
1090
1092
|
// Eagerly start loading to minimize delay on first access
|
|
1091
1093
|
let ModelPropertyClass = null;
|
|
1092
|
-
const modelPropertyImportPromise = import('./ModelProperty-
|
|
1094
|
+
const modelPropertyImportPromise = import('./ModelProperty-Bv7KzP97.js')
|
|
1093
1095
|
.then(module => {
|
|
1094
1096
|
ModelPropertyClass = module.ModelProperty;
|
|
1095
1097
|
return ModelPropertyClass;
|
|
@@ -1240,7 +1242,7 @@ class Model {
|
|
|
1240
1242
|
});
|
|
1241
1243
|
}
|
|
1242
1244
|
else {
|
|
1243
|
-
logger$
|
|
1245
|
+
logger$d(`Model "${context.modelName}" adopts model row ${dbId} written by its schema import`);
|
|
1244
1246
|
}
|
|
1245
1247
|
ensureRunning();
|
|
1246
1248
|
service.send({ type: 'updateContext', _dbId: dbId });
|
|
@@ -1341,7 +1343,7 @@ class Model {
|
|
|
1341
1343
|
if (cachedById) {
|
|
1342
1344
|
if (holds)
|
|
1343
1345
|
this.cache.holdInstance(cachedById);
|
|
1344
|
-
logger$
|
|
1346
|
+
logger$d(`Model.create: Found instance in ID cache for "${modelName}" (ID: ${id})`);
|
|
1345
1347
|
if (!waitForReady)
|
|
1346
1348
|
return cachedById;
|
|
1347
1349
|
return Model.readyOrRelease(cachedById, readyTimeout, holds);
|
|
@@ -1350,7 +1352,7 @@ class Model {
|
|
|
1350
1352
|
// a schema model (with id) keeps its name.
|
|
1351
1353
|
const uniqueModelName = this.findUniqueModelName(modelName, schemaName, !!id);
|
|
1352
1354
|
if (uniqueModelName !== modelName) {
|
|
1353
|
-
logger$
|
|
1355
|
+
logger$d(`Model.create: Found duplicate name "${modelName}" in schema "${schemaName}", using unique name "${uniqueModelName}"`);
|
|
1354
1356
|
}
|
|
1355
1357
|
const resolvedId = id ?? generateId();
|
|
1356
1358
|
// The id and the properties to create go in the initial context, so the load sees them
|
|
@@ -1360,7 +1362,7 @@ class Model {
|
|
|
1360
1362
|
// Use the unique model name for the instance (idFromSchema: true when id was provided, so schema model is not renamed)
|
|
1361
1363
|
const newInstance = new this(uniqueModelName, schemaName, resolvedId, initialContext, !!id);
|
|
1362
1364
|
if (options?.properties) {
|
|
1363
|
-
logger$
|
|
1365
|
+
logger$d(`[Model.create] Created instance with _pendingPropertyDefinitions in initial context (${Object.keys(options.properties).length} properties) for model "${uniqueModelName}"`);
|
|
1364
1366
|
}
|
|
1365
1367
|
// Wrap instance in Proxy for reactive property access. `name` is an alias for `modelName`.
|
|
1366
1368
|
const sendUpdate = (target, prop, value) => {
|
|
@@ -1371,10 +1373,10 @@ class Model {
|
|
|
1371
1373
|
const oldName = context.modelName;
|
|
1372
1374
|
const newName = value;
|
|
1373
1375
|
if (oldName === newName) {
|
|
1374
|
-
logger$
|
|
1376
|
+
logger$d(`Model name unchanged: "${oldName}"`);
|
|
1375
1377
|
return;
|
|
1376
1378
|
}
|
|
1377
|
-
logger$
|
|
1379
|
+
logger$d(`Updating model name from "${oldName}" to "${newName}"`);
|
|
1378
1380
|
Model.cache.setKeys(target, [context.id, modelKey(context.schemaName, newName)]);
|
|
1379
1381
|
// Update Model context
|
|
1380
1382
|
target._service.send({
|
|
@@ -1430,7 +1432,7 @@ class Model {
|
|
|
1430
1432
|
this.cache.add(proxiedInstance, [resolvedId, id ? modelKey(schemaName, uniqueModelName) : undefined], {
|
|
1431
1433
|
hold: holds,
|
|
1432
1434
|
});
|
|
1433
|
-
logger$
|
|
1435
|
+
logger$d(`Model.create: Created new instance for "${uniqueModelName}" (ID: ${id})`);
|
|
1434
1436
|
// Step 11: Register with schema if requested, OR trigger write if properties are provided
|
|
1435
1437
|
// If properties are provided, we need to write the model to get modelId for property creation
|
|
1436
1438
|
// If schema provided, trigger write process instead of registration
|
|
@@ -1455,7 +1457,7 @@ class Model {
|
|
|
1455
1457
|
adoptExistingRow,
|
|
1456
1458
|
});
|
|
1457
1459
|
if (!hasProperties) {
|
|
1458
|
-
persisted.catch((error) => logger$
|
|
1460
|
+
persisted.catch((error) => logger$d(`Model "${uniqueModelName}" was not written: ${error}`));
|
|
1459
1461
|
return;
|
|
1460
1462
|
}
|
|
1461
1463
|
await persisted.catch((error) => {
|
|
@@ -1598,8 +1600,8 @@ class Model {
|
|
|
1598
1600
|
if (db) {
|
|
1599
1601
|
const since = evictionLog.now();
|
|
1600
1602
|
try {
|
|
1601
|
-
const _mod_9 = await import('./
|
|
1602
|
-
const _ns_9 = _mod_9.
|
|
1603
|
+
const _mod_9 = await import('./Item-BU9wIT7l.js');
|
|
1604
|
+
const _ns_9 = _mod_9.bP;
|
|
1603
1605
|
const { resolveModelRecord, getModelSchemaName } = _ns_9;
|
|
1604
1606
|
const resolved = scope.modelFileId
|
|
1605
1607
|
? await resolveModelRecord(undefined, { modelFileId: scope.modelFileId }, db)
|
|
@@ -1617,7 +1619,7 @@ class Model {
|
|
|
1617
1619
|
}
|
|
1618
1620
|
}
|
|
1619
1621
|
catch (error) {
|
|
1620
|
-
logger$
|
|
1622
|
+
logger$d(`Model.resolveAsync: scoped lookup failed for "${modelName}": ${error instanceof Error ? error.message : String(error)}`);
|
|
1621
1623
|
}
|
|
1622
1624
|
}
|
|
1623
1625
|
}
|
|
@@ -1645,8 +1647,8 @@ class Model {
|
|
|
1645
1647
|
}
|
|
1646
1648
|
const since = options?.since ?? evictionLog.now();
|
|
1647
1649
|
try {
|
|
1648
|
-
const _mod_10 = await import('./
|
|
1649
|
-
const _ns_10 = _mod_10.
|
|
1650
|
+
const _mod_10 = await import('./Item-BU9wIT7l.js');
|
|
1651
|
+
const _ns_10 = _mod_10.bP;
|
|
1650
1652
|
const { resolveModelRecord, getModelSchemaName } = _ns_10;
|
|
1651
1653
|
const resolved = await resolveModelRecord(modelName, schemaName ? { schemaName, strict: true } : {}, db);
|
|
1652
1654
|
if (!resolved?.schemaFileId) {
|
|
@@ -1669,7 +1671,7 @@ class Model {
|
|
|
1669
1671
|
catch (error) {
|
|
1670
1672
|
if (error instanceof AmbiguousModelError)
|
|
1671
1673
|
throw error;
|
|
1672
|
-
logger$
|
|
1674
|
+
logger$d(`Model.getByNameAsync: Error looking up model by name "${modelName}": ${error instanceof Error ? error.message : String(error)}`);
|
|
1673
1675
|
return undefined;
|
|
1674
1676
|
}
|
|
1675
1677
|
}
|
|
@@ -1687,13 +1689,13 @@ class Model {
|
|
|
1687
1689
|
// Step 1: Check cache first (fast path)
|
|
1688
1690
|
const cached = this.getById(modelFileId, options);
|
|
1689
1691
|
if (cached) {
|
|
1690
|
-
logger$
|
|
1692
|
+
logger$d(`Model.createById: Found instance in cache for ID "${modelFileId}"`);
|
|
1691
1693
|
return cached;
|
|
1692
1694
|
}
|
|
1693
1695
|
// Step 2: Query database to get modelName and schemaName
|
|
1694
1696
|
const db = BaseDb.getAppDb();
|
|
1695
1697
|
if (!db) {
|
|
1696
|
-
logger$
|
|
1698
|
+
logger$d(`Model.createById: Database not available for ID "${modelFileId}"`);
|
|
1697
1699
|
return undefined;
|
|
1698
1700
|
}
|
|
1699
1701
|
const since = options?.since ?? evictionLog.now();
|
|
@@ -1711,30 +1713,30 @@ class Model {
|
|
|
1711
1713
|
.where(eq(models$1.schemaFileId, modelFileId))
|
|
1712
1714
|
.limit(1);
|
|
1713
1715
|
if (modelRecords.length === 0) {
|
|
1714
|
-
logger$
|
|
1716
|
+
logger$d(`Model.createById: Model not found in database for ID "${modelFileId}"`);
|
|
1715
1717
|
return undefined;
|
|
1716
1718
|
}
|
|
1717
1719
|
const { modelName, schemaName } = modelRecords[0];
|
|
1718
1720
|
if (!modelName || !schemaName) {
|
|
1719
|
-
logger$
|
|
1721
|
+
logger$d(`Model.createById: Missing modelName or schemaName for ID "${modelFileId}"`);
|
|
1720
1722
|
return undefined;
|
|
1721
1723
|
}
|
|
1722
1724
|
// Its schema was evicted while we read the row (e.g. a Schema live query in flight during
|
|
1723
1725
|
// Schema.destroy or test cleanup): don't bring the model back.
|
|
1724
1726
|
// or the model was destroyed (destroy() leaves the cache before it deletes the rows)
|
|
1725
1727
|
if (evictionLog.evictedSince([`schema:${schemaName}`, `model:${modelFileId}`], since)) {
|
|
1726
|
-
logger$
|
|
1728
|
+
logger$d(`Model.createById: "${modelFileId}" or its schema "${schemaName}" was evicted during the lookup`);
|
|
1727
1729
|
return undefined;
|
|
1728
1730
|
}
|
|
1729
1731
|
// Step 3: Create model using existing create method (which will handle caching)
|
|
1730
|
-
logger$
|
|
1732
|
+
logger$d(`Model.createById: Creating model "${modelName}" in schema "${schemaName}" from ID "${modelFileId}"`);
|
|
1731
1733
|
return this.create(modelName, schemaName, {
|
|
1732
1734
|
id: modelFileId, // id is now the schemaFileId (string)
|
|
1733
1735
|
hold: options?.hold,
|
|
1734
1736
|
});
|
|
1735
1737
|
}
|
|
1736
1738
|
catch (error) {
|
|
1737
|
-
logger$
|
|
1739
|
+
logger$d(`Model.createById: Error looking up model by ID "${modelFileId}": ${error instanceof Error ? error.message : String(error)}`);
|
|
1738
1740
|
return undefined;
|
|
1739
1741
|
}
|
|
1740
1742
|
}
|
|
@@ -1776,7 +1778,7 @@ class Model {
|
|
|
1776
1778
|
}
|
|
1777
1779
|
const db = BaseDb.getAppDb();
|
|
1778
1780
|
if (!db) {
|
|
1779
|
-
logger$
|
|
1781
|
+
logger$d(`Model.createBySchemaId: Database not available for schema "${schemaIdentifier}"`);
|
|
1780
1782
|
return [];
|
|
1781
1783
|
}
|
|
1782
1784
|
const since = evictionLog.now();
|
|
@@ -1793,7 +1795,7 @@ class Model {
|
|
|
1793
1795
|
.innerJoin(models$1, eq(modelSchemas.modelId, models$1.id))
|
|
1794
1796
|
.where(or(eq(schemas.schemaFileId, schemaIdentifier), eq(schemas.name, schemaIdentifier)));
|
|
1795
1797
|
if (modelRecords.length === 0) {
|
|
1796
|
-
logger$
|
|
1798
|
+
logger$d(`Model.createBySchemaId: No models found in database for schema "${schemaIdentifier}"`);
|
|
1797
1799
|
return [];
|
|
1798
1800
|
}
|
|
1799
1801
|
// Create Model instances for each model (create() handles caching)
|
|
@@ -1821,11 +1823,11 @@ class Model {
|
|
|
1821
1823
|
modelInstances.push(model);
|
|
1822
1824
|
}
|
|
1823
1825
|
}
|
|
1824
|
-
logger$
|
|
1826
|
+
logger$d(`Model.createBySchemaId: Found ${modelInstances.length} models for schema "${schemaIdentifier}"`);
|
|
1825
1827
|
return modelInstances;
|
|
1826
1828
|
}
|
|
1827
1829
|
catch (error) {
|
|
1828
|
-
logger$
|
|
1830
|
+
logger$d(`Model.createBySchemaId: Error looking up models for schema "${schemaIdentifier}": ${error instanceof Error ? error.message : String(error)}`);
|
|
1829
1831
|
return [];
|
|
1830
1832
|
}
|
|
1831
1833
|
}
|
|
@@ -1874,7 +1876,7 @@ class Model {
|
|
|
1874
1876
|
// Leave out models that errored or didn't get ready in time. Don't re-read their state afterwards:
|
|
1875
1877
|
// a ready model can already be validating again, and it still belongs in the result.
|
|
1876
1878
|
const ready = await Promise.all(instances.map((m) => waitForEntityReady(m, { timeout: readyTimeout }).then(() => true, (error) => {
|
|
1877
|
-
logger$
|
|
1879
|
+
logger$d(`Model.all: leaving out a model that isn't ready: ${error instanceof Error ? error.message : String(error)}`);
|
|
1878
1880
|
return false;
|
|
1879
1881
|
})));
|
|
1880
1882
|
return held(instances.filter((_, i) => ready[i]));
|
|
@@ -1929,7 +1931,7 @@ class Model {
|
|
|
1929
1931
|
return { hasConflict: false };
|
|
1930
1932
|
}
|
|
1931
1933
|
catch (error) {
|
|
1932
|
-
logger$
|
|
1934
|
+
logger$d(`Error checking for conflicts: ${error instanceof Error ? error.message : String(error)}`);
|
|
1933
1935
|
return { hasConflict: false }; // On error, assume no conflict to allow save
|
|
1934
1936
|
}
|
|
1935
1937
|
}
|
|
@@ -2093,7 +2095,7 @@ class Model {
|
|
|
2093
2095
|
*/
|
|
2094
2096
|
async reload() {
|
|
2095
2097
|
this._assertLoaded('reload');
|
|
2096
|
-
logger$
|
|
2098
|
+
logger$d(`Reloading model ${this.modelName} from database`);
|
|
2097
2099
|
await runEntityLoad(this, () => Model.load(this));
|
|
2098
2100
|
if (this.status === 'error')
|
|
2099
2101
|
throw new Error('Failed to reload model from database');
|
|
@@ -2146,9 +2148,9 @@ class Model {
|
|
|
2146
2148
|
.limit(1);
|
|
2147
2149
|
if (modelRecord) {
|
|
2148
2150
|
const modelId = modelRecord.id;
|
|
2149
|
-
const { detachModelRows } = await import('./detachRowsBeforeDelete-
|
|
2150
|
-
const _mod_11 = await import('./
|
|
2151
|
-
const _ns_11 = _mod_11.
|
|
2151
|
+
const { detachModelRows } = await import('./detachRowsBeforeDelete-Cie55SoG.js');
|
|
2152
|
+
const _mod_11 = await import('./Item-BU9wIT7l.js');
|
|
2153
|
+
const _ns_11 = _mod_11.bR;
|
|
2152
2154
|
const { inDefinitionsTransaction } = _ns_11;
|
|
2153
2155
|
// The model's rows go together or not at all
|
|
2154
2156
|
await inDefinitionsTransaction(async (tx) => {
|
|
@@ -2164,7 +2166,7 @@ class Model {
|
|
|
2164
2166
|
getModelProperty()?.evictForModel({ modelName, schemaName, modelId: dbId });
|
|
2165
2167
|
if (schemaName && modelName) {
|
|
2166
2168
|
// Imported here, not at module load: Model and Schema import each other.
|
|
2167
|
-
const _mod_12 = await import('./Schema-
|
|
2169
|
+
const _mod_12 = await import('./Schema-C_4wv0RP.js');
|
|
2168
2170
|
const _ns_12 = _mod_12.a;
|
|
2169
2171
|
const { Schema } = _ns_12;
|
|
2170
2172
|
const schema = await Schema.find({ schemaName, waitForReady: false });
|
|
@@ -2203,8 +2205,8 @@ class Model {
|
|
|
2203
2205
|
return undefined;
|
|
2204
2206
|
}
|
|
2205
2207
|
// Model names are only unique per schema: find this model's row by its id (schemaFileId).
|
|
2206
|
-
const _mod_13 = await import('./
|
|
2207
|
-
const _ns_13 = _mod_13.
|
|
2208
|
+
const _mod_13 = await import('./Item-BU9wIT7l.js');
|
|
2209
|
+
const _ns_13 = _mod_13.bP;
|
|
2208
2210
|
const { resolveModelRecord } = _ns_13;
|
|
2209
2211
|
const modelRecord = await resolveModelRecord(modelName, { modelFileId: context.id, schemaName: context.schemaName }, db);
|
|
2210
2212
|
if (!modelRecord?.id) {
|
|
@@ -2226,7 +2228,7 @@ class Model {
|
|
|
2226
2228
|
});
|
|
2227
2229
|
}
|
|
2228
2230
|
catch (error) {
|
|
2229
|
-
logger$
|
|
2231
|
+
logger$d(`[Model._setupLiveQuerySubscription] Error finding model: ${error}`);
|
|
2230
2232
|
return undefined;
|
|
2231
2233
|
}
|
|
2232
2234
|
}
|
|
@@ -2253,7 +2255,7 @@ class Model {
|
|
|
2253
2255
|
await modelPropertyImportPromise;
|
|
2254
2256
|
const ModelProperty = getModelProperty();
|
|
2255
2257
|
if (!ModelProperty) {
|
|
2256
|
-
logger$
|
|
2258
|
+
logger$d('[Model._setupLiveQuerySubscription] ModelProperty not yet loaded');
|
|
2257
2259
|
return;
|
|
2258
2260
|
}
|
|
2259
2261
|
const createPromises = ids.map(async (propertyFileId) => {
|
|
@@ -2261,7 +2263,7 @@ class Model {
|
|
|
2261
2263
|
await ModelProperty.createById(propertyFileId);
|
|
2262
2264
|
}
|
|
2263
2265
|
catch (error) {
|
|
2264
|
-
logger$
|
|
2266
|
+
logger$d(`[Model._setupLiveQuerySubscription] Error creating ModelProperty instance for propertyFileId "${propertyFileId}": ${error}`);
|
|
2265
2267
|
}
|
|
2266
2268
|
});
|
|
2267
2269
|
await Promise.all(createPromises);
|
|
@@ -2314,28 +2316,28 @@ class Model {
|
|
|
2314
2316
|
await modelPropertyImportPromise;
|
|
2315
2317
|
const ModelProperty = getModelProperty();
|
|
2316
2318
|
if (!ModelProperty) {
|
|
2317
|
-
logger$
|
|
2319
|
+
logger$d('[Model._refreshPropertiesFromDb] ModelProperty not yet loaded');
|
|
2318
2320
|
}
|
|
2319
2321
|
else {
|
|
2320
2322
|
const createPromises = propertyIds.map(async (propertyFileId) => {
|
|
2321
2323
|
try {
|
|
2322
2324
|
const property = await ModelProperty.createById(propertyFileId);
|
|
2323
2325
|
if (property) {
|
|
2324
|
-
logger$
|
|
2326
|
+
logger$d(`[Model._refreshPropertiesFromDb] Created/cached ModelProperty instance for propertyFileId "${propertyFileId}"`);
|
|
2325
2327
|
}
|
|
2326
2328
|
else {
|
|
2327
|
-
logger$
|
|
2329
|
+
logger$d(`[Model._refreshPropertiesFromDb] ModelProperty.createById returned undefined for propertyFileId "${propertyFileId}"`);
|
|
2328
2330
|
}
|
|
2329
2331
|
}
|
|
2330
2332
|
catch (error) {
|
|
2331
|
-
logger$
|
|
2333
|
+
logger$d(`[Model._refreshPropertiesFromDb] Error creating ModelProperty instance for propertyFileId "${propertyFileId}": ${error}`);
|
|
2332
2334
|
}
|
|
2333
2335
|
});
|
|
2334
2336
|
await Promise.all(createPromises);
|
|
2335
2337
|
}
|
|
2336
2338
|
}
|
|
2337
2339
|
catch (error) {
|
|
2338
|
-
logger$
|
|
2340
|
+
logger$d(`[Model._refreshPropertiesFromDb] Error importing ModelProperty or creating instances: ${error}`);
|
|
2339
2341
|
}
|
|
2340
2342
|
}
|
|
2341
2343
|
// Update context with refreshed property IDs AFTER creating instances (the awaits above can
|
|
@@ -2349,7 +2351,7 @@ class Model {
|
|
|
2349
2351
|
});
|
|
2350
2352
|
}
|
|
2351
2353
|
catch (error) {
|
|
2352
|
-
logger$
|
|
2354
|
+
logger$d(`[Model._refreshPropertiesFromDb] Error refreshing properties: ${error}`);
|
|
2353
2355
|
}
|
|
2354
2356
|
}
|
|
2355
2357
|
/**
|
|
@@ -2382,7 +2384,6 @@ var Model$1 = /*#__PURE__*/Object.freeze({
|
|
|
2382
2384
|
});
|
|
2383
2385
|
|
|
2384
2386
|
// Export the Model class
|
|
2385
|
-
const TModelValues = Type.Record(Type.String(), Type.Any());
|
|
2386
2387
|
const TModelSchema = Type.Record(Type.String(), TProperty);
|
|
2387
2388
|
|
|
2388
2389
|
let writeTail = Promise.resolve();
|
|
@@ -2585,7 +2586,7 @@ function toRunSyncArg(options) {
|
|
|
2585
2586
|
return undefined;
|
|
2586
2587
|
}
|
|
2587
2588
|
const runEasSync = fromPromise(async ({ input, }) => {
|
|
2588
|
-
const { runSyncFromEas } = await import('./syncDbWithEas-
|
|
2589
|
+
const { runSyncFromEas } = await import('./syncDbWithEas-V_9gIyHp.js');
|
|
2589
2590
|
// Sync checks for rows and then inserts the rest (no unique index on uids), so syncs in
|
|
2590
2591
|
// different tabs take turns. A sync can run for minutes, so wait without a timeout.
|
|
2591
2592
|
await withDbTabLock('eas-sync', async () => {
|
|
@@ -2901,14 +2902,14 @@ function applySeedConfigDefaults(config) {
|
|
|
2901
2902
|
return { ...config, filesDir, endpoints: next };
|
|
2902
2903
|
}
|
|
2903
2904
|
|
|
2904
|
-
const logger$
|
|
2905
|
+
const logger$c = debug('seedSdk:ClientManager:initialize');
|
|
2905
2906
|
/**
|
|
2906
2907
|
* The first init step: configures the platform services, EAS reads and the Arweave gateway from
|
|
2907
2908
|
* the init options, starts tab coordination and the item event handlers, and returns the context
|
|
2908
2909
|
* the following steps read.
|
|
2909
2910
|
*/
|
|
2910
2911
|
async function platformClassesInit({ event }) {
|
|
2911
|
-
logger$
|
|
2912
|
+
logger$c('initialize from ClientManager');
|
|
2912
2913
|
if (!event || !('options' in event)) {
|
|
2913
2914
|
throw new Error('Initialize event must include options');
|
|
2914
2915
|
}
|
|
@@ -2918,7 +2919,7 @@ async function platformClassesInit({ event }) {
|
|
|
2918
2919
|
if (!config) {
|
|
2919
2920
|
throw new Error('client.init requires a config');
|
|
2920
2921
|
}
|
|
2921
|
-
const { configurePlatform } = await import('./configurePlatform-
|
|
2922
|
+
const { configurePlatform } = await import('./configurePlatform-BH1YOZpg.js');
|
|
2922
2923
|
if (isBrowser()) {
|
|
2923
2924
|
const { createPlatformServices } = await import('./platform.browser.js');
|
|
2924
2925
|
configurePlatform(createPlatformServices());
|
|
@@ -2994,17 +2995,12 @@ async function platformClassesInit({ event }) {
|
|
|
2994
2995
|
};
|
|
2995
2996
|
}
|
|
2996
2997
|
|
|
2997
|
-
const logger$
|
|
2998
|
+
const logger$b = debug('seedSdk:client:writeToDb');
|
|
2998
2999
|
const saveAppState = fromCallback(({ sendBack, input: { key, value } }) => {
|
|
2999
3000
|
const _saveAppState = async () => {
|
|
3000
3001
|
try {
|
|
3001
3002
|
const appDb = BaseDb.getAppDb();
|
|
3002
3003
|
if (!appDb) {
|
|
3003
|
-
// In test environments, continue anyway
|
|
3004
|
-
if (isTestOrSeedDev()) {
|
|
3005
|
-
logger$a('[client/actors] [saveAppState] App DB not found, but continuing in test environment');
|
|
3006
|
-
return;
|
|
3007
|
-
}
|
|
3008
3004
|
throw new Error('App DB not found');
|
|
3009
3005
|
}
|
|
3010
3006
|
const result = await appDb.insert(appState)
|
|
@@ -3018,15 +3014,10 @@ const saveAppState = fromCallback(({ sendBack, input: { key, value } }) => {
|
|
|
3018
3014
|
value: JSON.stringify(value),
|
|
3019
3015
|
},
|
|
3020
3016
|
});
|
|
3021
|
-
logger$
|
|
3017
|
+
logger$b('result', result);
|
|
3022
3018
|
}
|
|
3023
3019
|
catch (error) {
|
|
3024
|
-
logger$
|
|
3025
|
-
// In test environments, continue anyway
|
|
3026
|
-
if (isTestOrSeedDev()) {
|
|
3027
|
-
logger$a('[client/actors] [saveAppState] Continuing despite error in test environment');
|
|
3028
|
-
return;
|
|
3029
|
-
}
|
|
3020
|
+
logger$b('[client/actors] [saveAppState] Error saving app state:', error);
|
|
3030
3021
|
throw error;
|
|
3031
3022
|
}
|
|
3032
3023
|
};
|
|
@@ -3039,24 +3030,13 @@ const saveAppState = fromCallback(({ sendBack, input: { key, value } }) => {
|
|
|
3039
3030
|
});
|
|
3040
3031
|
})
|
|
3041
3032
|
.catch((error) => {
|
|
3042
|
-
logger$
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
value,
|
|
3050
|
-
});
|
|
3051
|
-
}
|
|
3052
|
-
else {
|
|
3053
|
-
sendBack({
|
|
3054
|
-
type: 'saveAppStateError',
|
|
3055
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
3056
|
-
key,
|
|
3057
|
-
value,
|
|
3058
|
-
});
|
|
3059
|
-
}
|
|
3033
|
+
logger$b('[client/actors] [saveAppState] Error in promise chain:', error);
|
|
3034
|
+
sendBack({
|
|
3035
|
+
type: 'saveAppStateError',
|
|
3036
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
3037
|
+
key,
|
|
3038
|
+
value,
|
|
3039
|
+
});
|
|
3060
3040
|
});
|
|
3061
3041
|
});
|
|
3062
3042
|
|
|
@@ -3068,7 +3048,7 @@ async function fileSystemInit({ filesDir }) {
|
|
|
3068
3048
|
await BaseFileManager.initializeFileSystem(filesDir);
|
|
3069
3049
|
}
|
|
3070
3050
|
|
|
3071
|
-
const logger$
|
|
3051
|
+
const logger$a = debug('seedSdk:client:actors:dbInit');
|
|
3072
3052
|
/** Init step: opens and migrates the app database (BaseDb.prepareDb). */
|
|
3073
3053
|
async function dbInit({ filesDir, dbConfig }) {
|
|
3074
3054
|
if (!filesDir) {
|
|
@@ -3078,7 +3058,7 @@ async function dbInit({ filesDir, dbConfig }) {
|
|
|
3078
3058
|
if (!BaseDb.getAppDb()) {
|
|
3079
3059
|
throw new Error('Database not available after preparation');
|
|
3080
3060
|
}
|
|
3081
|
-
logger$
|
|
3061
|
+
logger$a('Database prepared and ready');
|
|
3082
3062
|
}
|
|
3083
3063
|
|
|
3084
3064
|
/**
|
|
@@ -3136,7 +3116,7 @@ const localModelSchemaLookups = async (modelNames) => {
|
|
|
3136
3116
|
return names.filter((name) => name !== 'Seed').map((name) => ({ name, level: levels.model(name) }));
|
|
3137
3117
|
};
|
|
3138
3118
|
|
|
3139
|
-
const logger$
|
|
3119
|
+
const logger$9 = debug('seedSdk:client:actors:addModelsToDb');
|
|
3140
3120
|
/**
|
|
3141
3121
|
* Returns every `models` row named in `modelNames`, inserting a null-schemaFileId stub for each name
|
|
3142
3122
|
* with no row yet (the schema import adopts stubs via findOrCreateModelRow). Check-then-insert:
|
|
@@ -3185,7 +3165,7 @@ async function addModelsToDb(context) {
|
|
|
3185
3165
|
for (const modelName of modelNames) {
|
|
3186
3166
|
const foundModel = rowFor(modelName);
|
|
3187
3167
|
if (!foundModel) {
|
|
3188
|
-
logger$
|
|
3168
|
+
logger$9('[client/actors] [addModelsToDb] Warning: Could not find or create model:', modelName);
|
|
3189
3169
|
hasModelsInDb = false;
|
|
3190
3170
|
continue;
|
|
3191
3171
|
}
|
|
@@ -3193,7 +3173,7 @@ async function addModelsToDb(context) {
|
|
|
3193
3173
|
}
|
|
3194
3174
|
// If we have no models to process, still send success (empty models is valid)
|
|
3195
3175
|
if (modelNamesWithRows.size === 0) {
|
|
3196
|
-
logger$
|
|
3176
|
+
logger$9('[client/actors] [addModelsToDb] No models to process, but continuing');
|
|
3197
3177
|
return;
|
|
3198
3178
|
}
|
|
3199
3179
|
// Fetch schemas from EAS in background - do not block init. EAS can be slow (10s+);
|
|
@@ -3205,7 +3185,7 @@ async function addModelsToDb(context) {
|
|
|
3205
3185
|
timeout = setTimeout(() => reject(new Error('EAS query timeout after 10 seconds')), 10000);
|
|
3206
3186
|
});
|
|
3207
3187
|
// The lookup and the model_uids rule sync uses too (each name at its nameMatch level).
|
|
3208
|
-
const { getModelSchemasFromEas } = await import('./eas-
|
|
3188
|
+
const { getModelSchemasFromEas } = await import('./eas-MXEdKKVc.js');
|
|
3209
3189
|
const lookups = await localModelSchemaLookups([...modelNamesWithRows]);
|
|
3210
3190
|
// Cleared, so a finished lookup doesn't keep a Node process alive for 10 s.
|
|
3211
3191
|
const schemas = await Promise.race([getModelSchemasFromEas(lookups), timeoutPromise]).finally(() => clearTimeout(timeout));
|
|
@@ -3217,16 +3197,11 @@ async function addModelsToDb(context) {
|
|
|
3217
3197
|
requestEasSyncFromModelsInit();
|
|
3218
3198
|
}
|
|
3219
3199
|
else {
|
|
3220
|
-
logger$
|
|
3200
|
+
logger$9('[client/actors] [addModelsToDb] No schemas found from EAS, but continuing');
|
|
3221
3201
|
}
|
|
3222
3202
|
}
|
|
3223
3203
|
catch (error) {
|
|
3224
|
-
|
|
3225
|
-
logger$8('[client/actors] [addModelsToDb] Warning: Could not fetch schemas from EAS (background):', error.message);
|
|
3226
|
-
}
|
|
3227
|
-
else {
|
|
3228
|
-
logger$8('[client/actors] [addModelsToDb] Error fetching schemas (background):', error.message);
|
|
3229
|
-
}
|
|
3204
|
+
logger$9('[client/actors] [addModelsToDb] Error fetching schemas (background):', error.message);
|
|
3230
3205
|
// Probably offline: the startup sync waits for this lookup, so try again.
|
|
3231
3206
|
modelUidsRetry.schedule();
|
|
3232
3207
|
}
|
|
@@ -3240,18 +3215,16 @@ async function addModelsToDb(context) {
|
|
|
3240
3215
|
await _addModelsToDb();
|
|
3241
3216
|
// Model.findByModelType reads the nameMatch levels synchronously: have them before init is ready.
|
|
3242
3217
|
await loadNameMatchLevels().catch((error) => {
|
|
3243
|
-
logger$
|
|
3218
|
+
logger$9('[client/actors] [addModelsToDb] Could not load nameMatch levels:', error);
|
|
3244
3219
|
});
|
|
3245
3220
|
}
|
|
3246
3221
|
catch (error) {
|
|
3247
|
-
logger$
|
|
3248
|
-
|
|
3249
|
-
if (!isTestOrSeedDev())
|
|
3250
|
-
throw error;
|
|
3222
|
+
logger$9('[client/actors] [addModelsToDb] Error:', error);
|
|
3223
|
+
throw error;
|
|
3251
3224
|
}
|
|
3252
3225
|
}
|
|
3253
3226
|
|
|
3254
|
-
const logger$
|
|
3227
|
+
const logger$8 = debug('seedSdk:helpers:localDbChain');
|
|
3255
3228
|
/** appState key recording which EAS chain the local database holds attestations from. */
|
|
3256
3229
|
const EAS_CHAIN_ID_APP_STATE_KEY = 'easChainId';
|
|
3257
3230
|
/**
|
|
@@ -3321,7 +3294,7 @@ async function loadLocalDbChain() {
|
|
|
3321
3294
|
const localChainId = await resolveLocalDbChainId(appDb);
|
|
3322
3295
|
if (localChainId !== undefined) {
|
|
3323
3296
|
configureEasReadChain('localDb', { chainId: localChainId });
|
|
3324
|
-
logger$
|
|
3297
|
+
logger$8('local DB holds chain %d', localChainId);
|
|
3325
3298
|
return;
|
|
3326
3299
|
}
|
|
3327
3300
|
if (isEasReadChainConfigured()) {
|
|
@@ -3370,7 +3343,7 @@ function resetAddressesPersistedAnnouncement() {
|
|
|
3370
3343
|
lastAnnounced = undefined;
|
|
3371
3344
|
}
|
|
3372
3345
|
|
|
3373
|
-
const logger$
|
|
3346
|
+
const logger$7 = debug('seedSdk:client:actors:saveConfig');
|
|
3374
3347
|
let warnedKeptStoredAddresses = false;
|
|
3375
3348
|
/**
|
|
3376
3349
|
* Saves the addresses passed to init.
|
|
@@ -3415,22 +3388,17 @@ async function persistInitAddresses(appDb, { addresses, addressesProvided, owned
|
|
|
3415
3388
|
* checks the database's chain against the configured one.
|
|
3416
3389
|
*/
|
|
3417
3390
|
async function saveConfig(context) {
|
|
3418
|
-
logger$
|
|
3391
|
+
logger$7('saveConfig starting');
|
|
3419
3392
|
const { endpoints, addresses, addressesProvided, ownedAddresses, watchedAddresses, arweaveDomain, uploadApiBaseUrl, gatewayTransport, gatewayHyperKey, gatewaySidecarHost, gatewaySidecarPort, gatewayProxyBaseUrl, } = context;
|
|
3420
3393
|
// Validate endpoints - required for proper initialization
|
|
3421
3394
|
if (!endpoints || !endpoints.filePaths || !endpoints.files) {
|
|
3422
|
-
logger$
|
|
3395
|
+
logger$7('[internal/actors] [saveConfig] Invalid endpoints:', { endpoints });
|
|
3423
3396
|
throw new Error('saveConfig called with invalid endpoints: endpoints must include both filePaths and files');
|
|
3424
3397
|
}
|
|
3425
3398
|
const _saveConfig = async () => {
|
|
3426
3399
|
try {
|
|
3427
3400
|
const appDb = BaseDb.getAppDb();
|
|
3428
3401
|
if (!appDb) {
|
|
3429
|
-
// In test environments, continue anyway
|
|
3430
|
-
if (isTestOrSeedDev()) {
|
|
3431
|
-
logger$6('[internal/actors] [saveConfig] App DB not found, but continuing in test environment');
|
|
3432
|
-
return;
|
|
3433
|
-
}
|
|
3434
3402
|
throw new Error('App DB not found');
|
|
3435
3403
|
}
|
|
3436
3404
|
const endpointsValueString = JSON.stringify(endpoints);
|
|
@@ -3483,21 +3451,16 @@ async function saveConfig(context) {
|
|
|
3483
3451
|
await loadLocalDbChain();
|
|
3484
3452
|
}
|
|
3485
3453
|
catch (error) {
|
|
3486
|
-
logger$
|
|
3487
|
-
// In test environments, continue anyway
|
|
3488
|
-
if (isTestOrSeedDev()) {
|
|
3489
|
-
logger$6('[internal/actors] [saveConfig] Continuing despite error in test environment');
|
|
3490
|
-
return;
|
|
3491
|
-
}
|
|
3454
|
+
logger$7('[internal/actors] [saveConfig] Error saving config:', error);
|
|
3492
3455
|
throw error;
|
|
3493
3456
|
}
|
|
3494
3457
|
};
|
|
3495
3458
|
// Init writes run one tab at a time (docs/MULTI_TAB.md).
|
|
3496
3459
|
await withSeedDbLock('init', context.filesDir, _saveConfig);
|
|
3497
|
-
logger$
|
|
3460
|
+
logger$7('[internal/actors] [saveConfig] saveConfig success');
|
|
3498
3461
|
}
|
|
3499
3462
|
|
|
3500
|
-
const logger$
|
|
3463
|
+
const logger$6 = debug('seedSdk:helpers:schema');
|
|
3501
3464
|
/** Filesystem-safe schema name: other characters become `_`, runs of `_` collapse, ends trimmed. */
|
|
3502
3465
|
const sanitizeSchemaName = (name) => name
|
|
3503
3466
|
.replace(/[^a-zA-Z0-9\s_-]/g, '_')
|
|
@@ -3551,7 +3514,7 @@ async function readSchemaFile(filePath) {
|
|
|
3551
3514
|
return schema;
|
|
3552
3515
|
}
|
|
3553
3516
|
catch (error) {
|
|
3554
|
-
logger$
|
|
3517
|
+
logger$6(`Could not read schema file ${filePath}:`, error);
|
|
3555
3518
|
return undefined;
|
|
3556
3519
|
}
|
|
3557
3520
|
}
|
|
@@ -3565,7 +3528,7 @@ async function listCompleteSchemaFiles() {
|
|
|
3565
3528
|
const workingDir = BaseFileManager.getWorkingDir();
|
|
3566
3529
|
try {
|
|
3567
3530
|
if (!(await BaseFileManager.pathExists(workingDir))) {
|
|
3568
|
-
logger$
|
|
3531
|
+
logger$6(`Working directory does not exist yet: ${workingDir}, returning empty schema list`);
|
|
3569
3532
|
return [];
|
|
3570
3533
|
}
|
|
3571
3534
|
const files = await fs.promises.readdir(workingDir);
|
|
@@ -3636,7 +3599,7 @@ async function loadAllSchemasFromDb() {
|
|
|
3636
3599
|
});
|
|
3637
3600
|
}
|
|
3638
3601
|
catch (error) {
|
|
3639
|
-
logger$
|
|
3602
|
+
logger$6(`Error resolving schema row ${row.name}:`, error);
|
|
3640
3603
|
}
|
|
3641
3604
|
}
|
|
3642
3605
|
return result;
|
|
@@ -3668,7 +3631,7 @@ var schema = /*#__PURE__*/Object.freeze({
|
|
|
3668
3631
|
readSchemaFile: readSchemaFile
|
|
3669
3632
|
});
|
|
3670
3633
|
|
|
3671
|
-
function toBytes(data) {
|
|
3634
|
+
function toBytes$1(data) {
|
|
3672
3635
|
if (typeof data === 'string')
|
|
3673
3636
|
return utf8ToBytes(data);
|
|
3674
3637
|
if (data instanceof Uint8Array)
|
|
@@ -3676,7 +3639,7 @@ function toBytes(data) {
|
|
|
3676
3639
|
return new Uint8Array(data);
|
|
3677
3640
|
}
|
|
3678
3641
|
const getContentHash = async (data) => {
|
|
3679
|
-
return bytesToHex(sha3_256(toBytes(data)));
|
|
3642
|
+
return bytesToHex(sha3_256(toBytes$1(data)));
|
|
3680
3643
|
};
|
|
3681
3644
|
/**
|
|
3682
3645
|
* Generate a deterministic ID from a seed string.
|
|
@@ -3700,7 +3663,7 @@ const getDeterministicId = (seed) => {
|
|
|
3700
3663
|
* Schema identity is the file id. Missing ids are assigned once, at first apply, and written into
|
|
3701
3664
|
* the stored file.
|
|
3702
3665
|
*/
|
|
3703
|
-
const logger$
|
|
3666
|
+
const logger$5 = debug('seedSdk:helpers:schemaApply');
|
|
3704
3667
|
const SCHEMA_FILE_FORMAT = 'https://seedprotocol.org/schemas/data-model/v1';
|
|
3705
3668
|
/**
|
|
3706
3669
|
* A complete schema file from any accepted shape: models and properties as objects, `version`
|
|
@@ -3920,8 +3883,8 @@ async function applySchemaFile(source, options = {}) {
|
|
|
3920
3883
|
if (!schemaRecord) {
|
|
3921
3884
|
warnOnEasNameCollisions(file);
|
|
3922
3885
|
warnOnInvalidNameMatch(file);
|
|
3923
|
-
const _mod_14 = await import('./
|
|
3924
|
-
const _ns_14 = _mod_14.
|
|
3886
|
+
const _mod_14 = await import('./Item-BU9wIT7l.js');
|
|
3887
|
+
const _ns_14 = _mod_14.bS;
|
|
3925
3888
|
const { addModelsToDb, broadcastSchemasInvalidate } = _ns_14;
|
|
3926
3889
|
const modelFileIds = new Map(Object.entries(file.models).map(([name, model]) => [name, model.id]));
|
|
3927
3890
|
const propertyDefinitions = schemaFilePropertyDefinitions(file);
|
|
@@ -3959,10 +3922,10 @@ async function applySchemaFile(source, options = {}) {
|
|
|
3959
3922
|
broadcastSchemasInvalidate();
|
|
3960
3923
|
// A cached instance is otherwise never re-read from its row (createById returns it as is)
|
|
3961
3924
|
await refreshCachedProperties(updatedPropertyFileIds);
|
|
3962
|
-
logger$
|
|
3925
|
+
logger$5(`Applied schema ${file.metadata.name} v${file.version} (${file.id})`);
|
|
3963
3926
|
}
|
|
3964
3927
|
else {
|
|
3965
|
-
logger$
|
|
3928
|
+
logger$5(`Schema ${file.metadata.name} v${file.version} (${file.id}) already applied`);
|
|
3966
3929
|
}
|
|
3967
3930
|
const models = await createModelsForSchemaFile(file);
|
|
3968
3931
|
// Callers use the models next (an item create reads their properties): wait until each has them.
|
|
@@ -3993,7 +3956,7 @@ async function storeSchemaFile(file, onChangedContent) {
|
|
|
3993
3956
|
}
|
|
3994
3957
|
if (onChangedContent === 'bump') {
|
|
3995
3958
|
file.version = Math.max(...versions.map((info) => info.version)) + 1;
|
|
3996
|
-
logger$
|
|
3959
|
+
logger$5(`Schema ${file.metadata.name} content changed, storing it as v${file.version}`);
|
|
3997
3960
|
return writeSchemaFile(file);
|
|
3998
3961
|
}
|
|
3999
3962
|
console.warn(`[seed] schema ${file.metadata.name} v${file.version}: content changed but id ` +
|
|
@@ -4015,7 +3978,7 @@ async function unlinkModelsFromSchema(db, schemaId, modelNames) {
|
|
|
4015
3978
|
if (links.length === 0)
|
|
4016
3979
|
return;
|
|
4017
3980
|
await db.delete(modelSchemas).where(inArray(modelSchemas.id, links.map((link) => link.id)));
|
|
4018
|
-
logger$
|
|
3981
|
+
logger$5(`Unlinked models dropped from the schema (schema id ${schemaId}): ${modelNames.join(', ')}`);
|
|
4019
3982
|
}
|
|
4020
3983
|
/**
|
|
4021
3984
|
* A schema row's definition. A draft row's is its `schema_data`; a published row's is its
|
|
@@ -4034,7 +3997,7 @@ async function resolveSchemaRow(row, db = BaseDb.getAppDb()) {
|
|
|
4034
3997
|
return { ...data, models: normalizeSchemaModels(data.models) };
|
|
4035
3998
|
}
|
|
4036
3999
|
catch (error) {
|
|
4037
|
-
logger$
|
|
4000
|
+
logger$5(`Could not parse schema_data of ${row.name}:`, error);
|
|
4038
4001
|
return undefined;
|
|
4039
4002
|
}
|
|
4040
4003
|
};
|
|
@@ -4072,8 +4035,8 @@ async function resolveSchemaRow(row, db = BaseDb.getAppDb()) {
|
|
|
4072
4035
|
metadata: { ...schema.metadata, name: row.name },
|
|
4073
4036
|
};
|
|
4074
4037
|
if (row.id && db) {
|
|
4075
|
-
const _mod_15 = await import('./
|
|
4076
|
-
const _ns_15 = _mod_15.
|
|
4038
|
+
const _mod_15 = await import('./Item-BU9wIT7l.js');
|
|
4039
|
+
const _ns_15 = _mod_15.bS;
|
|
4077
4040
|
const { loadModelsFromDbForSchema, mergeDbModelsIntoSchemaModels } = _ns_15;
|
|
4078
4041
|
const dbModels = await loadModelsFromDbForSchema(row.id, db);
|
|
4079
4042
|
if (Object.keys(dbModels).length > 0) {
|
|
@@ -4140,7 +4103,7 @@ async function writeNextSchemaVersion(schemaNameOrId, patch, change) {
|
|
|
4140
4103
|
],
|
|
4141
4104
|
};
|
|
4142
4105
|
const filePath = await writeSchemaFile(schemaFile);
|
|
4143
|
-
logger$
|
|
4106
|
+
logger$5(`Wrote schema ${schemaFile.metadata.name} v${version} at ${filePath}`);
|
|
4144
4107
|
if (change.apply !== false) {
|
|
4145
4108
|
await applySchemaFile(schemaFile, { store: false, modelRenames: change.modelRenames });
|
|
4146
4109
|
}
|
|
@@ -4443,7 +4406,7 @@ var SEEDPROTOCOL_Seed_Protocol_v1 = /*#__PURE__*/Object.freeze({
|
|
|
4443
4406
|
version: version
|
|
4444
4407
|
});
|
|
4445
4408
|
|
|
4446
|
-
const logger$
|
|
4409
|
+
const logger$4 = debug('seedSdk:client:actors:processSchemaFiles');
|
|
4447
4410
|
let processSchemaFilesChain = Promise.resolve();
|
|
4448
4411
|
function resolveSchemaFilePath(schemaFile) {
|
|
4449
4412
|
const path = BaseFileManager.getPathModule();
|
|
@@ -4475,7 +4438,7 @@ async function processSchemaFilesOnce(context) {
|
|
|
4475
4438
|
appliedNames.add(applied.schemaFile.metadata.name);
|
|
4476
4439
|
}
|
|
4477
4440
|
catch (error) {
|
|
4478
|
-
logger$
|
|
4441
|
+
logger$4('Error loading internal seed-protocol schema:', error);
|
|
4479
4442
|
}
|
|
4480
4443
|
// The app's schema (canonical) takes precedence over schemaFile
|
|
4481
4444
|
if (context.schema) {
|
|
@@ -4514,14 +4477,14 @@ async function processSchemaFilesOnce(context) {
|
|
|
4514
4477
|
loaded = await loadAllSchemasFromDb();
|
|
4515
4478
|
}
|
|
4516
4479
|
catch (error) {
|
|
4517
|
-
logger$
|
|
4480
|
+
logger$4('Error loading schemas from database (continuing anyway):', error);
|
|
4518
4481
|
}
|
|
4519
4482
|
for (const { schema } of loaded) {
|
|
4520
4483
|
try {
|
|
4521
4484
|
Object.assign(allModels, await createModelsForSchemaFile(schema));
|
|
4522
4485
|
}
|
|
4523
4486
|
catch (error) {
|
|
4524
|
-
logger$
|
|
4487
|
+
logger$4(`Error creating models for schema ${schema.metadata?.name}:`, error);
|
|
4525
4488
|
}
|
|
4526
4489
|
}
|
|
4527
4490
|
return allModels;
|
|
@@ -4532,7 +4495,7 @@ async function processSchemaFilesOnce(context) {
|
|
|
4532
4495
|
* (docs/MULTI_TAB.md), and so do overlapping inits in this tab.
|
|
4533
4496
|
*/
|
|
4534
4497
|
function processSchemaFiles(context) {
|
|
4535
|
-
logger$
|
|
4498
|
+
logger$4('processSchemaFiles started');
|
|
4536
4499
|
const run = processSchemaFilesChain.then(() => withSeedDbLock('init', context.filesDir, () => processSchemaFilesOnce(context)));
|
|
4537
4500
|
// The chain only orders runs; a failed run must not fail the next init too.
|
|
4538
4501
|
processSchemaFilesChain = run.then(() => { }, () => { });
|
|
@@ -4552,7 +4515,7 @@ async function applyPendingSchemaFiles(appliedNames) {
|
|
|
4552
4515
|
files = await listLatestSchemaFiles();
|
|
4553
4516
|
}
|
|
4554
4517
|
catch (error) {
|
|
4555
|
-
logger$
|
|
4518
|
+
logger$4('Error listing schema files (continuing anyway):', error);
|
|
4556
4519
|
return;
|
|
4557
4520
|
}
|
|
4558
4521
|
const rows = await db
|
|
@@ -4571,12 +4534,12 @@ async function applyPendingSchemaFiles(appliedNames) {
|
|
|
4571
4534
|
await applySchemaFile({ ...file, id: file.id ?? info.schemaFileId }, { store: false });
|
|
4572
4535
|
}
|
|
4573
4536
|
catch (error) {
|
|
4574
|
-
logger$
|
|
4537
|
+
logger$4(`Error applying schema file ${info.filePath}:`, error);
|
|
4575
4538
|
}
|
|
4576
4539
|
}
|
|
4577
4540
|
}
|
|
4578
4541
|
|
|
4579
|
-
const logger$
|
|
4542
|
+
const logger$3 = debug('seedSdk:db:canonicalizeModelTypes');
|
|
4580
4543
|
/**
|
|
4581
4544
|
* Model types in the local DB (`seeds.type`, `versions.seed_type`, `metadata.model_type`) are the
|
|
4582
4545
|
* EAS name of the model (ADR 0008): `html_page` for HTMLPage. Rows written before that hold other
|
|
@@ -4614,7 +4577,7 @@ async function canonicalizeModelTypes() {
|
|
|
4614
4577
|
await appDb.update(seeds).set({ type: next }).where(eq(seeds.type, type));
|
|
4615
4578
|
await appDb.update(versions).set({ seedType: next }).where(eq(versions.seedType, type));
|
|
4616
4579
|
await appDb.update(metadata$1).set({ modelType: next }).where(eq(metadata$1.modelType, type));
|
|
4617
|
-
logger$
|
|
4580
|
+
logger$3(`model type ${type} → ${next}`);
|
|
4618
4581
|
rewritten++;
|
|
4619
4582
|
}
|
|
4620
4583
|
return rewritten;
|
|
@@ -4627,7 +4590,7 @@ const runInBackground = () => {
|
|
|
4627
4590
|
.then(() => undefined)
|
|
4628
4591
|
.catch((error) => {
|
|
4629
4592
|
// Never fails init: readers still match old types by name, only exact type filters miss them.
|
|
4630
|
-
logger$
|
|
4593
|
+
logger$3('canonicalizing model types failed', error);
|
|
4631
4594
|
})
|
|
4632
4595
|
.finally(() => {
|
|
4633
4596
|
running = undefined;
|
|
@@ -4679,10 +4642,26 @@ const getVersionData = async ({ localId, seedLocalId, uid, }, db = BaseDb.getApp
|
|
|
4679
4642
|
return queryRows[0];
|
|
4680
4643
|
};
|
|
4681
4644
|
|
|
4682
|
-
|
|
4645
|
+
/**
|
|
4646
|
+
* The publisher a seed's version and property rows take: the seed's own (its `publisher`, else the
|
|
4647
|
+
* attester of its stored attestation). Undefined for a draft that belongs to no wallet yet; those
|
|
4648
|
+
* rows are then claimed together with their seed (claimUnpublishedDrafts).
|
|
4649
|
+
*/
|
|
4650
|
+
async function getSeedPublisher(seedLocalId,
|
|
4651
|
+
/** The database or transaction to read; the app database by default. */
|
|
4652
|
+
db = BaseDb.getAppDb()) {
|
|
4653
|
+
if (!seedLocalId)
|
|
4654
|
+
return undefined;
|
|
4655
|
+
const row = await findSeedRow({ localId: seedLocalId }, db);
|
|
4656
|
+
if (!row)
|
|
4657
|
+
return undefined;
|
|
4658
|
+
return normalizePublisher(resolvePublisherFromSeedRow(row));
|
|
4659
|
+
}
|
|
4660
|
+
|
|
4661
|
+
const logger$2 = debug('seedSdk:write:updateItemPropertyValue');
|
|
4683
4662
|
const updateItemPropertyValue = async ({ localId: localIdParam, propertyName, newValue, seedUid, seedLocalId, modelName, refSeedType, refResolvedValue, refResolvedDisplayValue, versionLocalId, versionUid, schemaUid, localStorageDir, dataType, refValueType, retract, }, options = {}) => {
|
|
4684
4663
|
if (!localIdParam && !seedLocalId) {
|
|
4685
|
-
logger$
|
|
4664
|
+
logger$2(`[db/write] [updateItemPropertyValue] no propertyLocalId or seedLocalId for property: ${propertyName}`);
|
|
4686
4665
|
return;
|
|
4687
4666
|
}
|
|
4688
4667
|
// Every write below binds parameters: store the value as given (no SQL escaping).
|
|
@@ -4739,7 +4718,9 @@ const updateItemPropertyValue = async ({ localId: localIdParam, propertyName, ne
|
|
|
4739
4718
|
rows = newestRows.slice(0, 1);
|
|
4740
4719
|
}
|
|
4741
4720
|
if (rows && rows.length > 0) {
|
|
4742
|
-
const { localId, uid, propertyId: propertyIdFromDb, propertyName: propertyNameFromDb, propertyValue: propertyValueFromDb, modelType, seedLocalId: seedLocalIdFromDb, versionLocalId, schemaUid, easDataType, refSeedType: refSeedTypeFromDb, refResolvedValue: refResolvedValueFromDb, retract: retractFromDb, } = rows[0];
|
|
4721
|
+
const { localId, uid, propertyId: propertyIdFromDb, propertyName: propertyNameFromDb, propertyValue: propertyValueFromDb, modelType, seedLocalId: seedLocalIdFromDb, versionLocalId, schemaUid: schemaUidFromDb, easDataType, refSeedType: refSeedTypeFromDb, refResolvedValue: refResolvedValueFromDb, retract: retractFromDb, } = rows[0];
|
|
4722
|
+
// The resolved schema uid fills in a row stored before its schema was known.
|
|
4723
|
+
const rowSchemaUid = schemaUidFromDb || schemaUid;
|
|
4743
4724
|
// Stored model types are the model's EAS name (ADR 0008): `blog_post` for BlogPost.
|
|
4744
4725
|
const modelTypeForName = modelName ? easName(modelName) : undefined;
|
|
4745
4726
|
if (propertyValueFromDb === newValue &&
|
|
@@ -4748,8 +4729,8 @@ const updateItemPropertyValue = async ({ localId: localIdParam, propertyName, ne
|
|
|
4748
4729
|
(refSeedTypeFromDb ?? null) === (refSeedType ?? null) &&
|
|
4749
4730
|
(refResolvedValueFromDb ?? null) === (refResolvedValue ?? null) &&
|
|
4750
4731
|
!!retractFromDb === !!retractValue) {
|
|
4751
|
-
logger$
|
|
4752
|
-
return localId == null ? undefined : { localId, uid: uid ?? null, schemaUid:
|
|
4732
|
+
logger$2(`[db/write] [updateItemPropertyValue] value is the same as most recent record for property: ${propertyNameFromDb}`);
|
|
4733
|
+
return localId == null ? undefined : { localId, uid: uid ?? null, schemaUid: schemaUidFromDb ?? '' };
|
|
4753
4734
|
}
|
|
4754
4735
|
// This means we already have a local-only record so we should just update that one
|
|
4755
4736
|
if (!uid) {
|
|
@@ -4774,11 +4755,14 @@ const updateItemPropertyValue = async ({ localId: localIdParam, propertyName, ne
|
|
|
4774
4755
|
if (propertyIdFromDb == null && property.propertyId != null) {
|
|
4775
4756
|
updatePayload.propertyId = property.propertyId;
|
|
4776
4757
|
}
|
|
4758
|
+
if (!schemaUidFromDb && schemaUid) {
|
|
4759
|
+
updatePayload.schemaUid = schemaUid;
|
|
4760
|
+
}
|
|
4777
4761
|
await appDb
|
|
4778
4762
|
.update(metadata$1)
|
|
4779
4763
|
.set(updatePayload)
|
|
4780
4764
|
.where(eq(metadata$1.localId, localId));
|
|
4781
|
-
return { localId, uid: null, schemaUid:
|
|
4765
|
+
return { localId, uid: null, schemaUid: rowSchemaUid ?? '' };
|
|
4782
4766
|
}
|
|
4783
4767
|
const seedDataFromDb = seedLocalId ? await findSeedRow({ localId: seedLocalId }, appDb) : null;
|
|
4784
4768
|
const versionDataFromDb = versionLocalId ? await getVersionData({ localId: versionLocalId }, appDb) : null;
|
|
@@ -4798,7 +4782,7 @@ const updateItemPropertyValue = async ({ localId: localIdParam, propertyName, ne
|
|
|
4798
4782
|
seedLocalId: seedLocalIdFromDb,
|
|
4799
4783
|
versionLocalId,
|
|
4800
4784
|
versionUid: versionDataFromDb?.uid || null,
|
|
4801
|
-
schemaUid,
|
|
4785
|
+
schemaUid: rowSchemaUid,
|
|
4802
4786
|
easDataType: easDataType || null,
|
|
4803
4787
|
refSeedType: refSeedType || null,
|
|
4804
4788
|
refResolvedValue: refResolvedValue || null,
|
|
@@ -4809,7 +4793,7 @@ const updateItemPropertyValue = async ({ localId: localIdParam, propertyName, ne
|
|
|
4809
4793
|
return {
|
|
4810
4794
|
localId: newLocalId,
|
|
4811
4795
|
uid: null,
|
|
4812
|
-
schemaUid:
|
|
4796
|
+
schemaUid: rowSchemaUid ?? '',
|
|
4813
4797
|
};
|
|
4814
4798
|
}
|
|
4815
4799
|
// Here there are no records for this property on this seed so we should create one
|
|
@@ -4868,6 +4852,116 @@ const createVersion = async ({ seedLocalId, seedUid, seedType, uid, }, options =
|
|
|
4868
4852
|
return newVersionLocalId;
|
|
4869
4853
|
};
|
|
4870
4854
|
|
|
4855
|
+
/** Error thrown when metadata validation fails (e.g. enum violation). */
|
|
4856
|
+
class MetadataValidationError extends Error {
|
|
4857
|
+
constructor(message, validationErrors) {
|
|
4858
|
+
super(message);
|
|
4859
|
+
this.validationErrors = validationErrors;
|
|
4860
|
+
this.name = 'MetadataValidationError';
|
|
4861
|
+
}
|
|
4862
|
+
}
|
|
4863
|
+
const createMetadata = async (metadataValues, propertyRecordSchema, options) => {
|
|
4864
|
+
const appDb = options?.db ?? BaseDb.getAppDb();
|
|
4865
|
+
metadataValues.localId = generateId();
|
|
4866
|
+
// A property row belongs to its seed's wallet (none yet for a draft made with no wallet connected).
|
|
4867
|
+
const publisher = await getSeedPublisher(metadataValues.seedLocalId, appDb);
|
|
4868
|
+
if (publisher) {
|
|
4869
|
+
metadataValues.publisher = publisher;
|
|
4870
|
+
}
|
|
4871
|
+
if (!metadataValues.modelType && metadataValues.modelName) {
|
|
4872
|
+
metadataValues.modelType = easName(metadataValues.modelName);
|
|
4873
|
+
}
|
|
4874
|
+
// property_id is the match key (decision D4); the row is written under the property's storage name.
|
|
4875
|
+
const property = await resolveMetadataProperty({
|
|
4876
|
+
propertyId: metadataValues.propertyId,
|
|
4877
|
+
definitionId: propertyRecordSchema?.id,
|
|
4878
|
+
propertyName: metadataValues.propertyName,
|
|
4879
|
+
modelName: metadataValues.modelName ?? metadataValues.modelType,
|
|
4880
|
+
modelFileId: options?.modelFileId,
|
|
4881
|
+
seedLocalId: metadataValues.seedLocalId,
|
|
4882
|
+
seedUid: metadataValues.seedUid,
|
|
4883
|
+
}, appDb, { onAmbiguousModel: 'none' });
|
|
4884
|
+
if (property) {
|
|
4885
|
+
metadataValues.propertyId = property.id;
|
|
4886
|
+
metadataValues.propertyName = property.storageName;
|
|
4887
|
+
}
|
|
4888
|
+
else if (metadataValues.propertyName && propertyRecordSchema) {
|
|
4889
|
+
metadataValues.propertyName = metadataStorageName(metadataValues.propertyName, propertyRecordSchema);
|
|
4890
|
+
}
|
|
4891
|
+
const isItemStorage = propertyRecordSchema && propertyRecordSchema.storageType === 'ItemStorage';
|
|
4892
|
+
// if (
|
|
4893
|
+
// propertyRecordSchema &&
|
|
4894
|
+
// propertyRecordSchema.localStorageDir &&
|
|
4895
|
+
// isItemStorage
|
|
4896
|
+
// ) {
|
|
4897
|
+
// const filename = `${metadataValues.seedUid || metadataValues.seedLocalId}${propertyRecordSchema.filenameSuffix}`
|
|
4898
|
+
// const filePath = path.join(propertyRecordSchema.localStorageDir, filename)
|
|
4899
|
+
// await fs.promises.writeFile(filePath, metadataValues.propertyValue)
|
|
4900
|
+
// metadataValues.propertyValue = filename
|
|
4901
|
+
// metadataValues.refValueType = 'file'
|
|
4902
|
+
// }
|
|
4903
|
+
// Convert propertyValue to string if it's not already (metadata table expects text)
|
|
4904
|
+
if (metadataValues.propertyValue !== undefined && metadataValues.propertyValue !== null) {
|
|
4905
|
+
if (Array.isArray(metadataValues.propertyValue)) {
|
|
4906
|
+
// List values: JSON array is the storage format (String() would give legacy comma-separated ids)
|
|
4907
|
+
metadataValues.propertyValue = JSON.stringify(metadataValues.propertyValue);
|
|
4908
|
+
}
|
|
4909
|
+
else if (typeof metadataValues.propertyValue !== 'string') {
|
|
4910
|
+
metadataValues.propertyValue = String(metadataValues.propertyValue);
|
|
4911
|
+
}
|
|
4912
|
+
}
|
|
4913
|
+
// Validate against property validation rules (enum, pattern, minLength, maxLength) unless skipped
|
|
4914
|
+
if (!options?.skipValidation &&
|
|
4915
|
+
propertyRecordSchema?.validation &&
|
|
4916
|
+
metadataValues.propertyValue != null &&
|
|
4917
|
+
metadataValues.propertyValue !== '') {
|
|
4918
|
+
const { SchemaValidationService } = await import('./SchemaValidationService-Dc-lpFQX.js');
|
|
4919
|
+
const validationService = new SchemaValidationService();
|
|
4920
|
+
const validationResult = validationService.validatePropertyValue(metadataValues.propertyValue, normalizeDataType(propertyRecordSchema.dataType), propertyRecordSchema.validation, propertyRecordSchema.refValueType
|
|
4921
|
+
? normalizeDataType(String(propertyRecordSchema.refValueType))
|
|
4922
|
+
: undefined);
|
|
4923
|
+
if (!validationResult.isValid && validationResult.errors.length > 0) {
|
|
4924
|
+
const validationErrors = validationResult.errors.map((e) => ({
|
|
4925
|
+
field: e.field,
|
|
4926
|
+
message: e.message,
|
|
4927
|
+
code: e.code,
|
|
4928
|
+
}));
|
|
4929
|
+
const message = validationErrors.map((e) => e.message).join('; ');
|
|
4930
|
+
throw new MetadataValidationError(message, validationErrors);
|
|
4931
|
+
}
|
|
4932
|
+
}
|
|
4933
|
+
// With a given db (a transaction), the caller resolves schemaUid first: the lookup may call EAS.
|
|
4934
|
+
// Without one it isn't required for metadata insertion, so a failed lookup leaves it unset.
|
|
4935
|
+
if (!options?.db && !isItemStorage && !metadataValues.schemaUid && metadataValues.propertyName) {
|
|
4936
|
+
const schemaUid = await lookupPropertySchemaUid(metadataValues.propertyName, propertyRecordSchema);
|
|
4937
|
+
if (schemaUid)
|
|
4938
|
+
metadataValues.schemaUid = schemaUid;
|
|
4939
|
+
}
|
|
4940
|
+
let inserted;
|
|
4941
|
+
try {
|
|
4942
|
+
inserted = await appDb
|
|
4943
|
+
.insert(metadata$1)
|
|
4944
|
+
.values({
|
|
4945
|
+
...metadataValues,
|
|
4946
|
+
// Stamped here: a value carried in metadataValues would otherwise win over the column default.
|
|
4947
|
+
createdAt: Date.now(),
|
|
4948
|
+
updatedAt: Date.now(),
|
|
4949
|
+
})
|
|
4950
|
+
.returning();
|
|
4951
|
+
}
|
|
4952
|
+
catch (error) {
|
|
4953
|
+
// Drizzle's message is the full SQL; the driver error that explains it is only on `cause`.
|
|
4954
|
+
const cause = error?.cause;
|
|
4955
|
+
const reason = cause instanceof Error ? cause.message : cause != null ? String(cause) : undefined;
|
|
4956
|
+
throw new Error(`Failed to insert metadata for property ${metadataValues.propertyName}` +
|
|
4957
|
+
(reason ? `: ${reason}` : `: ${error instanceof Error ? error.message : String(error)}`), { cause: error });
|
|
4958
|
+
}
|
|
4959
|
+
if (!inserted || inserted.length === 0) {
|
|
4960
|
+
throw new Error(`Failed to insert metadata record for property ${metadataValues.propertyName}`);
|
|
4961
|
+
}
|
|
4962
|
+
return inserted[0];
|
|
4963
|
+
};
|
|
4964
|
+
|
|
4871
4965
|
/** Absolute path of the file {@link writeItemStorageFile} writes for `target`. */
|
|
4872
4966
|
const getItemStorageFilePath = (target) => BaseFileManager.getFilesPath(getItemStorageSubdir(target.localStorageDir), target.fileName || getItemStorageLocalFileName(target.seedId, target.filenameSuffix));
|
|
4873
4967
|
/**
|
|
@@ -5008,8 +5102,8 @@ async function applyStorageValue(seedLocalId, propertyName, value, db, prepared)
|
|
|
5008
5102
|
const row = prepared
|
|
5009
5103
|
? { ...stored, refResolvedValue: prepared.fileName, schemaUid: prepared.schemaUid, refModelUid: prepared.refModelUid }
|
|
5010
5104
|
: stored;
|
|
5011
|
-
const _mod_16 = await import('./Item-
|
|
5012
|
-
const _ns_16 = _mod_16.
|
|
5105
|
+
const _mod_16 = await import('./Item-BU9wIT7l.js');
|
|
5106
|
+
const _ns_16 = _mod_16.bT;
|
|
5013
5107
|
const { currentPropertyRow } = _ns_16;
|
|
5014
5108
|
const existing = await currentPropertyRow({ seedLocalId }, {
|
|
5015
5109
|
propertyName,
|
|
@@ -5322,8 +5416,8 @@ function startItemOutbox() {
|
|
|
5322
5416
|
return {};
|
|
5323
5417
|
// Cached, shared with item creation, so a save doesn't wait on the EAS indexer each time.
|
|
5324
5418
|
// Not required for the row; publish resolves the schema again.
|
|
5325
|
-
const _mod_17 = await import('./Item-
|
|
5326
|
-
const _ns_17 = _mod_17.
|
|
5419
|
+
const _mod_17 = await import('./Item-BU9wIT7l.js');
|
|
5420
|
+
const _ns_17 = _mod_17.bU;
|
|
5327
5421
|
const { lookupPropertySchemaUid } = _ns_17;
|
|
5328
5422
|
return {
|
|
5329
5423
|
schemaUid: await lookupPropertySchemaUid(op.propertyName, value.propertyRecordSchema),
|
|
@@ -6258,7 +6352,7 @@ async function claimPendingCreates(tx, publisher) {
|
|
|
6258
6352
|
return claimed;
|
|
6259
6353
|
}
|
|
6260
6354
|
|
|
6261
|
-
const logger = debug('seedSdk:client');
|
|
6355
|
+
const logger$1 = debug('seedSdk:client');
|
|
6262
6356
|
const createClientManager = () => {
|
|
6263
6357
|
const actor = createActor(clientManagerMachine, {
|
|
6264
6358
|
input: {
|
|
@@ -6269,8 +6363,8 @@ const createClientManager = () => {
|
|
|
6269
6363
|
}
|
|
6270
6364
|
});
|
|
6271
6365
|
const actorSubscription = actor.subscribe((snapshot) => {
|
|
6272
|
-
logger('ClientManager snapshot.value:', snapshot.value);
|
|
6273
|
-
logger('ClientManager snapshot.context.isInitialized:', snapshot.context.isInitialized);
|
|
6366
|
+
logger$1('ClientManager snapshot.value:', snapshot.value);
|
|
6367
|
+
logger$1('ClientManager snapshot.context.isInitialized:', snapshot.context.isInitialized);
|
|
6274
6368
|
});
|
|
6275
6369
|
actor.start();
|
|
6276
6370
|
return { actor, actorSubscription };
|
|
@@ -6286,7 +6380,7 @@ const notifyServiceListeners = () => {
|
|
|
6286
6380
|
listener(clientManager);
|
|
6287
6381
|
}
|
|
6288
6382
|
catch (error) {
|
|
6289
|
-
logger('subscribeService listener threw:', error);
|
|
6383
|
+
logger$1('subscribeService listener threw:', error);
|
|
6290
6384
|
}
|
|
6291
6385
|
}
|
|
6292
6386
|
};
|
|
@@ -6356,14 +6450,14 @@ const coalescedInit = createCoalescedAsync(async (options) => {
|
|
|
6356
6450
|
const isStopped = snapshot.status === 'stopped';
|
|
6357
6451
|
if (isStopped) {
|
|
6358
6452
|
// stop() (or a failed test) stopped it: start over with a new actor and a clean context.
|
|
6359
|
-
logger('Client manager was stopped, creating a new one');
|
|
6453
|
+
logger$1('Client manager was stopped, creating a new one');
|
|
6360
6454
|
({ actor: clientManager, actorSubscription: subscription } = createClientManager());
|
|
6361
6455
|
notifyServiceListeners();
|
|
6362
6456
|
}
|
|
6363
6457
|
// Clear any stale context from previous failed initializations
|
|
6364
6458
|
// This ensures we start with a clean slate before the new init updates it
|
|
6365
6459
|
if (!isStopped && currentState !== 'uninitialized') {
|
|
6366
|
-
logger(`Client manager is in state ${currentState}, clearing stale context before re-init...`);
|
|
6460
|
+
logger$1(`Client manager is in state ${currentState}, clearing stale context before re-init...`);
|
|
6367
6461
|
clientManager.send({
|
|
6368
6462
|
type: 'updateContext',
|
|
6369
6463
|
context: {
|
|
@@ -6464,14 +6558,14 @@ const clientInstance = {
|
|
|
6464
6558
|
},
|
|
6465
6559
|
setAddresses: async (addresses) => {
|
|
6466
6560
|
ensureInitialized();
|
|
6467
|
-
logger('setAddresses', addresses);
|
|
6561
|
+
logger$1('setAddresses', addresses);
|
|
6468
6562
|
clientManager.send({ type: 'setAddresses', addresses });
|
|
6469
6563
|
await waitFor(clientManager, (snapshot) => !snapshot.context.isSaving, { timeout: 10000 });
|
|
6470
6564
|
const saveError = clientManager.getSnapshot().context.saveError;
|
|
6471
6565
|
if (saveError) {
|
|
6472
6566
|
throw saveError instanceof Error ? saveError : new Error(String(saveError));
|
|
6473
6567
|
}
|
|
6474
|
-
logger('setAddresses success', addresses);
|
|
6568
|
+
logger$1('setAddresses success', addresses);
|
|
6475
6569
|
},
|
|
6476
6570
|
getAddresses: async () => {
|
|
6477
6571
|
ensureInitialized();
|
|
@@ -6585,6 +6679,1457 @@ const getClient = () => {
|
|
|
6585
6679
|
return clientInstance;
|
|
6586
6680
|
};
|
|
6587
6681
|
|
|
6682
|
+
const getMetadata = async (props) => {
|
|
6683
|
+
const appDb = BaseDb.getAppDb();
|
|
6684
|
+
let storageTransactionId;
|
|
6685
|
+
if (props) {
|
|
6686
|
+
storageTransactionId = props.storageTransactionId;
|
|
6687
|
+
}
|
|
6688
|
+
const whereClauses = [];
|
|
6689
|
+
if (storageTransactionId) {
|
|
6690
|
+
whereClauses.push(eq(metadata$1.propertyName, 'storageTransactionId'));
|
|
6691
|
+
whereClauses.push(eq(metadata$1.propertyValue, storageTransactionId));
|
|
6692
|
+
}
|
|
6693
|
+
const metadataRows = await appDb
|
|
6694
|
+
.select()
|
|
6695
|
+
.from(metadata$1)
|
|
6696
|
+
.where(and(...whereClauses));
|
|
6697
|
+
if (!metadataRows || metadataRows.length === 0) {
|
|
6698
|
+
return;
|
|
6699
|
+
}
|
|
6700
|
+
return metadataRows[0];
|
|
6701
|
+
};
|
|
6702
|
+
|
|
6703
|
+
/**
|
|
6704
|
+
* The local seed row a relation/list/image ref points at, by local id or uid. A uid resolves as
|
|
6705
|
+
* {@link findSeedRow} resolves it (the order EAS sync matches seeds in): the old uid of a seed
|
|
6706
|
+
* that was unpublished and published again resolves to that seed, through its old version rows.
|
|
6707
|
+
* Null when nothing local matches (e.g. a published uid with no local copy).
|
|
6708
|
+
*/
|
|
6709
|
+
const findRelatedSeedRow = async ({ seedLocalId, seedUid, }) => {
|
|
6710
|
+
const row = await findSeedRow({ localId: seedLocalId, uid: seedUid });
|
|
6711
|
+
return row ? { seedLocalId: row.localId, seedUid: row.uid, revokedAt: row.revokedAt, type: row.type } : null;
|
|
6712
|
+
};
|
|
6713
|
+
/** The seed is attested and its attestation is live. */
|
|
6714
|
+
const isLivePublishedSeed = (row) => !!row?.seedUid && row.seedUid !== ZERO_BYTES32 && !isVersionRevoked(row.revokedAt);
|
|
6715
|
+
/** The seed was attested and that attestation was revoked (unpublished). */
|
|
6716
|
+
const isUnpublishedSeed = (row) => !!row?.seedUid && row.seedUid !== ZERO_BYTES32 && isVersionRevoked(row.revokedAt);
|
|
6717
|
+
/** Model a relation/list/image property points at, from its definition. */
|
|
6718
|
+
function relatedModelNameFromDef(propertyDef) {
|
|
6719
|
+
const def = propertyDef;
|
|
6720
|
+
if (!def)
|
|
6721
|
+
return undefined;
|
|
6722
|
+
const dataType = normalizeDataType(def.dataType);
|
|
6723
|
+
if (dataType === ModelPropertyDataTypes.Image)
|
|
6724
|
+
return 'Image';
|
|
6725
|
+
if (dataType === ModelPropertyDataTypes.File)
|
|
6726
|
+
return 'File';
|
|
6727
|
+
if (dataType === ModelPropertyDataTypes.Html)
|
|
6728
|
+
return 'Html';
|
|
6729
|
+
return def.ref ?? def.refModelName;
|
|
6730
|
+
}
|
|
6731
|
+
|
|
6732
|
+
const STORAGE_SEED_TYPES = new Set([
|
|
6733
|
+
ModelPropertyDataTypes.Image,
|
|
6734
|
+
ModelPropertyDataTypes.File,
|
|
6735
|
+
ModelPropertyDataTypes.Html,
|
|
6736
|
+
ModelPropertyDataTypes.Json,
|
|
6737
|
+
]);
|
|
6738
|
+
const isStorageTransactionPropertyName = (name) => name === 'storageTransactionId' || name === 'storage_transaction_id';
|
|
6739
|
+
function publishPropertyKind(key, def) {
|
|
6740
|
+
const dataType = normalizeDataType(def.dataType);
|
|
6741
|
+
const refValueType = normalizeDataType(def.refValueType);
|
|
6742
|
+
if (STORAGE_SEED_TYPES.has(dataType))
|
|
6743
|
+
return 'storageSeed';
|
|
6744
|
+
if (dataType === ModelPropertyDataTypes.Relation) {
|
|
6745
|
+
return STORAGE_SEED_TYPES.has(refValueType) ? 'storageSeed' : 'relation';
|
|
6746
|
+
}
|
|
6747
|
+
if (dataType === ModelPropertyDataTypes.List)
|
|
6748
|
+
return def.ref || def.refModelName ? 'list' : 'basic';
|
|
6749
|
+
if (def.storageType === 'ItemStorage')
|
|
6750
|
+
return 'itemStorage';
|
|
6751
|
+
if (isStorageTransactionPropertyName(key))
|
|
6752
|
+
return 'storageTransaction';
|
|
6753
|
+
return 'basic';
|
|
6754
|
+
}
|
|
6755
|
+
const isEmptyValue = (value) => value == null || value.trim() === '';
|
|
6756
|
+
/** The seed refs a property's stored value names: one (Relation, storage seed) or each list member. */
|
|
6757
|
+
function refsOf(value, kind) {
|
|
6758
|
+
if (isEmptyValue(value))
|
|
6759
|
+
return [];
|
|
6760
|
+
if (kind === 'list') {
|
|
6761
|
+
return parseListPropertyValueFromStorage(value)
|
|
6762
|
+
.map((member) => normalizeRelationPropertyValue(member))
|
|
6763
|
+
.filter((ref) => !!ref);
|
|
6764
|
+
}
|
|
6765
|
+
const ref = normalizeRelationPropertyValue(value);
|
|
6766
|
+
return ref ? [ref] : [];
|
|
6767
|
+
}
|
|
6768
|
+
async function modelOfSeed(seed, modelNameHint) {
|
|
6769
|
+
if (seed.modelFileId) {
|
|
6770
|
+
const model = await resolveModelRecord(undefined, { modelFileId: seed.modelFileId });
|
|
6771
|
+
if (model)
|
|
6772
|
+
return model;
|
|
6773
|
+
}
|
|
6774
|
+
return (await resolveModelRecordByNameOrType(seed.type ?? modelNameHint, {})) ?? undefined;
|
|
6775
|
+
}
|
|
6776
|
+
/** One seed with its model's properties and their current rows. Undefined when the seed or its model isn't local. */
|
|
6777
|
+
async function readNode(seedLocalId, kind, modelNameHint) {
|
|
6778
|
+
const db = BaseDb.getAppDb();
|
|
6779
|
+
if (!db)
|
|
6780
|
+
return undefined;
|
|
6781
|
+
const seed = await findSeedRow({ localId: seedLocalId });
|
|
6782
|
+
if (!seed)
|
|
6783
|
+
return undefined;
|
|
6784
|
+
const model = await modelOfSeed(seed, modelNameHint);
|
|
6785
|
+
if (!model)
|
|
6786
|
+
return undefined;
|
|
6787
|
+
const definitions = await getPropertyDefinitions(model.id);
|
|
6788
|
+
const metadataProperties = await readModelMetadataProperties(db, model.id);
|
|
6789
|
+
const seedUid = isValidEasAttestationUid(seed.uid) ? seed.uid : undefined;
|
|
6790
|
+
const seedRef = { seedLocalId: seed.localId, seedUid };
|
|
6791
|
+
const properties = [];
|
|
6792
|
+
for (const [key, definition] of definitions) {
|
|
6793
|
+
const property = metadataProperties.find((p) => p.id === definition._dbId);
|
|
6794
|
+
const match = property
|
|
6795
|
+
? { property, propertyId: property.id, names: metadataPropertyNameForms(property) }
|
|
6796
|
+
: { property: undefined, propertyId: null, names: [key] };
|
|
6797
|
+
const row = await currentPropertyRow(seedRef, { match, propertyName: key }, { overlay: false });
|
|
6798
|
+
const propertyKind = publishPropertyKind(key, definition);
|
|
6799
|
+
properties.push({
|
|
6800
|
+
key,
|
|
6801
|
+
definition,
|
|
6802
|
+
kind: propertyKind,
|
|
6803
|
+
row,
|
|
6804
|
+
uid: isValidEasAttestationUid(row?.uid) ? row.uid : undefined,
|
|
6805
|
+
walked: false,
|
|
6806
|
+
deferred: false,
|
|
6807
|
+
refs: [],
|
|
6808
|
+
});
|
|
6809
|
+
}
|
|
6810
|
+
const head = await getLatestVersionRow(seed.localId);
|
|
6811
|
+
const published = seedUid ? await getLatestPublishedVersionRow(seed.localId) : null;
|
|
6812
|
+
const isFullNode = kind === 'root' || kind === 'draft';
|
|
6813
|
+
const embeddedImages = isFullNode
|
|
6814
|
+
? await db.select().from(htmlEmbeddedImageCoPublish).where(eq(htmlEmbeddedImageCoPublish.parentSeedLocalId, seed.localId))
|
|
6815
|
+
: [];
|
|
6816
|
+
return {
|
|
6817
|
+
seedLocalId: seed.localId,
|
|
6818
|
+
kind,
|
|
6819
|
+
seedUid,
|
|
6820
|
+
newSeed: !seedUid || (kind === 'root' && isVersionRevoked(seed.revokedAt)),
|
|
6821
|
+
modelName: model.name,
|
|
6822
|
+
seedSchemaUid: isValidEasAttestationUid(seed.schemaUid) ? seed.schemaUid : undefined,
|
|
6823
|
+
version: {
|
|
6824
|
+
localId: head?.localId ?? undefined,
|
|
6825
|
+
uid: isValidEasAttestationUid(head?.uid) ? head.uid : undefined,
|
|
6826
|
+
publishedUid: published?.uid,
|
|
6827
|
+
},
|
|
6828
|
+
properties,
|
|
6829
|
+
links: [],
|
|
6830
|
+
embeddedImages,
|
|
6831
|
+
};
|
|
6832
|
+
}
|
|
6833
|
+
/** The Html property of `node` whose value is the Html storage seed `htmlSeedLocalId`. */
|
|
6834
|
+
function htmlPropertyForHtmlSeed(node, htmlSeedLocalId) {
|
|
6835
|
+
const want = htmlSeedLocalId.trim();
|
|
6836
|
+
return node.properties.find((p) => normalizeDataType(p.definition.dataType) === ModelPropertyDataTypes.Html && p.row?.propertyValue?.trim() === want);
|
|
6837
|
+
}
|
|
6838
|
+
/**
|
|
6839
|
+
* The publish graph of the seed `root` (an Item, or any `{ seedLocalId }`). Waits for pending
|
|
6840
|
+
* outbox ops first, so every edit made before publishing is in its row (ADR 0019), then reads rows
|
|
6841
|
+
* only.
|
|
6842
|
+
*/
|
|
6843
|
+
async function getPublishGraph(root, options = {}) {
|
|
6844
|
+
const { waitForPendingOutboxOps } = await import('./index-BXTyvnKb.js');
|
|
6845
|
+
await waitForPendingOutboxOps();
|
|
6846
|
+
const rootNode = await readNode(root.seedLocalId, 'root', root.modelName);
|
|
6847
|
+
if (!rootNode)
|
|
6848
|
+
throw new Error(`Cannot publish ${root.seedLocalId}: no local seed with a known model`);
|
|
6849
|
+
const republishRevokedSeed = !!rootNode.seedUid && rootNode.newSeed;
|
|
6850
|
+
const forceFullSnapshot = options.publishMode === 'new_version' || republishRevokedSeed;
|
|
6851
|
+
const graph = {
|
|
6852
|
+
root: rootNode,
|
|
6853
|
+
nodes: [],
|
|
6854
|
+
forceFullSnapshot,
|
|
6855
|
+
republishRevokedSeed,
|
|
6856
|
+
unpublishedRelatedItems: [],
|
|
6857
|
+
relatedRefUids: new Map(),
|
|
6858
|
+
brokenRefs: [],
|
|
6859
|
+
missingEmbeddedImages: [],
|
|
6860
|
+
};
|
|
6861
|
+
const nodes = new Map([[rootNode.seedLocalId, rootNode]]);
|
|
6862
|
+
// Items getting a new seed in this publish whose walk hasn't finished: a ref to one is a back edge
|
|
6863
|
+
const inProgress = new Set();
|
|
6864
|
+
const noteUnpublished = (propertyKey, modelName, seed) => {
|
|
6865
|
+
if (graph.unpublishedRelatedItems.some((r) => r.propertyName === propertyKey && r.seedLocalId === seed.seedLocalId))
|
|
6866
|
+
return;
|
|
6867
|
+
graph.unpublishedRelatedItems.push({
|
|
6868
|
+
propertyName: propertyKey,
|
|
6869
|
+
modelName: modelName ?? seed.type ?? 'unknown',
|
|
6870
|
+
seedLocalId: seed.seedLocalId,
|
|
6871
|
+
seedUid: seed.seedUid,
|
|
6872
|
+
});
|
|
6873
|
+
};
|
|
6874
|
+
const link = (target, owner, propertyKey) => {
|
|
6875
|
+
if (!target.links.some((l) => l.seedLocalId === owner.seedLocalId && l.propertyKey === propertyKey)) {
|
|
6876
|
+
target.links.push({ seedLocalId: owner.seedLocalId, propertyKey });
|
|
6877
|
+
}
|
|
6878
|
+
};
|
|
6879
|
+
/**
|
|
6880
|
+
* Follows one ref of `property`: a draft target becomes a node (walked now, or already in the
|
|
6881
|
+
* graph). Returns the ref with its node, or undefined for a back edge.
|
|
6882
|
+
*/
|
|
6883
|
+
const follow = async (owner, property, ref) => {
|
|
6884
|
+
const list = property.kind === 'list';
|
|
6885
|
+
const { seedLocalId, seedUid } = resolveSeedIdsFromRefString(ref);
|
|
6886
|
+
if (!seedLocalId && !seedUid) {
|
|
6887
|
+
graph.brokenRefs.push({ seedLocalId: owner.seedLocalId, propertyKey: property.key, ref, reason: 'invalid', list });
|
|
6888
|
+
return { ref };
|
|
6889
|
+
}
|
|
6890
|
+
const row = await findRelatedSeedRow({ seedLocalId, seedUid });
|
|
6891
|
+
const targetLocalId = row?.seedLocalId ?? seedLocalId;
|
|
6892
|
+
if (property.kind !== 'storageSeed' && targetLocalId && inProgress.has(targetLocalId))
|
|
6893
|
+
return 'backEdge';
|
|
6894
|
+
if (isUnpublishedSeed(row)) {
|
|
6895
|
+
const targetNode = nodes.get(row.seedLocalId);
|
|
6896
|
+
noteUnpublished(property.key, targetNode?.modelName ?? relatedModelNameFromDef(property.definition), row);
|
|
6897
|
+
return { ref, seedLocalId: row.seedLocalId };
|
|
6898
|
+
}
|
|
6899
|
+
if (isLivePublishedSeed(row)) {
|
|
6900
|
+
if (ref !== row.seedUid)
|
|
6901
|
+
graph.relatedRefUids.set(ref, row.seedUid);
|
|
6902
|
+
return { ref, seedLocalId: row.seedLocalId };
|
|
6903
|
+
}
|
|
6904
|
+
if (!row) {
|
|
6905
|
+
// An attested uid with no local copy is already published: attested as-is
|
|
6906
|
+
if (!isPublishedSeedRef(ref)) {
|
|
6907
|
+
graph.brokenRefs.push({ seedLocalId: owner.seedLocalId, propertyKey: property.key, ref, reason: 'missing', list });
|
|
6908
|
+
}
|
|
6909
|
+
return { ref, seedLocalId };
|
|
6910
|
+
}
|
|
6911
|
+
let node = nodes.get(row.seedLocalId);
|
|
6912
|
+
if (!node) {
|
|
6913
|
+
const kind = property.kind === 'storageSeed' ? 'storageSeed' : 'draft';
|
|
6914
|
+
node = await readNode(row.seedLocalId, kind, relatedModelNameFromDef(property.definition));
|
|
6915
|
+
if (!node) {
|
|
6916
|
+
graph.brokenRefs.push({ seedLocalId: owner.seedLocalId, propertyKey: property.key, ref, reason: 'missing', list });
|
|
6917
|
+
return { ref, seedLocalId: row.seedLocalId };
|
|
6918
|
+
}
|
|
6919
|
+
nodes.set(node.seedLocalId, node);
|
|
6920
|
+
await visit(node);
|
|
6921
|
+
}
|
|
6922
|
+
return { ref, seedLocalId: node.seedLocalId, node };
|
|
6923
|
+
};
|
|
6924
|
+
const walkProperty = async (owner, property) => {
|
|
6925
|
+
const refs = refsOf(property.row?.propertyValue, property.kind);
|
|
6926
|
+
property.walked = refs.length > 0 && (!property.uid || forceFullSnapshot);
|
|
6927
|
+
if (!property.walked)
|
|
6928
|
+
return;
|
|
6929
|
+
// A list member that is a back edge defers the whole list; its other drafts are still published
|
|
6930
|
+
const followed = [];
|
|
6931
|
+
for (const ref of refs) {
|
|
6932
|
+
const result = await follow(owner, property, ref);
|
|
6933
|
+
if (result === 'backEdge') {
|
|
6934
|
+
property.deferred = true;
|
|
6935
|
+
if (property.kind !== 'list')
|
|
6936
|
+
return;
|
|
6937
|
+
continue;
|
|
6938
|
+
}
|
|
6939
|
+
followed.push(result);
|
|
6940
|
+
}
|
|
6941
|
+
property.refs = followed;
|
|
6942
|
+
if (property.deferred)
|
|
6943
|
+
return;
|
|
6944
|
+
for (const { node } of followed)
|
|
6945
|
+
if (node)
|
|
6946
|
+
link(node, owner, property.key);
|
|
6947
|
+
};
|
|
6948
|
+
const walkEmbeddedImages = async (owner) => {
|
|
6949
|
+
const done = new Set();
|
|
6950
|
+
for (const row of owner.embeddedImages) {
|
|
6951
|
+
if (done.has(row.imageSeedLocalId))
|
|
6952
|
+
continue;
|
|
6953
|
+
done.add(row.imageSeedLocalId);
|
|
6954
|
+
const seed = await findRelatedSeedRow({ seedLocalId: row.imageSeedLocalId });
|
|
6955
|
+
if (isUnpublishedSeed(seed)) {
|
|
6956
|
+
noteUnpublished(htmlPropertyForHtmlSeed(owner, row.htmlSeedLocalId)?.key ?? 'html', 'Image', seed);
|
|
6957
|
+
continue;
|
|
6958
|
+
}
|
|
6959
|
+
if (isLivePublishedSeed(seed))
|
|
6960
|
+
continue;
|
|
6961
|
+
if (!seed) {
|
|
6962
|
+
graph.missingEmbeddedImages.push({ seedLocalId: owner.seedLocalId, imageSeedLocalId: row.imageSeedLocalId });
|
|
6963
|
+
continue;
|
|
6964
|
+
}
|
|
6965
|
+
if (nodes.has(seed.seedLocalId))
|
|
6966
|
+
continue;
|
|
6967
|
+
const node = await readNode(seed.seedLocalId, 'embeddedImage', 'Image');
|
|
6968
|
+
if (!node) {
|
|
6969
|
+
graph.missingEmbeddedImages.push({ seedLocalId: owner.seedLocalId, imageSeedLocalId: row.imageSeedLocalId });
|
|
6970
|
+
continue;
|
|
6971
|
+
}
|
|
6972
|
+
nodes.set(node.seedLocalId, node);
|
|
6973
|
+
graph.nodes.push(node);
|
|
6974
|
+
}
|
|
6975
|
+
};
|
|
6976
|
+
async function visit(node) {
|
|
6977
|
+
const full = node.kind === 'root' || node.kind === 'draft';
|
|
6978
|
+
if (node.newSeed)
|
|
6979
|
+
inProgress.add(node.seedLocalId);
|
|
6980
|
+
try {
|
|
6981
|
+
if (full) {
|
|
6982
|
+
for (const property of node.properties) {
|
|
6983
|
+
if (property.kind === 'relation' || property.kind === 'storageSeed')
|
|
6984
|
+
await walkProperty(node, property);
|
|
6985
|
+
}
|
|
6986
|
+
await walkEmbeddedImages(node);
|
|
6987
|
+
for (const property of node.properties) {
|
|
6988
|
+
if (property.kind === 'list')
|
|
6989
|
+
await walkProperty(node, property);
|
|
6990
|
+
}
|
|
6991
|
+
}
|
|
6992
|
+
}
|
|
6993
|
+
finally {
|
|
6994
|
+
inProgress.delete(node.seedLocalId);
|
|
6995
|
+
}
|
|
6996
|
+
graph.nodes.push(node);
|
|
6997
|
+
}
|
|
6998
|
+
await visit(rootNode);
|
|
6999
|
+
return graph;
|
|
7000
|
+
}
|
|
7001
|
+
/** Nodes of `graph` that are full items (the root and model drafts): the owners of Html properties and embedded images. */
|
|
7002
|
+
const publishGraphItemNodes = (graph) => graph.nodes.filter((n) => n.kind === 'root' || n.kind === 'draft');
|
|
7003
|
+
|
|
7004
|
+
const HTML_EMBEDDED_MAX_IMAGES_PER_DOC = 50;
|
|
7005
|
+
const HTML_EMBEDDED_MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
7006
|
+
const HTML_EMBEDDED_MAX_TOTAL_BYTES = 25 * 1024 * 1024;
|
|
7007
|
+
const DATA_URI_IMAGE = /^data:(image\/[a-zA-Z0-9.+-]+);base64,([\s\S]*)$/i;
|
|
7008
|
+
const MIME_NORMALIZE = {
|
|
7009
|
+
'image/jpg': 'image/jpeg',
|
|
7010
|
+
};
|
|
7011
|
+
function normalizeMime(m) {
|
|
7012
|
+
const lower = m.toLowerCase().trim();
|
|
7013
|
+
return MIME_NORMALIZE[lower] ?? lower;
|
|
7014
|
+
}
|
|
7015
|
+
const ALLOWED_MIMES = new Set([
|
|
7016
|
+
'image/png',
|
|
7017
|
+
'image/jpeg',
|
|
7018
|
+
'image/webp',
|
|
7019
|
+
'image/gif',
|
|
7020
|
+
'image/svg+xml',
|
|
7021
|
+
]);
|
|
7022
|
+
class HtmlEmbeddedDataUriLimitError extends Error {
|
|
7023
|
+
constructor(message) {
|
|
7024
|
+
super(message);
|
|
7025
|
+
this.name = 'HtmlEmbeddedDataUriLimitError';
|
|
7026
|
+
}
|
|
7027
|
+
}
|
|
7028
|
+
function resolveEffectiveHtmlEmbeddedDataUriPolicy(propertyDef, publishLevel) {
|
|
7029
|
+
const fromProp = propertyDef?.htmlEmbeddedDataUriPolicy;
|
|
7030
|
+
if (fromProp === 'materialize' || fromProp === 'preserve')
|
|
7031
|
+
return fromProp;
|
|
7032
|
+
if (publishLevel === 'materialize' || publishLevel === 'preserve')
|
|
7033
|
+
return publishLevel;
|
|
7034
|
+
return 'materialize';
|
|
7035
|
+
}
|
|
7036
|
+
function getAttr(node, name) {
|
|
7037
|
+
const a = node.attrs?.find((x) => x.name === name);
|
|
7038
|
+
return a?.value;
|
|
7039
|
+
}
|
|
7040
|
+
/**
|
|
7041
|
+
* Walk fragment tree; collect unique data:image base64 URIs with limits.
|
|
7042
|
+
*/
|
|
7043
|
+
async function extractDataUriImagesFromHtml(html) {
|
|
7044
|
+
const fragment = parseFragment(html);
|
|
7045
|
+
const found = [];
|
|
7046
|
+
const seenUris = new Set();
|
|
7047
|
+
let totalBytes = 0;
|
|
7048
|
+
const walk = async (node) => {
|
|
7049
|
+
if (node.nodeName === '#text' || node.nodeName === '#comment')
|
|
7050
|
+
return;
|
|
7051
|
+
const el = node;
|
|
7052
|
+
if (el.tagName === 'img') {
|
|
7053
|
+
const src = getAttr(el, 'src')?.trim();
|
|
7054
|
+
if (src && src.startsWith('data:image/')) {
|
|
7055
|
+
const m = DATA_URI_IMAGE.exec(src);
|
|
7056
|
+
if (m) {
|
|
7057
|
+
const mimeRaw = normalizeMime(m[1]);
|
|
7058
|
+
if (!ALLOWED_MIMES.has(mimeRaw)) {
|
|
7059
|
+
throw new HtmlEmbeddedDataUriLimitError(`Unsupported embedded image type: ${mimeRaw}. Allowed: ${[...ALLOWED_MIMES].join(', ')}`);
|
|
7060
|
+
}
|
|
7061
|
+
let binary;
|
|
7062
|
+
try {
|
|
7063
|
+
const b64 = m[2].replace(/\s/g, '');
|
|
7064
|
+
binary = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
7065
|
+
}
|
|
7066
|
+
catch {
|
|
7067
|
+
throw new HtmlEmbeddedDataUriLimitError('Invalid base64 in embedded image data URI');
|
|
7068
|
+
}
|
|
7069
|
+
const byteLength = binary.byteLength;
|
|
7070
|
+
if (byteLength > HTML_EMBEDDED_MAX_IMAGE_BYTES) {
|
|
7071
|
+
throw new HtmlEmbeddedDataUriLimitError(`Embedded image exceeds max size (${HTML_EMBEDDED_MAX_IMAGE_BYTES} bytes)`);
|
|
7072
|
+
}
|
|
7073
|
+
totalBytes += byteLength;
|
|
7074
|
+
if (totalBytes > HTML_EMBEDDED_MAX_TOTAL_BYTES) {
|
|
7075
|
+
throw new HtmlEmbeddedDataUriLimitError(`Total embedded image size exceeds max (${HTML_EMBEDDED_MAX_TOTAL_BYTES} bytes)`);
|
|
7076
|
+
}
|
|
7077
|
+
if (!seenUris.has(src)) {
|
|
7078
|
+
if (seenUris.size >= HTML_EMBEDDED_MAX_IMAGES_PER_DOC) {
|
|
7079
|
+
throw new HtmlEmbeddedDataUriLimitError(`Too many distinct embedded images (max ${HTML_EMBEDDED_MAX_IMAGES_PER_DOC})`);
|
|
7080
|
+
}
|
|
7081
|
+
seenUris.add(src);
|
|
7082
|
+
const stableKey = await getContentHash(binary);
|
|
7083
|
+
found.push({ dataUri: src, stableKey, byteLength, mimeType: mimeRaw, bytes: binary });
|
|
7084
|
+
}
|
|
7085
|
+
}
|
|
7086
|
+
}
|
|
7087
|
+
}
|
|
7088
|
+
const parent = el;
|
|
7089
|
+
if (parent.childNodes) {
|
|
7090
|
+
for (const c of parent.childNodes)
|
|
7091
|
+
await walk(c);
|
|
7092
|
+
}
|
|
7093
|
+
};
|
|
7094
|
+
for (const c of fragment.childNodes)
|
|
7095
|
+
await walk(c);
|
|
7096
|
+
return found;
|
|
7097
|
+
}
|
|
7098
|
+
function escapeAttributeValue(value) {
|
|
7099
|
+
return value.replace(/&/g, '&').replace(/"/g, '"');
|
|
7100
|
+
}
|
|
7101
|
+
/**
|
|
7102
|
+
* Rewrite attribute values of `html` by splicing each new value into the original string: every
|
|
7103
|
+
* byte other than the rewritten attributes stays as written. Re-serializing the parsed tree would
|
|
7104
|
+
* not round-trip (parse5 drops the newline the parser eats after `<pre>`/`<textarea>`/`<listing>`,
|
|
7105
|
+
* a fragment parse drops doctype/html/head/body, and quoting, case and line endings are normalized),
|
|
7106
|
+
* and sealed Html must not change with the serializer.
|
|
7107
|
+
*
|
|
7108
|
+
* `rewrite` gets each attribute's (entity-decoded) value and returns its new value, or undefined to
|
|
7109
|
+
* leave it. A rewritten attribute is written as `name="value"`.
|
|
7110
|
+
*/
|
|
7111
|
+
function rewriteHtmlAttributes(html, rewrite) {
|
|
7112
|
+
const fragment = parseFragment(html, { sourceCodeLocationInfo: true });
|
|
7113
|
+
const edits = [];
|
|
7114
|
+
const walk = (node) => {
|
|
7115
|
+
if (node.nodeName === '#text' || node.nodeName === '#comment')
|
|
7116
|
+
return;
|
|
7117
|
+
const el = node;
|
|
7118
|
+
if (el.attrs?.length) {
|
|
7119
|
+
for (const attr of el.attrs) {
|
|
7120
|
+
if (attr.prefix)
|
|
7121
|
+
continue;
|
|
7122
|
+
const next = rewrite(el, attr.name, attr.value);
|
|
7123
|
+
const location = el.sourceCodeLocation?.attrs?.[attr.name];
|
|
7124
|
+
if (next === undefined || !location)
|
|
7125
|
+
continue;
|
|
7126
|
+
edits.push({
|
|
7127
|
+
start: location.startOffset,
|
|
7128
|
+
end: location.endOffset,
|
|
7129
|
+
text: `${attr.name}="${escapeAttributeValue(next)}"`,
|
|
7130
|
+
});
|
|
7131
|
+
}
|
|
7132
|
+
}
|
|
7133
|
+
const parent = el;
|
|
7134
|
+
if (parent.childNodes) {
|
|
7135
|
+
for (const c of parent.childNodes)
|
|
7136
|
+
walk(c);
|
|
7137
|
+
}
|
|
7138
|
+
};
|
|
7139
|
+
for (const c of fragment.childNodes)
|
|
7140
|
+
walk(c);
|
|
7141
|
+
let out = html;
|
|
7142
|
+
for (const edit of edits.sort((a, b) => b.start - a.start)) {
|
|
7143
|
+
out = out.slice(0, edit.start) + edit.text + out.slice(edit.end);
|
|
7144
|
+
}
|
|
7145
|
+
return out;
|
|
7146
|
+
}
|
|
7147
|
+
/**
|
|
7148
|
+
* Replace `data:image/...` `<img src>` values with Arweave URLs (keyed by data URI). Only those
|
|
7149
|
+
* `src` attributes change; the rest of the Html is kept byte for byte.
|
|
7150
|
+
*/
|
|
7151
|
+
function replaceDataUrisInParsedHtml(html, replacements) {
|
|
7152
|
+
if (replacements.size === 0)
|
|
7153
|
+
return html;
|
|
7154
|
+
return rewriteHtmlAttributes(html, (el, attrName, value) => {
|
|
7155
|
+
if (el.tagName !== 'img' || attrName !== 'src')
|
|
7156
|
+
return undefined;
|
|
7157
|
+
return replacements.get(value.trim());
|
|
7158
|
+
});
|
|
7159
|
+
}
|
|
7160
|
+
/**
|
|
7161
|
+
* `seed:property/<propertyName>` in Html stands for the Arweave URL of the same item's
|
|
7162
|
+
* `<propertyName>` storage property (Image, File or Json). Publish uploads that property first,
|
|
7163
|
+
* then rewrites the placeholder to its gateway URL before the Html is uploaded, so Html can show a
|
|
7164
|
+
* file published alongside it without embedding it (e.g. `<img class="u-featured"
|
|
7165
|
+
* src="seed:property/featureImage">`).
|
|
7166
|
+
*/
|
|
7167
|
+
const SEED_PROPERTY_REF_PREFIX = 'seed:property/';
|
|
7168
|
+
const SEED_PROPERTY_REF = /^seed:property\/([A-Za-z_$][A-Za-z0-9_$]*)$/;
|
|
7169
|
+
/** Attributes whose whole (trimmed) value may be a `seed:property/` placeholder. */
|
|
7170
|
+
const SEED_PROPERTY_REF_ATTRIBUTES = new Set(['src', 'href', 'poster']);
|
|
7171
|
+
const seedPropertyRefName = (attrName, value) => SEED_PROPERTY_REF_ATTRIBUTES.has(attrName) ? SEED_PROPERTY_REF.exec(value.trim())?.[1] : undefined;
|
|
7172
|
+
/** A `seed:property/` placeholder that names no usable property, or whose upload is unknown. */
|
|
7173
|
+
class HtmlSeedPropertyRefError extends Error {
|
|
7174
|
+
constructor(message) {
|
|
7175
|
+
super(message);
|
|
7176
|
+
this.name = 'HtmlSeedPropertyRefError';
|
|
7177
|
+
}
|
|
7178
|
+
}
|
|
7179
|
+
/** Property names referenced by `seed:property/<name>` placeholders in `html`, each once. */
|
|
7180
|
+
function findSeedPropertyRefsInHtml(html) {
|
|
7181
|
+
if (!html.includes(SEED_PROPERTY_REF_PREFIX))
|
|
7182
|
+
return [];
|
|
7183
|
+
const names = new Set();
|
|
7184
|
+
rewriteHtmlAttributes(html, (_el, attrName, value) => {
|
|
7185
|
+
const name = seedPropertyRefName(attrName, value);
|
|
7186
|
+
if (name)
|
|
7187
|
+
names.add(name);
|
|
7188
|
+
return undefined;
|
|
7189
|
+
});
|
|
7190
|
+
return [...names];
|
|
7191
|
+
}
|
|
7192
|
+
/** Replace `seed:property/<name>` placeholders with `urlByPropertyName.get(name)`; other bytes are kept. */
|
|
7193
|
+
function replaceSeedPropertyRefsInHtml(html, urlByPropertyName) {
|
|
7194
|
+
if (urlByPropertyName.size === 0)
|
|
7195
|
+
return html;
|
|
7196
|
+
return rewriteHtmlAttributes(html, (_el, attrName, value) => {
|
|
7197
|
+
const name = seedPropertyRefName(attrName, value);
|
|
7198
|
+
return name ? urlByPropertyName.get(name) : undefined;
|
|
7199
|
+
});
|
|
7200
|
+
}
|
|
7201
|
+
const SEED_PROPERTY_REF_DATA_TYPES = [
|
|
7202
|
+
ModelPropertyDataTypes.Image,
|
|
7203
|
+
ModelPropertyDataTypes.File,
|
|
7204
|
+
ModelPropertyDataTypes.Json,
|
|
7205
|
+
];
|
|
7206
|
+
/**
|
|
7207
|
+
* The storage property of `node` a `seed:property/<name>` placeholder names, and the seed it holds.
|
|
7208
|
+
* Html properties are refused: an Html seed may itself be deferred to after the rewrite.
|
|
7209
|
+
*/
|
|
7210
|
+
function resolveSeedPropertyRefTarget(node, propertyName) {
|
|
7211
|
+
const property = node.properties.find((p) => p.key === propertyName);
|
|
7212
|
+
if (!property) {
|
|
7213
|
+
throw new HtmlSeedPropertyRefError(`Html references ${SEED_PROPERTY_REF_PREFIX}${propertyName}, but ${node.modelName} has no property ${propertyName}`);
|
|
7214
|
+
}
|
|
7215
|
+
const def = property.definition;
|
|
7216
|
+
const dataType = normalizeDataType(def.dataType);
|
|
7217
|
+
const storageType = dataType === ModelPropertyDataTypes.Relation ? normalizeDataType(def.refValueType) : dataType;
|
|
7218
|
+
if (!SEED_PROPERTY_REF_DATA_TYPES.includes(storageType)) {
|
|
7219
|
+
throw new HtmlSeedPropertyRefError(`Html references ${SEED_PROPERTY_REF_PREFIX}${propertyName}, but ${propertyName} is ${def.dataType}; ` +
|
|
7220
|
+
`only Image, File and Json properties can be referenced`);
|
|
7221
|
+
}
|
|
7222
|
+
const ids = resolveSeedIdsFromRefString(normalizeRelationPropertyValue(property.row?.propertyValue) ?? '');
|
|
7223
|
+
if (!ids.seedLocalId && !ids.seedUid) {
|
|
7224
|
+
throw new HtmlSeedPropertyRefError(`Html references ${SEED_PROPERTY_REF_PREFIX}${propertyName}, but ${propertyName} has no value`);
|
|
7225
|
+
}
|
|
7226
|
+
return ids;
|
|
7227
|
+
}
|
|
7228
|
+
const ARWEAVE_TX_ID$1 = /^[a-z0-9_-]{43}$/i;
|
|
7229
|
+
/**
|
|
7230
|
+
* The Arweave tx of the storage seed a placeholder names: uploaded in this publish's phase 1, or,
|
|
7231
|
+
* when not uploaded now, the transaction it was published with (its storageTransactionId row).
|
|
7232
|
+
*/
|
|
7233
|
+
async function resolveSeedPropertyRefTxId(node, propertyName, txBySeedLocalId) {
|
|
7234
|
+
const target = resolveSeedPropertyRefTarget(node, propertyName);
|
|
7235
|
+
const uploaded = target.seedLocalId ? txBySeedLocalId.get(target.seedLocalId) : undefined;
|
|
7236
|
+
if (uploaded)
|
|
7237
|
+
return uploaded;
|
|
7238
|
+
const recorded = await storageSeedTransactionId(target);
|
|
7239
|
+
if (recorded && ARWEAVE_TX_ID$1.test(recorded))
|
|
7240
|
+
return recorded;
|
|
7241
|
+
throw new HtmlSeedPropertyRefError(`Html references ${SEED_PROPERTY_REF_PREFIX}${propertyName}, but ${propertyName} was not uploaded ` +
|
|
7242
|
+
`in this publish and has no published transaction`);
|
|
7243
|
+
}
|
|
7244
|
+
/**
|
|
7245
|
+
* Name of the file holding an embedded image's bytes in the images folder: `<imageSeedLocalId>.<ext>`,
|
|
7246
|
+
* the extension following the image's MIME type (one of {@link ALLOWED_MIMES}).
|
|
7247
|
+
*/
|
|
7248
|
+
function embeddedImageFileName(imageSeedLocalId, mimeType) {
|
|
7249
|
+
return `${imageSeedLocalId}.${getExtensionForMimeType(mimeType) ?? 'bin'}`;
|
|
7250
|
+
}
|
|
7251
|
+
/** Path of the file {@link createImageItemFromBytes} wrote for an Image seed, or undefined if there is none. */
|
|
7252
|
+
async function findEmbeddedImageFilePath(imageSeedLocalId) {
|
|
7253
|
+
for (const mimeType of ALLOWED_MIMES) {
|
|
7254
|
+
const filePath = BaseFileManager.getFilesPath('images', embeddedImageFileName(imageSeedLocalId, mimeType));
|
|
7255
|
+
if (await BaseFileManager.pathExists(filePath))
|
|
7256
|
+
return filePath;
|
|
7257
|
+
}
|
|
7258
|
+
return undefined;
|
|
7259
|
+
}
|
|
7260
|
+
/**
|
|
7261
|
+
* Create an Image item for an embedded image and write its bytes to the images folder, where publish
|
|
7262
|
+
* reads them for upload ({@link findEmbeddedImageFilePath}). Its storageTransactionId stays empty
|
|
7263
|
+
* until the upload writes the transaction id, as for any other storage seed.
|
|
7264
|
+
*/
|
|
7265
|
+
async function createImageItemFromBytes(bytes, mimeType) {
|
|
7266
|
+
const imageItem = await Item.create({
|
|
7267
|
+
modelName: 'Image',
|
|
7268
|
+
modelFileId: INTERNAL_STORAGE_MODEL_FILE_IDS.image,
|
|
7269
|
+
});
|
|
7270
|
+
await waitForEntityReady(imageItem, { timeout: 60000 });
|
|
7271
|
+
const filePath = BaseFileManager.getFilesPath('images', embeddedImageFileName(imageItem.seedLocalId, mimeType));
|
|
7272
|
+
await BaseFileManager.createDirIfNotExists(BaseFileManager.getFilesPath('images'));
|
|
7273
|
+
const data = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
7274
|
+
try {
|
|
7275
|
+
await BaseFileManager.saveFile(filePath, data);
|
|
7276
|
+
}
|
|
7277
|
+
catch {
|
|
7278
|
+
const fs = await BaseFileManager.getFs();
|
|
7279
|
+
fs.writeFileSync(filePath, bytes);
|
|
7280
|
+
}
|
|
7281
|
+
const { seedLocalId } = imageItem;
|
|
7282
|
+
// Publish reads the image from its rows when it uploads and attests it
|
|
7283
|
+
imageItem.unload();
|
|
7284
|
+
return { seedLocalId };
|
|
7285
|
+
}
|
|
7286
|
+
/** The Html properties of a node with a stored Html seed and file name. */
|
|
7287
|
+
const htmlValuesOf = (node) => node.properties.flatMap((property) => {
|
|
7288
|
+
if (normalizeDataType(property.definition.dataType) !== ModelPropertyDataTypes.Html)
|
|
7289
|
+
return [];
|
|
7290
|
+
const htmlSeedLocalId = property.row?.propertyValue?.trim();
|
|
7291
|
+
const fileName = property.row?.refResolvedValue;
|
|
7292
|
+
return htmlSeedLocalId && fileName ? [{ property, htmlSeedLocalId, filePath: `${BaseFileManager.getFilesPath('html')}/${fileName}` }] : [];
|
|
7293
|
+
});
|
|
7294
|
+
/**
|
|
7295
|
+
* Scan Html storage properties, materialize embedded images as Image items, insert co-publish rows.
|
|
7296
|
+
* Call before phase-1 Arweave tx build. Does not rewrite Html files yet.
|
|
7297
|
+
*/
|
|
7298
|
+
async function prepareHtmlEmbeddedImagesForPublish(item, publishPolicy) {
|
|
7299
|
+
const appDb = BaseDb.getAppDb();
|
|
7300
|
+
if (!appDb) {
|
|
7301
|
+
return { deferredHtmlSeedLocalIds: [] };
|
|
7302
|
+
}
|
|
7303
|
+
const deferred = new Set();
|
|
7304
|
+
// The item's own Html, and the Html of the draft items published with it: each gets its embedded
|
|
7305
|
+
// images materialized and co-published (rows keyed by the item owning the Html property).
|
|
7306
|
+
const graph = await getPublishGraph(item);
|
|
7307
|
+
for (const node of publishGraphItemNodes(graph)) {
|
|
7308
|
+
await prepareHtmlEmbeddedImagesOfNode(appDb, node, publishPolicy, deferred);
|
|
7309
|
+
}
|
|
7310
|
+
return { deferredHtmlSeedLocalIds: [...deferred] };
|
|
7311
|
+
}
|
|
7312
|
+
/**
|
|
7313
|
+
* An embedded image an earlier attempt materialized for this data URI that can be used again: its
|
|
7314
|
+
* Image seed is still a local draft and its file is there.
|
|
7315
|
+
*/
|
|
7316
|
+
async function reusableImage(row) {
|
|
7317
|
+
if (!row)
|
|
7318
|
+
return undefined;
|
|
7319
|
+
const seed = await findSeedRow({ localId: row.imageSeedLocalId });
|
|
7320
|
+
if (!seed || isValidEasAttestationUid(seed.uid))
|
|
7321
|
+
return undefined;
|
|
7322
|
+
return (await findEmbeddedImageFilePath(row.imageSeedLocalId)) ? row.imageSeedLocalId : undefined;
|
|
7323
|
+
}
|
|
7324
|
+
async function prepareHtmlEmbeddedImagesOfNode(appDb, node, publishPolicy, deferred) {
|
|
7325
|
+
// An earlier attempt's rows: their images are reused for the same Html and data URI
|
|
7326
|
+
const earlier = node.embeddedImages;
|
|
7327
|
+
const kept = [];
|
|
7328
|
+
for (const { property, htmlSeedLocalId, filePath } of htmlValuesOf(node)) {
|
|
7329
|
+
if (!(await BaseFileManager.pathExists(filePath)))
|
|
7330
|
+
continue;
|
|
7331
|
+
const html = await BaseFileManager.readFileAsString(filePath);
|
|
7332
|
+
// seed:property/ placeholders are filled in after phase 1 whatever the data URI policy. A
|
|
7333
|
+
// placeholder naming no usable property fails here, before anything is uploaded.
|
|
7334
|
+
const refs = findSeedPropertyRefsInHtml(html);
|
|
7335
|
+
for (const name of refs)
|
|
7336
|
+
resolveSeedPropertyRefTarget(node, name);
|
|
7337
|
+
if (refs.length > 0)
|
|
7338
|
+
deferred.add(htmlSeedLocalId);
|
|
7339
|
+
// The policy is the row's definition (a schema file's htmlEmbeddedDataUriPolicy or a code-defined one)
|
|
7340
|
+
const policy = resolveEffectiveHtmlEmbeddedDataUriPolicy(property.definition, publishPolicy);
|
|
7341
|
+
if (policy !== 'materialize')
|
|
7342
|
+
continue;
|
|
7343
|
+
const extracted = await extractDataUriImagesFromHtml(html);
|
|
7344
|
+
if (extracted.length === 0)
|
|
7345
|
+
continue;
|
|
7346
|
+
deferred.add(htmlSeedLocalId);
|
|
7347
|
+
for (const ent of extracted) {
|
|
7348
|
+
const previous = earlier.find((r) => r.htmlSeedLocalId.trim() === htmlSeedLocalId && r.stableKey === ent.stableKey);
|
|
7349
|
+
const reused = await reusableImage(previous);
|
|
7350
|
+
if (reused) {
|
|
7351
|
+
kept.push(previous);
|
|
7352
|
+
continue;
|
|
7353
|
+
}
|
|
7354
|
+
const { seedLocalId: imageSeedLocalId } = await createImageItemFromBytes(ent.bytes, ent.mimeType);
|
|
7355
|
+
if (previous)
|
|
7356
|
+
await appDb.delete(htmlEmbeddedImageCoPublish).where(eq(htmlEmbeddedImageCoPublish.id, previous.id));
|
|
7357
|
+
await appDb.insert(htmlEmbeddedImageCoPublish).values({
|
|
7358
|
+
parentSeedLocalId: node.seedLocalId,
|
|
7359
|
+
htmlSeedLocalId,
|
|
7360
|
+
imageSeedLocalId,
|
|
7361
|
+
stableKey: ent.stableKey,
|
|
7362
|
+
createdAt: Date.now(),
|
|
7363
|
+
}).onConflictDoNothing();
|
|
7364
|
+
}
|
|
7365
|
+
}
|
|
7366
|
+
// Rows for Html or data URIs no longer there
|
|
7367
|
+
for (const row of earlier) {
|
|
7368
|
+
if (kept.includes(row))
|
|
7369
|
+
continue;
|
|
7370
|
+
await appDb.delete(htmlEmbeddedImageCoPublish).where(eq(htmlEmbeddedImageCoPublish.id, row.id));
|
|
7371
|
+
}
|
|
7372
|
+
}
|
|
7373
|
+
/**
|
|
7374
|
+
* After phase-1 uploads, rewrite Html files on disk: data URI → Arweave gateway URL, and
|
|
7375
|
+
* `seed:property/<name>` → the gateway URL of that property's upload.
|
|
7376
|
+
*
|
|
7377
|
+
* `deferredHtmlSeedLocalIds` (from {@link prepareHtmlEmbeddedImagesForPublish}) limits the rewrite to
|
|
7378
|
+
* the Html held back from phase 1; without it, every Html of the item and its co-published drafts is
|
|
7379
|
+
* considered.
|
|
7380
|
+
*/
|
|
7381
|
+
async function rewriteHtmlEmbeddedImagesOnDisk(parentSeedLocalId, uploadedTransactions, deferredHtmlSeedLocalIds) {
|
|
7382
|
+
const appDb = BaseDb.getAppDb();
|
|
7383
|
+
if (!appDb)
|
|
7384
|
+
return;
|
|
7385
|
+
const txBySeedLocalId = new Map();
|
|
7386
|
+
for (const u of uploadedTransactions) {
|
|
7387
|
+
if (u.seedLocalId && u.txId) {
|
|
7388
|
+
txBySeedLocalId.set(u.seedLocalId.trim(), u.txId.trim());
|
|
7389
|
+
}
|
|
7390
|
+
}
|
|
7391
|
+
const deferred = deferredHtmlSeedLocalIds
|
|
7392
|
+
? new Set(deferredHtmlSeedLocalIds.map((id) => id.trim()))
|
|
7393
|
+
: undefined;
|
|
7394
|
+
// The parent's Html and that of the draft items published with it (see prepareHtmlEmbeddedImagesForPublish).
|
|
7395
|
+
const graph = await getPublishGraph({ seedLocalId: parentSeedLocalId });
|
|
7396
|
+
for (const node of publishGraphItemNodes(graph)) {
|
|
7397
|
+
await rewriteHtmlEmbeddedImagesOfNode(node, txBySeedLocalId, deferred);
|
|
7398
|
+
}
|
|
7399
|
+
}
|
|
7400
|
+
async function rewriteHtmlEmbeddedImagesOfNode(node, txBySeedLocalId, deferred) {
|
|
7401
|
+
for (const { htmlSeedLocalId, filePath } of htmlValuesOf(node)) {
|
|
7402
|
+
if (deferred && !deferred.has(htmlSeedLocalId))
|
|
7403
|
+
continue;
|
|
7404
|
+
if (!(await BaseFileManager.pathExists(filePath)))
|
|
7405
|
+
continue;
|
|
7406
|
+
const html = await BaseFileManager.readFileAsString(filePath);
|
|
7407
|
+
const replacements = new Map();
|
|
7408
|
+
const entries = await extractDataUriImagesFromHtml(html);
|
|
7409
|
+
for (const r of node.embeddedImages) {
|
|
7410
|
+
if (r.htmlSeedLocalId.trim() !== htmlSeedLocalId)
|
|
7411
|
+
continue;
|
|
7412
|
+
const txId = txBySeedLocalId.get(r.imageSeedLocalId.trim());
|
|
7413
|
+
const dataUri = entries.find((e) => e.stableKey === r.stableKey)?.dataUri;
|
|
7414
|
+
if (txId && dataUri)
|
|
7415
|
+
replacements.set(dataUri, getPublicArweaveRawUrl(txId));
|
|
7416
|
+
}
|
|
7417
|
+
const urlByPropertyName = new Map();
|
|
7418
|
+
for (const name of findSeedPropertyRefsInHtml(html)) {
|
|
7419
|
+
const txId = await resolveSeedPropertyRefTxId(node, name, txBySeedLocalId);
|
|
7420
|
+
urlByPropertyName.set(name, getPublicArweaveRawUrl(txId));
|
|
7421
|
+
}
|
|
7422
|
+
const newHtml = replaceSeedPropertyRefsInHtml(replaceDataUrisInParsedHtml(html, replacements), urlByPropertyName);
|
|
7423
|
+
if (newHtml === html)
|
|
7424
|
+
continue;
|
|
7425
|
+
try {
|
|
7426
|
+
await BaseFileManager.saveFile(filePath, newHtml);
|
|
7427
|
+
}
|
|
7428
|
+
catch {
|
|
7429
|
+
const fs = await BaseFileManager.getFs();
|
|
7430
|
+
fs.writeFileSync(filePath, newHtml);
|
|
7431
|
+
}
|
|
7432
|
+
}
|
|
7433
|
+
}
|
|
7434
|
+
/**
|
|
7435
|
+
* Remove co-publish rows after a successful publish for this parent seed.
|
|
7436
|
+
*/
|
|
7437
|
+
async function clearHtmlEmbeddedImageCoPublishRows(parentSeedLocalId) {
|
|
7438
|
+
const appDb = BaseDb.getAppDb();
|
|
7439
|
+
if (!appDb || !parentSeedLocalId)
|
|
7440
|
+
return;
|
|
7441
|
+
await appDb
|
|
7442
|
+
.delete(htmlEmbeddedImageCoPublish)
|
|
7443
|
+
.where(eq(htmlEmbeddedImageCoPublish.parentSeedLocalId, parentSeedLocalId.trim()));
|
|
7444
|
+
}
|
|
7445
|
+
|
|
7446
|
+
/**
|
|
7447
|
+
* `Promise.all(items.map(fn))` with at most `limit` calls in flight. Results keep the input order.
|
|
7448
|
+
* Rejects with the first error (by time); after that no new calls start, though calls already in
|
|
7449
|
+
* flight run to completion.
|
|
7450
|
+
*/
|
|
7451
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
7452
|
+
const results = new Array(items.length);
|
|
7453
|
+
const workerCount = Math.max(1, Math.min(Math.floor(limit) || 1, items.length));
|
|
7454
|
+
let next = 0;
|
|
7455
|
+
let failed = false;
|
|
7456
|
+
const worker = async () => {
|
|
7457
|
+
while (!failed && next < items.length) {
|
|
7458
|
+
const index = next++;
|
|
7459
|
+
try {
|
|
7460
|
+
results[index] = await fn(items[index], index);
|
|
7461
|
+
}
|
|
7462
|
+
catch (error) {
|
|
7463
|
+
failed = true;
|
|
7464
|
+
throw error;
|
|
7465
|
+
}
|
|
7466
|
+
}
|
|
7467
|
+
};
|
|
7468
|
+
await Promise.all(Array.from({ length: workerCount }, worker));
|
|
7469
|
+
return results;
|
|
7470
|
+
}
|
|
7471
|
+
|
|
7472
|
+
/**
|
|
7473
|
+
* Composite uploads: several ItemStorage files of one item in a single Arweave transaction.
|
|
7474
|
+
*
|
|
7475
|
+
* Producer: `getPublishUploads.ts` (`processUploadPropertyData`), for an item with a
|
|
7476
|
+
* `storageTransactionId` property and ItemStorage properties (e.g. a Post's `html` and `json`).
|
|
7477
|
+
* Consumer: the download worker (`browser/workers/filesDownload.ts`), which keeps its own copy of
|
|
7478
|
+
* {@link decodeCompositeUpload} because it runs from a string; a test keeps the two in step.
|
|
7479
|
+
*
|
|
7480
|
+
* Format (unchanged for text children, so composites already on Arweave still read):
|
|
7481
|
+
*
|
|
7482
|
+
* ===FILE_SEPARATOR===<name>===FILE_SEPARATOR===<utf-8 text> (repeated per child)
|
|
7483
|
+
*
|
|
7484
|
+
* A child that is not valid UTF-8 text (or whose text contains the separator) is written as
|
|
7485
|
+
* base64 under the name `<name>;base64`. Composites written before this suffix existed converted
|
|
7486
|
+
* every child to text, which corrupted binary children; those bytes cannot be recovered.
|
|
7487
|
+
*/
|
|
7488
|
+
const COMPOSITE_SEPARATOR = '===FILE_SEPARATOR===';
|
|
7489
|
+
const COMPOSITE_BASE64_SUFFIX = ';base64';
|
|
7490
|
+
const bytesToBase64 = (bytes) => {
|
|
7491
|
+
let binary = '';
|
|
7492
|
+
const chunk = 0x8000;
|
|
7493
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
7494
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
7495
|
+
}
|
|
7496
|
+
return btoa(binary);
|
|
7497
|
+
};
|
|
7498
|
+
/** The child's text when it can travel as text: valid UTF-8, no NUL bytes, no separator. */
|
|
7499
|
+
const childAsText = (bytes) => {
|
|
7500
|
+
if (bytes.includes(0))
|
|
7501
|
+
return undefined;
|
|
7502
|
+
let text;
|
|
7503
|
+
try {
|
|
7504
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
7505
|
+
}
|
|
7506
|
+
catch {
|
|
7507
|
+
return undefined;
|
|
7508
|
+
}
|
|
7509
|
+
return text.includes(COMPOSITE_SEPARATOR) ? undefined : text;
|
|
7510
|
+
};
|
|
7511
|
+
const encodeCompositeUpload = (children) => {
|
|
7512
|
+
let composite = '';
|
|
7513
|
+
for (const { name, bytes } of children) {
|
|
7514
|
+
const text = childAsText(bytes);
|
|
7515
|
+
composite +=
|
|
7516
|
+
text !== undefined
|
|
7517
|
+
? `${COMPOSITE_SEPARATOR}${name}${COMPOSITE_SEPARATOR}${text}`
|
|
7518
|
+
: `${COMPOSITE_SEPARATOR}${name}${COMPOSITE_BASE64_SUFFIX}${COMPOSITE_SEPARATOR}${bytesToBase64(bytes)}`;
|
|
7519
|
+
}
|
|
7520
|
+
return new TextEncoder().encode(composite);
|
|
7521
|
+
};
|
|
7522
|
+
new TextEncoder().encode(COMPOSITE_SEPARATOR);
|
|
7523
|
+
|
|
7524
|
+
const logger = debug('seedSdk:item:getPublishUploads');
|
|
7525
|
+
/** L1 transactions {@link getPublishUploads} creates at once (each asks the gateway for price and anchor). */
|
|
7526
|
+
const CREATE_TRANSACTION_CONCURRENCY = 4;
|
|
7527
|
+
const toUint8Array = async (data) => {
|
|
7528
|
+
if (typeof Blob !== 'undefined' && data instanceof Blob) {
|
|
7529
|
+
return new Uint8Array(await data.arrayBuffer());
|
|
7530
|
+
}
|
|
7531
|
+
return new Uint8Array(data);
|
|
7532
|
+
};
|
|
7533
|
+
/** A local file's bytes plus its MIME type: the Blob's own type, else the name's extension, else a sniff of the bytes. */
|
|
7534
|
+
const readUploadFile = async (filePath, fileName = filePath) => {
|
|
7535
|
+
const data = await BaseFileManager.readFileAsBuffer(filePath);
|
|
7536
|
+
const bytes = await toUint8Array(data);
|
|
7537
|
+
const sourceType = typeof Blob !== 'undefined' && data instanceof Blob ? data.type : undefined;
|
|
7538
|
+
return { bytes, contentType: resolveUploadMimeType({ sourceType, fileName, bytes }) };
|
|
7539
|
+
};
|
|
7540
|
+
/**
|
|
7541
|
+
* The one place an upload's bytes become a {@link PublishUploadData}: everything after this
|
|
7542
|
+
* (Content-SHA-256, tags, the L1 transaction or the bundler DataItem, signing) works from what it
|
|
7543
|
+
* returns. A future private-publish option that encrypts content belongs here, before the hash and
|
|
7544
|
+
* tags are computed, so both publish paths pick it up.
|
|
7545
|
+
*/
|
|
7546
|
+
const buildPublishUploadData = (data, contentType, target, options) => {
|
|
7547
|
+
const contentHash = getContentSha256(data);
|
|
7548
|
+
return {
|
|
7549
|
+
data,
|
|
7550
|
+
contentHash,
|
|
7551
|
+
contentType,
|
|
7552
|
+
tags: buildArweaveContentTags(contentHash, contentType, options?.arweaveUploadTags),
|
|
7553
|
+
...target,
|
|
7554
|
+
};
|
|
7555
|
+
};
|
|
7556
|
+
const isUsablePath = (filePath) => !!filePath && !filePath.endsWith('undefined') && !filePath.endsWith('null');
|
|
7557
|
+
/**
|
|
7558
|
+
* Local file of one ItemStorage property of a node. Tries, in order: its current row's
|
|
7559
|
+
* refResolvedValue + localStorageDir, then (for a local draft, or no row) the name saveItemStorage
|
|
7560
|
+
* gives a new file (`<seedUid or seedLocalId><extension>` in the model's storage directory). A
|
|
7561
|
+
* published or derived row is never swapped for a local file of another name: that file would be
|
|
7562
|
+
* older content.
|
|
7563
|
+
*
|
|
7564
|
+
* `missingLocalEdit` lists the paths tried when nothing exists although the row is a saved local
|
|
7565
|
+
* edit (no attestation uid, not derived from one, refResolvedValue set by saveItemStorage): that
|
|
7566
|
+
* edit's file should be there. A published or derived row without a file (e.g. content not
|
|
7567
|
+
* downloaded, or a property the composite never had) is skipped.
|
|
7568
|
+
*/
|
|
7569
|
+
const resolveItemStorageChildFile = async (node, property) => {
|
|
7570
|
+
const tried = [];
|
|
7571
|
+
const tryPath = async (filePath) => {
|
|
7572
|
+
if (!isUsablePath(filePath) || tried.includes(filePath))
|
|
7573
|
+
return undefined;
|
|
7574
|
+
tried.push(filePath);
|
|
7575
|
+
return (await BaseFileManager.pathExists(filePath)) ? filePath : undefined;
|
|
7576
|
+
};
|
|
7577
|
+
const { row, definition } = property;
|
|
7578
|
+
const dir = getItemStorageSubdir(definition.localStorageDir || row?.localStorageDir);
|
|
7579
|
+
if (row?.refResolvedValue) {
|
|
7580
|
+
const fromRow = await tryPath(BaseFileManager.getFilesPath(dir, row.refResolvedValue));
|
|
7581
|
+
if (fromRow)
|
|
7582
|
+
return { filePath: fromRow };
|
|
7583
|
+
}
|
|
7584
|
+
const isLocalDraft = !!row && !row.uid && row.derivedFromUid == null;
|
|
7585
|
+
const seedIds = !row || isLocalDraft ? [row?.seedUid, node.seedUid, row?.seedLocalId, node.seedLocalId] : [];
|
|
7586
|
+
for (const seedId of seedIds) {
|
|
7587
|
+
if (!seedId)
|
|
7588
|
+
continue;
|
|
7589
|
+
const derived = await tryPath(BaseFileManager.getFilesPath(dir, getItemStorageLocalFileName(seedId, definition.filenameSuffix)));
|
|
7590
|
+
if (derived)
|
|
7591
|
+
return { filePath: derived };
|
|
7592
|
+
}
|
|
7593
|
+
// saveItemStorage sets refResolvedValue on the draft row once the file is written. A draft row
|
|
7594
|
+
// without one is the empty row createNewItem makes for every model property: nothing to upload.
|
|
7595
|
+
return isLocalDraft && !!row?.refResolvedValue ? { missingLocalEdit: tried } : {};
|
|
7596
|
+
};
|
|
7597
|
+
const slotOf = (node) => node.properties.find((p) => p.kind === 'storageTransaction');
|
|
7598
|
+
/** Where an upload attesting through `node`'s storageTransactionId is recorded. */
|
|
7599
|
+
const slotTarget = (node) => {
|
|
7600
|
+
const slot = slotOf(node);
|
|
7601
|
+
return {
|
|
7602
|
+
seedLocalId: node.seedLocalId,
|
|
7603
|
+
itemPropertyName: slot?.key ?? 'storageTransactionId',
|
|
7604
|
+
itemPropertyLocalId: slot?.row?.localId ?? '',
|
|
7605
|
+
versionLocalId: slot?.row?.versionLocalId ?? node.version.localId ?? '',
|
|
7606
|
+
};
|
|
7607
|
+
};
|
|
7608
|
+
/** A node's own files: the ItemStorage properties beside its storageTransactionId, as one composite. */
|
|
7609
|
+
async function compositeSource(node) {
|
|
7610
|
+
if (!slotOf(node))
|
|
7611
|
+
return undefined;
|
|
7612
|
+
const children = [];
|
|
7613
|
+
const missingLocalEdits = [];
|
|
7614
|
+
for (const property of node.properties) {
|
|
7615
|
+
if (property.kind !== 'itemStorage')
|
|
7616
|
+
continue;
|
|
7617
|
+
const { filePath, missingLocalEdit } = await resolveItemStorageChildFile(node, property);
|
|
7618
|
+
if (filePath)
|
|
7619
|
+
children.push({ name: property.key, filePath });
|
|
7620
|
+
else if (missingLocalEdit)
|
|
7621
|
+
missingLocalEdits.push({ name: property.key, tried: missingLocalEdit });
|
|
7622
|
+
}
|
|
7623
|
+
if (children.length === 0 && missingLocalEdits.length === 0)
|
|
7624
|
+
return undefined;
|
|
7625
|
+
return { ...slotTarget(node), children, ...(missingLocalEdits.length ? { missingLocalEdits } : {}) };
|
|
7626
|
+
}
|
|
7627
|
+
/** The file of a storage seed this publish attests, named by the referring property's row. */
|
|
7628
|
+
async function storageSeedSource(owner, property) {
|
|
7629
|
+
const target = property.refs[0]?.node;
|
|
7630
|
+
const refResolvedValue = property.row?.refResolvedValue;
|
|
7631
|
+
if (!target || !refResolvedValue)
|
|
7632
|
+
return undefined;
|
|
7633
|
+
const filePath = storageUploadPath({
|
|
7634
|
+
dataType: property.definition.refValueType ?? property.definition.dataType,
|
|
7635
|
+
refResolvedValue,
|
|
7636
|
+
localStorageDir: property.row?.localStorageDir,
|
|
7637
|
+
});
|
|
7638
|
+
if (!(await BaseFileManager.pathExists(filePath)))
|
|
7639
|
+
return undefined;
|
|
7640
|
+
return {
|
|
7641
|
+
seedLocalId: target.seedLocalId,
|
|
7642
|
+
itemPropertyName: property.key,
|
|
7643
|
+
itemPropertyLocalId: property.row?.localId ?? '',
|
|
7644
|
+
versionLocalId: property.row?.versionLocalId ?? owner.version.localId ?? '',
|
|
7645
|
+
filePath,
|
|
7646
|
+
};
|
|
7647
|
+
}
|
|
7648
|
+
const isHtmlStorageProperty = (property) => isDataType(property.definition.refValueType ?? property.definition.dataType, ModelPropertyDataTypes.Html);
|
|
7649
|
+
/**
|
|
7650
|
+
* Every upload a publish makes, in graph order (the root's first), one per attesting seed. The
|
|
7651
|
+
* options select the phases of a publish with Html-embedded images: phase 1 defers the listed Html
|
|
7652
|
+
* storage seeds, phase 2 uploads only those.
|
|
7653
|
+
*/
|
|
7654
|
+
async function getPublishUploadSources(graph, options) {
|
|
7655
|
+
const onlyHtml = options?.onlyHtmlStorageSeedLocalIds?.length ? new Set(options.onlyHtmlStorageSeedLocalIds) : undefined;
|
|
7656
|
+
const deferHtml = new Set(options?.deferHtmlStorageSeedLocalIds ?? []);
|
|
7657
|
+
const sources = [];
|
|
7658
|
+
const add = (source) => {
|
|
7659
|
+
if (source && !sources.some((s) => s.seedLocalId === source.seedLocalId))
|
|
7660
|
+
sources.push(source);
|
|
7661
|
+
};
|
|
7662
|
+
for (const node of [graph.root, ...graph.nodes.filter((n) => n !== graph.root)]) {
|
|
7663
|
+
if (!onlyHtml)
|
|
7664
|
+
add(await compositeSource(node));
|
|
7665
|
+
if (node.kind !== 'root' && node.kind !== 'draft')
|
|
7666
|
+
continue;
|
|
7667
|
+
for (const property of node.properties) {
|
|
7668
|
+
if (property.kind !== 'storageSeed' || property.deferred)
|
|
7669
|
+
continue;
|
|
7670
|
+
const seedLocalId = property.refs[0]?.node?.seedLocalId;
|
|
7671
|
+
if (!seedLocalId)
|
|
7672
|
+
continue;
|
|
7673
|
+
if (isHtmlStorageProperty(property) ? deferHtml.has(seedLocalId) || (onlyHtml && !onlyHtml.has(seedLocalId)) : onlyHtml)
|
|
7674
|
+
continue;
|
|
7675
|
+
add(await storageSeedSource(node, property));
|
|
7676
|
+
}
|
|
7677
|
+
if (onlyHtml)
|
|
7678
|
+
continue;
|
|
7679
|
+
for (const row of node.embeddedImages) {
|
|
7680
|
+
const image = graph.nodes.find((n) => n.seedLocalId === row.imageSeedLocalId && n.kind === 'embeddedImage');
|
|
7681
|
+
const filePath = image ? await findEmbeddedImageFilePath(image.seedLocalId) : undefined;
|
|
7682
|
+
if (image && filePath)
|
|
7683
|
+
add({ ...slotTarget(image), filePath });
|
|
7684
|
+
}
|
|
7685
|
+
}
|
|
7686
|
+
return sources;
|
|
7687
|
+
}
|
|
7688
|
+
/** The bytes of one source; a missing local ItemStorage edit fails the publish. */
|
|
7689
|
+
async function readSource(source, options) {
|
|
7690
|
+
const { filePath, children, missingLocalEdits, ...target } = source;
|
|
7691
|
+
const missing = missingLocalEdits?.[0];
|
|
7692
|
+
if (missing) {
|
|
7693
|
+
// Uploading without it would publish a composite that silently lacks this edit.
|
|
7694
|
+
throw new Error(`Cannot publish ${source.seedLocalId}: the local file for ${missing.name} ` +
|
|
7695
|
+
`was not found (looked in ${missing.tried.join(', ')})`);
|
|
7696
|
+
}
|
|
7697
|
+
if (children) {
|
|
7698
|
+
// Composite of the node's ItemStorage files; see helpers/compositeUpload.ts for the format.
|
|
7699
|
+
// Children are read as bytes: a binary child is base64-framed instead of decoded as text.
|
|
7700
|
+
const parts = [];
|
|
7701
|
+
for (const child of children)
|
|
7702
|
+
parts.push({ name: child.name, bytes: (await readUploadFile(child.filePath)).bytes });
|
|
7703
|
+
return buildPublishUploadData(encodeCompositeUpload(parts), 'application/octet-stream', target, options);
|
|
7704
|
+
}
|
|
7705
|
+
const { bytes, contentType } = await readUploadFile(filePath);
|
|
7706
|
+
return buildPublishUploadData(bytes, contentType, target, options);
|
|
7707
|
+
}
|
|
7708
|
+
/** Fails like a publish would when an item's relation names a seed that isn't there. */
|
|
7709
|
+
function assertRefsResolve(graph) {
|
|
7710
|
+
for (const broken of graph.brokenRefs) {
|
|
7711
|
+
if (broken.list)
|
|
7712
|
+
continue;
|
|
7713
|
+
throw new Error(broken.reason === 'invalid'
|
|
7714
|
+
? `Invalid relation value for ${broken.propertyKey}: expected local seed id or 0x uid`
|
|
7715
|
+
: `No relatedItem found for ${broken.propertyKey}`);
|
|
7716
|
+
}
|
|
7717
|
+
}
|
|
7718
|
+
/**
|
|
7719
|
+
* True if publish would include at least one Arweave upload (local file / storage seed present,
|
|
7720
|
+
* or a saved local edit whose file is missing, which the upload then reports). Reads rows and
|
|
7721
|
+
* file metadata only, for routing (e.g. skip the Arweave steps).
|
|
7722
|
+
*/
|
|
7723
|
+
async function itemHasPublishUploadCandidates(item) {
|
|
7724
|
+
const graph = await getPublishGraph(item);
|
|
7725
|
+
assertRefsResolve(graph);
|
|
7726
|
+
return (await getPublishUploadSources(graph)).length > 0;
|
|
7727
|
+
}
|
|
7728
|
+
/**
|
|
7729
|
+
* Upload data for a publish of `item`: its own, and that of every seed the publish attests with it
|
|
7730
|
+
* (see publishGraph.ts). Raw bytes plus Content-SHA-256 / Content-Type / configured tags; no
|
|
7731
|
+
* Arweave transaction is created, so this does not touch the network. The bundler path in
|
|
7732
|
+
* `@seedprotocol/publish` signs these as DataItems; {@link getPublishUploads} turns them into L1
|
|
7733
|
+
* transactions.
|
|
7734
|
+
*/
|
|
7735
|
+
const getPublishUploadData = async (item, options) => {
|
|
7736
|
+
const graph = await getPublishGraph(item);
|
|
7737
|
+
assertRefsResolve(graph);
|
|
7738
|
+
const sources = await getPublishUploadSources(graph, options);
|
|
7739
|
+
logger('%d uploads for %s', sources.length, item.seedLocalId);
|
|
7740
|
+
const uploads = [];
|
|
7741
|
+
for (const source of sources)
|
|
7742
|
+
uploads.push(await readSource(source, options));
|
|
7743
|
+
return uploads;
|
|
7744
|
+
};
|
|
7745
|
+
/**
|
|
7746
|
+
* Arweave L1 transactions for a publish of `item`: {@link getPublishUploadData}, each made into an
|
|
7747
|
+
* unsigned transaction carrying the same tags.
|
|
7748
|
+
*/
|
|
7749
|
+
const getPublishUploads = async (item, options) => {
|
|
7750
|
+
const uploadData = await getPublishUploadData(item, options);
|
|
7751
|
+
// Each createTransaction fetches the price / anchor from the gateway: run a few at once.
|
|
7752
|
+
return mapWithConcurrency(uploadData, CREATE_TRANSACTION_CONCURRENCY, async (upload) => ({
|
|
7753
|
+
itemPropertyName: upload.itemPropertyName,
|
|
7754
|
+
itemPropertyLocalId: upload.itemPropertyLocalId,
|
|
7755
|
+
seedLocalId: upload.seedLocalId,
|
|
7756
|
+
versionLocalId: upload.versionLocalId,
|
|
7757
|
+
transactionToSign: await BaseArweaveClient.createTransaction(upload.data, {
|
|
7758
|
+
tags: upload.tags.length ? upload.tags : undefined,
|
|
7759
|
+
}),
|
|
7760
|
+
}));
|
|
7761
|
+
};
|
|
7762
|
+
|
|
7763
|
+
var getPublishUploads$1 = /*#__PURE__*/Object.freeze({
|
|
7764
|
+
__proto__: null,
|
|
7765
|
+
CREATE_TRANSACTION_CONCURRENCY: CREATE_TRANSACTION_CONCURRENCY,
|
|
7766
|
+
getPublishUploadData: getPublishUploadData,
|
|
7767
|
+
getPublishUploadSources: getPublishUploadSources,
|
|
7768
|
+
getPublishUploads: getPublishUploads,
|
|
7769
|
+
itemHasPublishUploadCandidates: itemHasPublishUploadCandidates,
|
|
7770
|
+
resolveItemStorageChildFile: resolveItemStorageChildFile
|
|
7771
|
+
});
|
|
7772
|
+
|
|
7773
|
+
/** The item can't be exported as it is. `code` says why. */
|
|
7774
|
+
class ArchiveExportError extends Error {
|
|
7775
|
+
constructor(code, message) {
|
|
7776
|
+
super(message);
|
|
7777
|
+
this.name = 'ArchiveExportError';
|
|
7778
|
+
this.code = code;
|
|
7779
|
+
}
|
|
7780
|
+
}
|
|
7781
|
+
const ARWEAVE_TX_ID = /^[a-z0-9_-]{43}$/i;
|
|
7782
|
+
const SOFTWARE_AGENT = '@seedprotocol/sdk exportArchive (https://github.com/JournoLabs/seed-protocol-sdk)';
|
|
7783
|
+
const isoFromMs = (ms) => typeof ms === 'number' && Number.isFinite(ms) && ms > 0 ? new Date(ms).toISOString() : undefined;
|
|
7784
|
+
const toBytes = async (data) => typeof Blob !== 'undefined' && data instanceof Blob ? new Uint8Array(await data.arrayBuffer()) : new Uint8Array(data);
|
|
7785
|
+
const readBytes = async (filePath) => toBytes(await BaseFileManager.readFileAsBuffer(filePath));
|
|
7786
|
+
/** The Arweave transaction id a gateway URL serves (`https://<host>/<tx>` or `/raw/<tx>`), if it is one. */
|
|
7787
|
+
function arweaveTxIdOfUrl(url) {
|
|
7788
|
+
try {
|
|
7789
|
+
const parsed = new URL(url);
|
|
7790
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:')
|
|
7791
|
+
return undefined;
|
|
7792
|
+
const segments = parsed.pathname.split('/').filter(Boolean);
|
|
7793
|
+
const last = segments[segments.length - 1];
|
|
7794
|
+
const isTxPath = segments.length === 1 || (segments.length === 2 && segments[0] === 'raw');
|
|
7795
|
+
return isTxPath && last && ARWEAVE_TX_ID.test(last) ? last : undefined;
|
|
7796
|
+
}
|
|
7797
|
+
catch {
|
|
7798
|
+
return undefined;
|
|
7799
|
+
}
|
|
7800
|
+
}
|
|
7801
|
+
const hasValue = (value) => value != null && value !== '';
|
|
7802
|
+
/** A saved local value that no attestation backs: the bag would mix it with the published record. */
|
|
7803
|
+
const isUnpublishedEdit = (property) => {
|
|
7804
|
+
const row = property.row;
|
|
7805
|
+
return !!row && !property.deferred && !row.uid && row.derivedFromUid == null && hasValue(row.propertyValue);
|
|
7806
|
+
};
|
|
7807
|
+
/**
|
|
7808
|
+
* Builds the bag for `item`'s current published state:
|
|
7809
|
+
*
|
|
7810
|
+
* - `data/index.html` (or `data/<property>.html` when the model has several Html properties): the
|
|
7811
|
+
* Html with references to files in the bag rewritten to relative paths.
|
|
7812
|
+
* - `data/source/<property>.html`: each Html exactly as published.
|
|
7813
|
+
* - `data/assets/<sha256>.<ext>`: Images, Files and images the Html references.
|
|
7814
|
+
* - `data/item.json`: every property's value, attestation uid and Arweave transaction.
|
|
7815
|
+
* - `metadata/dc.xml` and `metadata/premis.xml`.
|
|
7816
|
+
*
|
|
7817
|
+
* Throws {@link ArchiveExportError} `not_published` for an item without a live published version,
|
|
7818
|
+
* and `unpublished_changes` when a property holds a saved value that isn't published yet.
|
|
7819
|
+
* A file that can't be read or downloaded is left out and reported in `omitted`, not thrown.
|
|
7820
|
+
*/
|
|
7821
|
+
async function exportArchive(item, options = {}) {
|
|
7822
|
+
const fetch = options.fetch ?? true;
|
|
7823
|
+
const now = options.baggingDate ?? new Date();
|
|
7824
|
+
const graph = await getPublishGraph(item);
|
|
7825
|
+
const root = graph.root;
|
|
7826
|
+
const seedRow = await findSeedRow({ localId: root.seedLocalId });
|
|
7827
|
+
const seedUid = root.seedUid;
|
|
7828
|
+
const version = await getLatestPublishedVersionRow(root.seedLocalId);
|
|
7829
|
+
if (!seedUid || !isValidEasAttestationUid(seedUid) || seedRow?.revokedAt || !version) {
|
|
7830
|
+
throw new ArchiveExportError('not_published', `${root.modelName} ${root.seedLocalId} has no live published version`);
|
|
7831
|
+
}
|
|
7832
|
+
const unpublished = root.properties.filter(isUnpublishedEdit).map((p) => p.key);
|
|
7833
|
+
if (unpublished.length > 0) {
|
|
7834
|
+
throw new ArchiveExportError('unpublished_changes', `${root.modelName} ${root.seedLocalId} has unpublished changes to ${unpublished.join(', ')}; publish them first`);
|
|
7835
|
+
}
|
|
7836
|
+
const chainId = getEasReadChainId();
|
|
7837
|
+
const chain = getEasChainDeployment(chainId);
|
|
7838
|
+
const explorerUrl = (uid) => (chain ? getEasAttestationExplorerUrl(chain, uid) : undefined);
|
|
7839
|
+
const schemaName = item.getService?.().getSnapshot().context.schemaName;
|
|
7840
|
+
const attester = seedRow?.publisher ?? undefined;
|
|
7841
|
+
const omitted = [];
|
|
7842
|
+
const payload = [];
|
|
7843
|
+
const fileInfo = new Map();
|
|
7844
|
+
const assetPathByTx = new Map();
|
|
7845
|
+
const assetPathByProperty = new Map();
|
|
7846
|
+
const htmlSources = [];
|
|
7847
|
+
const properties = {};
|
|
7848
|
+
const addAsset = (bytes, mimeType, arweaveTransactionId) => {
|
|
7849
|
+
const ext = getExtensionForMimeType(mimeType);
|
|
7850
|
+
const path = `assets/${digestHex('sha256', bytes)}${ext ? `.${ext}` : ''}`;
|
|
7851
|
+
if (!payload.some((f) => f.path === path)) {
|
|
7852
|
+
payload.push({ path, bytes });
|
|
7853
|
+
fileInfo.set(path, { mimeType, arweaveTransactionId });
|
|
7854
|
+
}
|
|
7855
|
+
if (arweaveTransactionId)
|
|
7856
|
+
assetPathByTx.set(arweaveTransactionId, path);
|
|
7857
|
+
return path;
|
|
7858
|
+
};
|
|
7859
|
+
for (const property of root.properties) {
|
|
7860
|
+
const { key, definition: def, kind, row } = property;
|
|
7861
|
+
const dataType = normalizeDataType(def.dataType) ?? String(def.dataType);
|
|
7862
|
+
const entry = {
|
|
7863
|
+
dataType,
|
|
7864
|
+
attestationUid: row?.uid ?? row?.derivedFromUid ?? undefined,
|
|
7865
|
+
attestedAt: isoFromMs(row?.attestationCreatedAt),
|
|
7866
|
+
};
|
|
7867
|
+
properties[key] = entry;
|
|
7868
|
+
if (kind === 'basic' || kind === 'storageTransaction') {
|
|
7869
|
+
entry.value = row?.propertyValue ?? null;
|
|
7870
|
+
continue;
|
|
7871
|
+
}
|
|
7872
|
+
if (kind === 'relation' || kind === 'list') {
|
|
7873
|
+
entry.value = kind === 'list' ? property.refs.map((r) => r.ref) : (property.refs[0]?.ref ?? null);
|
|
7874
|
+
continue;
|
|
7875
|
+
}
|
|
7876
|
+
if (kind === 'itemStorage') {
|
|
7877
|
+
const { filePath } = await resolveItemStorageChildFile(root, property);
|
|
7878
|
+
if (!filePath) {
|
|
7879
|
+
if (row && hasValue(row.propertyValue))
|
|
7880
|
+
omitted.push({ property: key, reason: 'its file is not on this device' });
|
|
7881
|
+
continue;
|
|
7882
|
+
}
|
|
7883
|
+
const bytes = await readBytes(filePath);
|
|
7884
|
+
if (dataType === 'Html') {
|
|
7885
|
+
htmlSources.push({ key, text: new TextDecoder().decode(bytes) });
|
|
7886
|
+
}
|
|
7887
|
+
else {
|
|
7888
|
+
const mimeType = resolveUploadMimeType({ fileName: filePath, bytes }) ?? 'application/octet-stream';
|
|
7889
|
+
entry.file = addAsset(bytes, mimeType);
|
|
7890
|
+
assetPathByProperty.set(key, entry.file);
|
|
7891
|
+
}
|
|
7892
|
+
continue;
|
|
7893
|
+
}
|
|
7894
|
+
// storageSeed: the value is a storage seed whose own row records the Arweave transaction.
|
|
7895
|
+
if (!row || !hasValue(row.propertyValue))
|
|
7896
|
+
continue;
|
|
7897
|
+
const storageKind = storedValueKind(def, row);
|
|
7898
|
+
if (!storageKind || storageKind === 'itemStorage') {
|
|
7899
|
+
omitted.push({ property: key, reason: `${dataType} storage seeds are not exported yet` });
|
|
7900
|
+
continue;
|
|
7901
|
+
}
|
|
7902
|
+
const arweaveTransactionId = await storageSeedTransactionId(resolveSeedIdsFromRefString(normalizeRelationPropertyValue(row.propertyValue) ?? ''));
|
|
7903
|
+
if (arweaveTransactionId)
|
|
7904
|
+
entry.arweaveTransactionId = arweaveTransactionId;
|
|
7905
|
+
const display = await resolveStorageDisplay(storageKind, row, { fetch });
|
|
7906
|
+
if (storageKind === 'html') {
|
|
7907
|
+
const text = display.filePath ? await BaseFileManager.readFileAsString(display.filePath) : display.renderValue;
|
|
7908
|
+
if (text === undefined) {
|
|
7909
|
+
omitted.push({ property: key, reason: 'its Html could not be read or downloaded' });
|
|
7910
|
+
continue;
|
|
7911
|
+
}
|
|
7912
|
+
htmlSources.push({ key, text, arweaveTransactionId });
|
|
7913
|
+
continue;
|
|
7914
|
+
}
|
|
7915
|
+
if (!display.filePath) {
|
|
7916
|
+
omitted.push({ property: key, reason: 'its file could not be read or downloaded' });
|
|
7917
|
+
continue;
|
|
7918
|
+
}
|
|
7919
|
+
const bytes = await readBytes(display.filePath);
|
|
7920
|
+
const mimeType = resolveUploadMimeType({ fileName: display.filePath, bytes }) ?? 'application/octet-stream';
|
|
7921
|
+
entry.file = addAsset(bytes, mimeType, arweaveTransactionId);
|
|
7922
|
+
assetPathByProperty.set(key, entry.file);
|
|
7923
|
+
}
|
|
7924
|
+
/** The bag path for an image the Html names by transaction id but no property holds (an embedded image). */
|
|
7925
|
+
const embeddedAssetPath = async (txId) => {
|
|
7926
|
+
const known = assetPathByTx.get(txId);
|
|
7927
|
+
if (known)
|
|
7928
|
+
return known;
|
|
7929
|
+
const imageRow = await getMetadata({ storageTransactionId: txId });
|
|
7930
|
+
let filePath = imageRow?.seedLocalId ? await findEmbeddedImageFilePath(imageRow.seedLocalId) : undefined;
|
|
7931
|
+
if (!filePath) {
|
|
7932
|
+
const display = await resolveStorageDisplay('image', { refResolvedValue: txId, localStorageDir: '/images' }, { fetch });
|
|
7933
|
+
filePath = display.filePath;
|
|
7934
|
+
}
|
|
7935
|
+
if (!filePath)
|
|
7936
|
+
return undefined;
|
|
7937
|
+
const bytes = await readBytes(filePath);
|
|
7938
|
+
const mimeType = resolveUploadMimeType({ fileName: filePath, bytes }) ?? 'application/octet-stream';
|
|
7939
|
+
return addAsset(bytes, mimeType, txId);
|
|
7940
|
+
};
|
|
7941
|
+
const encoder = new TextEncoder();
|
|
7942
|
+
for (const html of htmlSources) {
|
|
7943
|
+
const targets = new Map();
|
|
7944
|
+
// Collect first: rewriteHtmlUrls takes a synchronous mapping.
|
|
7945
|
+
rewriteHtmlUrls(html.text, (url) => {
|
|
7946
|
+
targets.set(url, url);
|
|
7947
|
+
return undefined;
|
|
7948
|
+
});
|
|
7949
|
+
for (const url of targets.keys()) {
|
|
7950
|
+
if (url.startsWith(SEED_PROPERTY_REF_PREFIX)) {
|
|
7951
|
+
const path = assetPathByProperty.get(url.slice(SEED_PROPERTY_REF_PREFIX.length));
|
|
7952
|
+
if (path)
|
|
7953
|
+
targets.set(url, path);
|
|
7954
|
+
else
|
|
7955
|
+
targets.delete(url);
|
|
7956
|
+
continue;
|
|
7957
|
+
}
|
|
7958
|
+
const txId = arweaveTxIdOfUrl(url);
|
|
7959
|
+
const path = txId ? await embeddedAssetPath(txId) : undefined;
|
|
7960
|
+
if (path)
|
|
7961
|
+
targets.set(url, path);
|
|
7962
|
+
else {
|
|
7963
|
+
targets.delete(url);
|
|
7964
|
+
if (txId)
|
|
7965
|
+
omitted.push({ property: html.key, reason: `references ${url}, which is not in the bag` });
|
|
7966
|
+
}
|
|
7967
|
+
}
|
|
7968
|
+
const rewritten = rewriteHtmlUrls(html.text, (url) => targets.get(url));
|
|
7969
|
+
const viewPath = htmlSources.length === 1 ? 'index.html' : `${html.key}.html`;
|
|
7970
|
+
payload.push({ path: viewPath, bytes: encoder.encode(rewritten) });
|
|
7971
|
+
fileInfo.set(viewPath, { mimeType: 'text/html' });
|
|
7972
|
+
const sourcePath = `source/${html.key}.html`;
|
|
7973
|
+
payload.push({ path: sourcePath, bytes: encoder.encode(html.text) });
|
|
7974
|
+
fileInfo.set(sourcePath, { mimeType: 'text/html', arweaveTransactionId: html.arweaveTransactionId });
|
|
7975
|
+
properties[html.key].file = sourcePath;
|
|
7976
|
+
}
|
|
7977
|
+
const itemJson = {
|
|
7978
|
+
seedUid,
|
|
7979
|
+
versionUid: version.uid,
|
|
7980
|
+
schemaName,
|
|
7981
|
+
modelName: root.modelName,
|
|
7982
|
+
chain: { id: chainId, name: chain?.name },
|
|
7983
|
+
attester,
|
|
7984
|
+
properties,
|
|
7985
|
+
omitted,
|
|
7986
|
+
};
|
|
7987
|
+
payload.push({ path: 'item.json', bytes: encoder.encode(`${JSON.stringify(itemJson, null, 2)}\n`) });
|
|
7988
|
+
fileInfo.set('item.json', { mimeType: 'application/json' });
|
|
7989
|
+
// Descriptive metadata comes from the archive document, so nothing is guessed from property names.
|
|
7990
|
+
const primary = htmlSources[0];
|
|
7991
|
+
const doc = primary ? parseArchiveDocument(primary.text) : undefined;
|
|
7992
|
+
const versionDate = isoFromMs(version.attestationCreatedAt);
|
|
7993
|
+
const dc = dublinCoreXml({
|
|
7994
|
+
title: doc?.name,
|
|
7995
|
+
creator: doc?.authors,
|
|
7996
|
+
description: doc?.summary,
|
|
7997
|
+
date: doc?.published ?? versionDate?.slice(0, 10),
|
|
7998
|
+
type: 'Text',
|
|
7999
|
+
format: primary ? 'text/html' : undefined,
|
|
8000
|
+
identifier: [seedUid, explorerUrl(seedUid)].filter((v) => !!v),
|
|
8001
|
+
source: doc?.url,
|
|
8002
|
+
});
|
|
8003
|
+
const premis = premisXml({
|
|
8004
|
+
objects: [
|
|
8005
|
+
{
|
|
8006
|
+
kind: 'intellectualEntity',
|
|
8007
|
+
identifiers: [
|
|
8008
|
+
{ type: 'Seed Protocol seed UID', value: seedUid },
|
|
8009
|
+
{ type: 'Seed Protocol version UID', value: version.uid },
|
|
8010
|
+
],
|
|
8011
|
+
},
|
|
8012
|
+
...payload.map((file) => {
|
|
8013
|
+
const info = fileInfo.get(file.path);
|
|
8014
|
+
return {
|
|
8015
|
+
kind: 'file',
|
|
8016
|
+
identifiers: [{ type: 'bag path', value: `data/${file.path}` }],
|
|
8017
|
+
fixity: { 'SHA-256': digestHex('sha256', file.bytes), 'SHA-512': digestHex('sha512', file.bytes) },
|
|
8018
|
+
size: file.bytes.byteLength,
|
|
8019
|
+
formatName: info?.mimeType ?? 'application/octet-stream',
|
|
8020
|
+
originalName: info?.arweaveTransactionId ? getPublicArweaveRawUrl(info.arweaveTransactionId) : undefined,
|
|
8021
|
+
};
|
|
8022
|
+
}),
|
|
8023
|
+
],
|
|
8024
|
+
events: [...(await historyEvents(root.seedLocalId, seedUid, version, attester)), ...bagEvents(now, seedUid)],
|
|
8025
|
+
agents: [
|
|
8026
|
+
...(attester
|
|
8027
|
+
? [{ identifiers: [{ type: 'EVM address', value: attester }], note: `EAS attester on chain ${chainId}` }]
|
|
8028
|
+
: []),
|
|
8029
|
+
{ identifiers: [{ type: 'software', value: '@seedprotocol/sdk' }], name: '@seedprotocol/sdk', type: 'software' },
|
|
8030
|
+
],
|
|
8031
|
+
});
|
|
8032
|
+
const info = [];
|
|
8033
|
+
if (options.sourceOrganization)
|
|
8034
|
+
info.push(['Source-Organization', options.sourceOrganization]);
|
|
8035
|
+
info.push(['External-Identifier', seedUid]);
|
|
8036
|
+
if (doc?.name)
|
|
8037
|
+
info.push(['External-Description', doc.name]);
|
|
8038
|
+
info.push(['Seed-Uid', seedUid], ['Seed-Version-Uid', version.uid], ['Seed-Model', root.modelName]);
|
|
8039
|
+
if (schemaName)
|
|
8040
|
+
info.push(['Seed-Schema', schemaName]);
|
|
8041
|
+
info.push(['EAS-Chain-Id', String(chainId)]);
|
|
8042
|
+
if (chain?.name)
|
|
8043
|
+
info.push(['EAS-Chain-Name', chain.name]);
|
|
8044
|
+
if (attester)
|
|
8045
|
+
info.push(['EAS-Attester', attester]);
|
|
8046
|
+
const bag = createBag({
|
|
8047
|
+
payload,
|
|
8048
|
+
tagFiles: [
|
|
8049
|
+
{ path: 'metadata/dc.xml', bytes: encoder.encode(dc) },
|
|
8050
|
+
{ path: 'metadata/premis.xml', bytes: encoder.encode(premis) },
|
|
8051
|
+
],
|
|
8052
|
+
info,
|
|
8053
|
+
baggingDate: now,
|
|
8054
|
+
softwareAgent: SOFTWARE_AGENT,
|
|
8055
|
+
});
|
|
8056
|
+
return { bag, omitted };
|
|
8057
|
+
}
|
|
8058
|
+
/**
|
|
8059
|
+
* The seed's attested history: its first live Version as creation, later Versions and property
|
|
8060
|
+
* attestations made after the current Version (patches) as modifications. Attestations without
|
|
8061
|
+
* a recorded time are left out, since PREMIS events need one.
|
|
8062
|
+
*/
|
|
8063
|
+
async function historyEvents(seedLocalId, seedUid, current, attester) {
|
|
8064
|
+
const db = BaseDb.getAppDb();
|
|
8065
|
+
const agents = attester ? [{ type: 'EVM address', value: attester, role: 'attester' }] : [];
|
|
8066
|
+
const objects = [{ type: 'Seed Protocol seed UID', value: seedUid }];
|
|
8067
|
+
const versionRows = (await db
|
|
8068
|
+
.select({ uid: versions.uid, attestationCreatedAt: versions.attestationCreatedAt, revokedAt: versions.revokedAt })
|
|
8069
|
+
.from(versions)
|
|
8070
|
+
.where(eq(versions.seedLocalId, seedLocalId)))
|
|
8071
|
+
.filter((v) => isValidEasAttestationUid(v.uid) && v.attestationCreatedAt)
|
|
8072
|
+
.sort((a, b) => a.attestationCreatedAt - b.attestationCreatedAt);
|
|
8073
|
+
const events = versionRows.map((v, index) => ({
|
|
8074
|
+
identifier: { type: 'EAS attestation UID', value: v.uid },
|
|
8075
|
+
type: index === 0 ? 'creation' : 'modification',
|
|
8076
|
+
dateTime: isoFromMs(v.attestationCreatedAt),
|
|
8077
|
+
detail: index === 0 ? 'First Version attested' : 'New Version attested',
|
|
8078
|
+
outcome: 'success',
|
|
8079
|
+
outcomeNote: v.revokedAt ? `Version revoked at ${new Date(v.revokedAt * 1000).toISOString()}` : undefined,
|
|
8080
|
+
agents,
|
|
8081
|
+
objects,
|
|
8082
|
+
}));
|
|
8083
|
+
if (current.attestationCreatedAt) {
|
|
8084
|
+
const patches = (await db
|
|
8085
|
+
.select({ uid: metadata$1.uid, propertyName: metadata$1.propertyName, attestationCreatedAt: metadata$1.attestationCreatedAt })
|
|
8086
|
+
.from(metadata$1)
|
|
8087
|
+
.where(and(eq(metadata$1.seedLocalId, seedLocalId), eq(metadata$1.versionUid, current.uid))));
|
|
8088
|
+
for (const row of patches) {
|
|
8089
|
+
if (!isValidEasAttestationUid(row.uid) || !row.attestationCreatedAt)
|
|
8090
|
+
continue;
|
|
8091
|
+
if (row.attestationCreatedAt <= current.attestationCreatedAt)
|
|
8092
|
+
continue;
|
|
8093
|
+
events.push({
|
|
8094
|
+
identifier: { type: 'EAS attestation UID', value: row.uid },
|
|
8095
|
+
type: 'modification',
|
|
8096
|
+
dateTime: isoFromMs(row.attestationCreatedAt),
|
|
8097
|
+
detail: `Property ${row.propertyName} attested again on Version ${current.uid}`,
|
|
8098
|
+
outcome: 'success',
|
|
8099
|
+
agents,
|
|
8100
|
+
objects,
|
|
8101
|
+
});
|
|
8102
|
+
}
|
|
8103
|
+
}
|
|
8104
|
+
return events.sort((a, b) => a.dateTime.localeCompare(b.dateTime));
|
|
8105
|
+
}
|
|
8106
|
+
/** The bag's own events: its checksums and the package itself. */
|
|
8107
|
+
const bagEvents = (now, seedUid) => {
|
|
8108
|
+
const agents = [{ type: 'software', value: '@seedprotocol/sdk', role: 'executing program' }];
|
|
8109
|
+
const objects = [{ type: 'Seed Protocol seed UID', value: seedUid }];
|
|
8110
|
+
const dateTime = now.toISOString();
|
|
8111
|
+
return [
|
|
8112
|
+
{
|
|
8113
|
+
identifier: { type: 'bag event', value: `message-digest-${dateTime}` },
|
|
8114
|
+
type: 'message digest calculation',
|
|
8115
|
+
dateTime,
|
|
8116
|
+
detail: 'SHA-256 and SHA-512 of every payload and tag file',
|
|
8117
|
+
outcome: 'success',
|
|
8118
|
+
agents,
|
|
8119
|
+
objects,
|
|
8120
|
+
},
|
|
8121
|
+
{
|
|
8122
|
+
identifier: { type: 'bag event', value: `package-${dateTime}` },
|
|
8123
|
+
type: 'information package creation',
|
|
8124
|
+
dateTime,
|
|
8125
|
+
detail: 'BagIt 1.0 bag written by exportArchive',
|
|
8126
|
+
outcome: 'success',
|
|
8127
|
+
agents,
|
|
8128
|
+
objects,
|
|
8129
|
+
},
|
|
8130
|
+
];
|
|
8131
|
+
};
|
|
8132
|
+
|
|
6588
8133
|
/**
|
|
6589
8134
|
* Thrown by Item.create when values don't fit the model: unknown keys, wrong types, or values
|
|
6590
8135
|
* that break the property's validation rules. Nothing is left written: most values are checked
|
|
@@ -6634,5 +8179,45 @@ var errors = /*#__PURE__*/Object.freeze({
|
|
|
6634
8179
|
UnknownModelError: UnknownModelError
|
|
6635
8180
|
});
|
|
6636
8181
|
|
|
6637
|
-
|
|
6638
|
-
|
|
8182
|
+
/** Error thrown when publish validation fails. Includes all validation errors for user to fix. */
|
|
8183
|
+
class PublishValidationFailedError extends Error {
|
|
8184
|
+
constructor(message, validationErrors) {
|
|
8185
|
+
super(message);
|
|
8186
|
+
this.validationErrors = validationErrors;
|
|
8187
|
+
this.name = 'PublishValidationFailedError';
|
|
8188
|
+
}
|
|
8189
|
+
}
|
|
8190
|
+
/** Validation error code for a relation, list member or image/file ref to an unpublished seed. */
|
|
8191
|
+
const RELATED_ITEM_UNPUBLISHED_CODE = 'related_item_unpublished';
|
|
8192
|
+
const describeUnpublishedRelatedItem = (r) => `${r.propertyName} -> ${r.modelName} ${r.seedLocalId} (seed ${r.seedUid})`;
|
|
8193
|
+
/**
|
|
8194
|
+
* Thrown before anything is uploaded or attested when the item references items whose seed was
|
|
8195
|
+
* unpublished: the parent would attest a revoked seed uid. Republish those items (which gives
|
|
8196
|
+
* them a new seed uid) or remove the references, then publish again.
|
|
8197
|
+
* See docs/ATTESTATION_REVOCATION.md ("Republishing").
|
|
8198
|
+
*
|
|
8199
|
+
* A PublishValidationFailedError, so validateItemForPublish and the publish package's checking
|
|
8200
|
+
* step report it as validation errors (code `related_item_unpublished`, one per reference).
|
|
8201
|
+
*/
|
|
8202
|
+
class RelatedItemUnpublishedError extends PublishValidationFailedError {
|
|
8203
|
+
constructor(unpublishedRelatedItems, otherValidationErrors = []) {
|
|
8204
|
+
const lines = unpublishedRelatedItems.map(describeUnpublishedRelatedItem);
|
|
8205
|
+
const message = `Cannot publish: it references ${lines.length} unpublished item${lines.length === 1 ? '' : 's'} ` +
|
|
8206
|
+
`(their seed attestations were revoked). Republish them or remove the references, then publish again:\n` +
|
|
8207
|
+
lines.join('\n');
|
|
8208
|
+
super(message, [
|
|
8209
|
+
...unpublishedRelatedItems.map((r) => ({
|
|
8210
|
+
field: r.propertyName,
|
|
8211
|
+
message: `${r.propertyName} references unpublished ${r.modelName} ${r.seedLocalId} (seed ${r.seedUid}). ` +
|
|
8212
|
+
`Republish it or remove the reference.`,
|
|
8213
|
+
code: RELATED_ITEM_UNPUBLISHED_CODE,
|
|
8214
|
+
})),
|
|
8215
|
+
...otherValidationErrors,
|
|
8216
|
+
]);
|
|
8217
|
+
this.name = 'RelatedItemUnpublishedError';
|
|
8218
|
+
this.unpublishedRelatedItems = unpublishedRelatedItems;
|
|
8219
|
+
}
|
|
8220
|
+
}
|
|
8221
|
+
|
|
8222
|
+
export { extractDataUriImagesFromHtml as $, ArchiveExportError as A, waitForEasReadChain as B, ConflictError as C, assertLocalDbChain as D, writeModelUids as E, getPublishGraph as F, htmlPropertyForHtmlSeed as G, HtmlEmbeddedDataUriLimitError as H, ItemCreateError as I, saveDefinition as J, getPublishUploadSources as K, EAS_CHAIN_ID_APP_STATE_KEY as L, Model as M, HTML_EMBEDDED_MAX_IMAGES_PER_DOC as N, HTML_EMBEDDED_MAX_IMAGE_BYTES as O, PublishValidationFailedError as P, HTML_EMBEDDED_MAX_TOTAL_BYTES as Q, RELATED_ITEM_UNPUBLISHED_CODE as R, SCHEMA_FILE_FORMAT as S, TModelSchema as T, UnknownModelError as U, INIT_CONFIG_CONFLICT_MESSAGE as V, SEED_PROPERTY_REF_PREFIX as W, claimUnpublishedDrafts as X, clearHtmlEmbeddedImageCoPublishRows as Y, createLocalQueryDataSource as Z, easSyncActor as _, HtmlSeedPropertyRefError as a, findSeedPropertyRefsInHtml as a0, getClient as a1, getPublishUploadData as a2, getPublishedSeedRecord as a3, getSchemaNameFromId as a4, isSeedQueryLocalSourceRegistered as a5, itemHasPublishUploadCandidates as a6, listCompleteSchemaFiles as a7, mergeEasSyncRequestIntents as a8, prepareHtmlEmbeddedImagesForPublish as a9, registerSeedQueryLocalSource as aa, replaceDataUrisInParsedHtml as ab, replaceSeedPropertyRefsInHtml as ac, requestEasSyncFromAddressChange as ad, requestEasSyncFromModelsInit as ae, resolveEffectiveHtmlEmbeddedDataUriPolicy as af, rewriteHtmlEmbeddedImagesOnDisk as ag, sendEasSyncClientRequest as ah, setRetriesWhenOnlineEnabled as ai, startEasSyncActor as aj, unregisterSeedQueryLocalSource as ak, saveDefinition$1 as al, Model$1 as am, schema as an, SEEDPROTOCOL_Seed_Protocol_v1 as ao, itemAppliers as ap, getPublishUploads$1 as aq, errors as ar, ItemCreateValidationError as b, RelatedItemUnpublishedError as c, deleteFiles as d, RelationTargetError as e, ClientManager as f, exportArchive as g, syncSchemaFromSource as h, importJsonSchema as i, transformImportToSchemaFile as j, localModelSchemaLookups as k, loadSchemaFromFile as l, listLatestSchemaFiles as m, readSchemaFile as n, applySchemaFile as o, resolveSchemaRow as p, loadAllSchemasFromDb as q, readJsonImportFile as r, startItemOutbox as s, throwItemWriteError as t, getDraftSaveError as u, queueDraftSave as v, writeNextSchemaVersion as w, setupEntityLiveQuery as x, mergeSchemaFiles as y, trackDraftSave as z };
|
|
8223
|
+
//# sourceMappingURL=publishErrors-DZXDX3B6.js.map
|