@seedprotocol/sdk 0.7.11-next.1384.g8b47246 → 0.7.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BaseFileManager-DLou1TZS.js → BaseFileManager-D-pLXQAp.js} +2 -14
- package/dist/BaseFileManager-D-pLXQAp.js.map +1 -0
- package/dist/{Item-Ld1iPicQ.js → Item-BU9wIT7l.js} +61 -59
- package/dist/Item-BU9wIT7l.js.map +1 -0
- package/dist/{ModelProperty-C8gTx4pO.js → ModelProperty-Bv7KzP97.js} +40 -37
- package/dist/ModelProperty-Bv7KzP97.js.map +1 -0
- package/dist/{Schema-BzeBUZxi.js → Schema-C_4wv0RP.js} +22 -22
- package/dist/{Schema-BzeBUZxi.js.map → Schema-C_4wv0RP.js.map} +1 -1
- package/dist/{SchemaValidationService-CW0s7V7R.js → SchemaValidationService-Dc-lpFQX.js} +9 -6
- package/dist/{SchemaValidationService-CW0s7V7R.js.map → SchemaValidationService-Dc-lpFQX.js.map} +1 -1
- package/dist/{tabEvents-BnBzIdEi.js → arweaveContentTags-iW_HcWJA.js} +53 -5
- 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-DV1OXCp0.d.ts → index.node-C3rI1upv.d.ts} +1 -253
- package/dist/dist/{index.node-DV1OXCp0.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-Cvac_VGu.js → download-DHWUPLlK.js} +4 -5
- package/dist/{download-Cvac_VGu.js.map → download-DHWUPLlK.js.map} +1 -1
- package/dist/{eas-CdhfODXL.js → eas-MXEdKKVc.js} +10 -7
- package/dist/{eas-CdhfODXL.js.map → eas-MXEdKKVc.js.map} +1 -1
- package/dist/{ensureImageLocal-DNM6c2yD.js → ensureImageLocal-B6suz6FC.js} +8 -9
- package/dist/{ensureImageLocal-DNM6c2yD.js.map → ensureImageLocal-B6suz6FC.js.map} +1 -1
- package/dist/{getPublishPayload-CLrAcZwr.js → getPublishPayload-Dd3HCYeI.js} +8 -49
- package/dist/getPublishPayload-Dd3HCYeI.js.map +1 -0
- package/dist/{getPublishPendingDiff-DiPff8pS.js → getPublishPendingDiff-kcTP52Kj.js} +11 -9
- package/dist/{getPublishPendingDiff-DiPff8pS.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-9udoYObJ.js → index-BXTyvnKb.js} +11 -8
- package/dist/index-BXTyvnKb.js.map +1 -0
- package/dist/{index-CghAYA-z.js → index-CmNqUKQN.js} +11 -8
- package/dist/index-CmNqUKQN.js.map +1 -0
- package/dist/{index-Cdm7Q7el.js → index-Xs_EDi-_.js} +18 -403
- package/dist/index-Xs_EDi-_.js.map +1 -0
- package/dist/{index.browser-D9oC-KKx.js → index.browser-BkcOQGhw.js} +25 -69
- package/dist/index.browser-BkcOQGhw.js.map +1 -0
- package/dist/{index.node-DV1OXCp0.js → index.node-C3rI1upv.js} +8 -6
- package/dist/index.node-C3rI1upv.js.map +1 -0
- package/dist/main.js +19 -22
- package/dist/main.js.map +1 -1
- package/dist/{minimumUpgradeVersion-BDhHvHtr.js → minimumUpgradeVersion-Dg2MguFf.js} +38 -9
- package/dist/minimumUpgradeVersion-Dg2MguFf.js.map +1 -0
- package/dist/{modelPropertyRecordSchemas-se4QMXSi.js → modelPropertyRecordSchemas-CEac3FB8.js} +8 -5
- package/dist/{modelPropertyRecordSchemas-se4QMXSi.js.map → modelPropertyRecordSchemas-CEac3FB8.js.map} +1 -1
- package/dist/node.js +14 -11
- package/dist/node.js.map +1 -1
- package/dist/platform.browser.js +6 -7
- package/dist/platform.browser.js.map +1 -1
- package/dist/platform.node.js +5 -5
- package/dist/{property-Broz_CWj.js → property-4jSqHieb.js} +11 -8
- package/dist/property-4jSqHieb.js.map +1 -0
- package/dist/{errors-DKxFSvb3.js → publishErrors-DZXDX3B6.js} +1688 -195
- package/dist/publishErrors-DZXDX3B6.js.map +1 -0
- package/dist/{readHtmlBodyForStorageSeed-FvwYPn4m.js → readHtmlBodyForStorageSeed-Cva1DPzE.js} +9 -9
- package/dist/{readHtmlBodyForStorageSeed-FvwYPn4m.js.map → readHtmlBodyForStorageSeed-Cva1DPzE.js.map} +1 -1
- package/dist/seedSchema/index.ts +0 -1
- package/dist/src/ItemProperty/ItemProperty.d.ts +1 -0
- package/dist/src/ItemProperty/ItemProperty.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/db/minimumUpgradeVersion.d.ts +20 -5
- package/dist/src/db/minimumUpgradeVersion.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/index.d.ts +4 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/node/db/Db.d.ts.map +1 -1
- package/dist/src/node/index.d.ts +9 -1
- package/dist/src/node/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-Dj_8VqM7.js → syncDbWithEas-V_9gIyHp.js} +21 -20
- package/dist/syncDbWithEas-V_9gIyHp.js.map +1 -0
- package/dist/{tolerantWebAccess-CNfvmR3m.js → tolerantWebAccess-D6hFxREe.js} +7 -8
- package/dist/{tolerantWebAccess-CNfvmR3m.js.map → tolerantWebAccess-D6hFxREe.js.map} +1 -1
- package/dist/{updateSeedRevokedAt-D-3SLW-D.js → updateSeedRevokedAt-CsAoUEEt.js} +4 -4
- package/dist/{updateSeedRevokedAt-D-3SLW-D.js.map → updateSeedRevokedAt-CsAoUEEt.js.map} +1 -1
- package/package.json +7 -7
- package/dist/BaseFileManager-DLou1TZS.js.map +0 -1
- package/dist/Item-Ld1iPicQ.js.map +0 -1
- package/dist/ModelProperty-C8gTx4pO.js.map +0 -1
- package/dist/arweaveContentTags-Bh4wCTal.js +0 -51
- package/dist/arweaveContentTags-Bh4wCTal.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-DKxFSvb3.js.map +0 -1
- package/dist/getPublishPayload-CLrAcZwr.js.map +0 -1
- package/dist/getPublishUploads-DFcgc27c.js +0 -784
- package/dist/getPublishUploads-DFcgc27c.js.map +0 -1
- package/dist/index-9udoYObJ.js.map +0 -1
- package/dist/index-BjWk0x4-.js.map +0 -1
- package/dist/index-Cdm7Q7el.js.map +0 -1
- package/dist/index-CghAYA-z.js.map +0 -1
- package/dist/index.browser-D9oC-KKx.js.map +0 -1
- package/dist/index.node-DV1OXCp0.js.map +0 -1
- package/dist/minimumUpgradeVersion-BDhHvHtr.js.map +0 -1
- package/dist/property-Broz_CWj.js.map +0 -1
- package/dist/publishGraph-DnWoWZRP.js +0 -311
- package/dist/publishGraph-DnWoWZRP.js.map +0 -1
- package/dist/seedSchema/PropertyUidSchema.ts +0 -16
- package/dist/src/seedSchema/PropertyUidSchema.d.ts +0 -64
- package/dist/src/seedSchema/PropertyUidSchema.d.ts.map +0 -1
- package/dist/syncDbWithEas-Dj_8VqM7.js.map +0 -1
- package/dist/tabEvents-BnBzIdEi.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 { 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';
|
|
6
6
|
import 'pluralize';
|
|
7
|
-
import { localNamesInBestMatchTier, configureEasReadChain, matchEasModelSchemas, whenEasReadChainSettled, getEasReadChainId, DEFAULT_EAS_CHAIN_ID, isEasReadChainConfigured, easName, isValidEasAttestationUid } from '@seedprotocol/eas';
|
|
8
|
-
import { k as generateId, l as isInternalSchema, a as TProperty,
|
|
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('./Item-
|
|
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('./Item-
|
|
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('./Item-
|
|
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('./Item-
|
|
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('./Item-
|
|
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
|
/**
|
|
@@ -2584,7 +2586,7 @@ function toRunSyncArg(options) {
|
|
|
2584
2586
|
return undefined;
|
|
2585
2587
|
}
|
|
2586
2588
|
const runEasSync = fromPromise(async ({ input, }) => {
|
|
2587
|
-
const { runSyncFromEas } = await import('./syncDbWithEas-
|
|
2589
|
+
const { runSyncFromEas } = await import('./syncDbWithEas-V_9gIyHp.js');
|
|
2588
2590
|
// Sync checks for rows and then inserts the rest (no unique index on uids), so syncs in
|
|
2589
2591
|
// different tabs take turns. A sync can run for minutes, so wait without a timeout.
|
|
2590
2592
|
await withDbTabLock('eas-sync', async () => {
|
|
@@ -2900,14 +2902,14 @@ function applySeedConfigDefaults(config) {
|
|
|
2900
2902
|
return { ...config, filesDir, endpoints: next };
|
|
2901
2903
|
}
|
|
2902
2904
|
|
|
2903
|
-
const logger$
|
|
2905
|
+
const logger$c = debug('seedSdk:ClientManager:initialize');
|
|
2904
2906
|
/**
|
|
2905
2907
|
* The first init step: configures the platform services, EAS reads and the Arweave gateway from
|
|
2906
2908
|
* the init options, starts tab coordination and the item event handlers, and returns the context
|
|
2907
2909
|
* the following steps read.
|
|
2908
2910
|
*/
|
|
2909
2911
|
async function platformClassesInit({ event }) {
|
|
2910
|
-
logger$
|
|
2912
|
+
logger$c('initialize from ClientManager');
|
|
2911
2913
|
if (!event || !('options' in event)) {
|
|
2912
2914
|
throw new Error('Initialize event must include options');
|
|
2913
2915
|
}
|
|
@@ -2917,7 +2919,7 @@ async function platformClassesInit({ event }) {
|
|
|
2917
2919
|
if (!config) {
|
|
2918
2920
|
throw new Error('client.init requires a config');
|
|
2919
2921
|
}
|
|
2920
|
-
const { configurePlatform } = await import('./configurePlatform-
|
|
2922
|
+
const { configurePlatform } = await import('./configurePlatform-BH1YOZpg.js');
|
|
2921
2923
|
if (isBrowser()) {
|
|
2922
2924
|
const { createPlatformServices } = await import('./platform.browser.js');
|
|
2923
2925
|
configurePlatform(createPlatformServices());
|
|
@@ -2993,7 +2995,7 @@ async function platformClassesInit({ event }) {
|
|
|
2993
2995
|
};
|
|
2994
2996
|
}
|
|
2995
2997
|
|
|
2996
|
-
const logger$
|
|
2998
|
+
const logger$b = debug('seedSdk:client:writeToDb');
|
|
2997
2999
|
const saveAppState = fromCallback(({ sendBack, input: { key, value } }) => {
|
|
2998
3000
|
const _saveAppState = async () => {
|
|
2999
3001
|
try {
|
|
@@ -3012,10 +3014,10 @@ const saveAppState = fromCallback(({ sendBack, input: { key, value } }) => {
|
|
|
3012
3014
|
value: JSON.stringify(value),
|
|
3013
3015
|
},
|
|
3014
3016
|
});
|
|
3015
|
-
logger$
|
|
3017
|
+
logger$b('result', result);
|
|
3016
3018
|
}
|
|
3017
3019
|
catch (error) {
|
|
3018
|
-
logger$
|
|
3020
|
+
logger$b('[client/actors] [saveAppState] Error saving app state:', error);
|
|
3019
3021
|
throw error;
|
|
3020
3022
|
}
|
|
3021
3023
|
};
|
|
@@ -3028,7 +3030,7 @@ const saveAppState = fromCallback(({ sendBack, input: { key, value } }) => {
|
|
|
3028
3030
|
});
|
|
3029
3031
|
})
|
|
3030
3032
|
.catch((error) => {
|
|
3031
|
-
logger$
|
|
3033
|
+
logger$b('[client/actors] [saveAppState] Error in promise chain:', error);
|
|
3032
3034
|
sendBack({
|
|
3033
3035
|
type: 'saveAppStateError',
|
|
3034
3036
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
@@ -3046,7 +3048,7 @@ async function fileSystemInit({ filesDir }) {
|
|
|
3046
3048
|
await BaseFileManager.initializeFileSystem(filesDir);
|
|
3047
3049
|
}
|
|
3048
3050
|
|
|
3049
|
-
const logger$
|
|
3051
|
+
const logger$a = debug('seedSdk:client:actors:dbInit');
|
|
3050
3052
|
/** Init step: opens and migrates the app database (BaseDb.prepareDb). */
|
|
3051
3053
|
async function dbInit({ filesDir, dbConfig }) {
|
|
3052
3054
|
if (!filesDir) {
|
|
@@ -3056,7 +3058,7 @@ async function dbInit({ filesDir, dbConfig }) {
|
|
|
3056
3058
|
if (!BaseDb.getAppDb()) {
|
|
3057
3059
|
throw new Error('Database not available after preparation');
|
|
3058
3060
|
}
|
|
3059
|
-
logger$
|
|
3061
|
+
logger$a('Database prepared and ready');
|
|
3060
3062
|
}
|
|
3061
3063
|
|
|
3062
3064
|
/**
|
|
@@ -3114,7 +3116,7 @@ const localModelSchemaLookups = async (modelNames) => {
|
|
|
3114
3116
|
return names.filter((name) => name !== 'Seed').map((name) => ({ name, level: levels.model(name) }));
|
|
3115
3117
|
};
|
|
3116
3118
|
|
|
3117
|
-
const logger$
|
|
3119
|
+
const logger$9 = debug('seedSdk:client:actors:addModelsToDb');
|
|
3118
3120
|
/**
|
|
3119
3121
|
* Returns every `models` row named in `modelNames`, inserting a null-schemaFileId stub for each name
|
|
3120
3122
|
* with no row yet (the schema import adopts stubs via findOrCreateModelRow). Check-then-insert:
|
|
@@ -3163,7 +3165,7 @@ async function addModelsToDb(context) {
|
|
|
3163
3165
|
for (const modelName of modelNames) {
|
|
3164
3166
|
const foundModel = rowFor(modelName);
|
|
3165
3167
|
if (!foundModel) {
|
|
3166
|
-
logger$
|
|
3168
|
+
logger$9('[client/actors] [addModelsToDb] Warning: Could not find or create model:', modelName);
|
|
3167
3169
|
hasModelsInDb = false;
|
|
3168
3170
|
continue;
|
|
3169
3171
|
}
|
|
@@ -3171,7 +3173,7 @@ async function addModelsToDb(context) {
|
|
|
3171
3173
|
}
|
|
3172
3174
|
// If we have no models to process, still send success (empty models is valid)
|
|
3173
3175
|
if (modelNamesWithRows.size === 0) {
|
|
3174
|
-
logger$
|
|
3176
|
+
logger$9('[client/actors] [addModelsToDb] No models to process, but continuing');
|
|
3175
3177
|
return;
|
|
3176
3178
|
}
|
|
3177
3179
|
// Fetch schemas from EAS in background - do not block init. EAS can be slow (10s+);
|
|
@@ -3183,7 +3185,7 @@ async function addModelsToDb(context) {
|
|
|
3183
3185
|
timeout = setTimeout(() => reject(new Error('EAS query timeout after 10 seconds')), 10000);
|
|
3184
3186
|
});
|
|
3185
3187
|
// The lookup and the model_uids rule sync uses too (each name at its nameMatch level).
|
|
3186
|
-
const { getModelSchemasFromEas } = await import('./eas-
|
|
3188
|
+
const { getModelSchemasFromEas } = await import('./eas-MXEdKKVc.js');
|
|
3187
3189
|
const lookups = await localModelSchemaLookups([...modelNamesWithRows]);
|
|
3188
3190
|
// Cleared, so a finished lookup doesn't keep a Node process alive for 10 s.
|
|
3189
3191
|
const schemas = await Promise.race([getModelSchemasFromEas(lookups), timeoutPromise]).finally(() => clearTimeout(timeout));
|
|
@@ -3195,11 +3197,11 @@ async function addModelsToDb(context) {
|
|
|
3195
3197
|
requestEasSyncFromModelsInit();
|
|
3196
3198
|
}
|
|
3197
3199
|
else {
|
|
3198
|
-
logger$
|
|
3200
|
+
logger$9('[client/actors] [addModelsToDb] No schemas found from EAS, but continuing');
|
|
3199
3201
|
}
|
|
3200
3202
|
}
|
|
3201
3203
|
catch (error) {
|
|
3202
|
-
logger$
|
|
3204
|
+
logger$9('[client/actors] [addModelsToDb] Error fetching schemas (background):', error.message);
|
|
3203
3205
|
// Probably offline: the startup sync waits for this lookup, so try again.
|
|
3204
3206
|
modelUidsRetry.schedule();
|
|
3205
3207
|
}
|
|
@@ -3213,16 +3215,16 @@ async function addModelsToDb(context) {
|
|
|
3213
3215
|
await _addModelsToDb();
|
|
3214
3216
|
// Model.findByModelType reads the nameMatch levels synchronously: have them before init is ready.
|
|
3215
3217
|
await loadNameMatchLevels().catch((error) => {
|
|
3216
|
-
logger$
|
|
3218
|
+
logger$9('[client/actors] [addModelsToDb] Could not load nameMatch levels:', error);
|
|
3217
3219
|
});
|
|
3218
3220
|
}
|
|
3219
3221
|
catch (error) {
|
|
3220
|
-
logger$
|
|
3222
|
+
logger$9('[client/actors] [addModelsToDb] Error:', error);
|
|
3221
3223
|
throw error;
|
|
3222
3224
|
}
|
|
3223
3225
|
}
|
|
3224
3226
|
|
|
3225
|
-
const logger$
|
|
3227
|
+
const logger$8 = debug('seedSdk:helpers:localDbChain');
|
|
3226
3228
|
/** appState key recording which EAS chain the local database holds attestations from. */
|
|
3227
3229
|
const EAS_CHAIN_ID_APP_STATE_KEY = 'easChainId';
|
|
3228
3230
|
/**
|
|
@@ -3292,7 +3294,7 @@ async function loadLocalDbChain() {
|
|
|
3292
3294
|
const localChainId = await resolveLocalDbChainId(appDb);
|
|
3293
3295
|
if (localChainId !== undefined) {
|
|
3294
3296
|
configureEasReadChain('localDb', { chainId: localChainId });
|
|
3295
|
-
logger$
|
|
3297
|
+
logger$8('local DB holds chain %d', localChainId);
|
|
3296
3298
|
return;
|
|
3297
3299
|
}
|
|
3298
3300
|
if (isEasReadChainConfigured()) {
|
|
@@ -3341,7 +3343,7 @@ function resetAddressesPersistedAnnouncement() {
|
|
|
3341
3343
|
lastAnnounced = undefined;
|
|
3342
3344
|
}
|
|
3343
3345
|
|
|
3344
|
-
const logger$
|
|
3346
|
+
const logger$7 = debug('seedSdk:client:actors:saveConfig');
|
|
3345
3347
|
let warnedKeptStoredAddresses = false;
|
|
3346
3348
|
/**
|
|
3347
3349
|
* Saves the addresses passed to init.
|
|
@@ -3386,11 +3388,11 @@ async function persistInitAddresses(appDb, { addresses, addressesProvided, owned
|
|
|
3386
3388
|
* checks the database's chain against the configured one.
|
|
3387
3389
|
*/
|
|
3388
3390
|
async function saveConfig(context) {
|
|
3389
|
-
logger$
|
|
3391
|
+
logger$7('saveConfig starting');
|
|
3390
3392
|
const { endpoints, addresses, addressesProvided, ownedAddresses, watchedAddresses, arweaveDomain, uploadApiBaseUrl, gatewayTransport, gatewayHyperKey, gatewaySidecarHost, gatewaySidecarPort, gatewayProxyBaseUrl, } = context;
|
|
3391
3393
|
// Validate endpoints - required for proper initialization
|
|
3392
3394
|
if (!endpoints || !endpoints.filePaths || !endpoints.files) {
|
|
3393
|
-
logger$
|
|
3395
|
+
logger$7('[internal/actors] [saveConfig] Invalid endpoints:', { endpoints });
|
|
3394
3396
|
throw new Error('saveConfig called with invalid endpoints: endpoints must include both filePaths and files');
|
|
3395
3397
|
}
|
|
3396
3398
|
const _saveConfig = async () => {
|
|
@@ -3449,16 +3451,16 @@ async function saveConfig(context) {
|
|
|
3449
3451
|
await loadLocalDbChain();
|
|
3450
3452
|
}
|
|
3451
3453
|
catch (error) {
|
|
3452
|
-
logger$
|
|
3454
|
+
logger$7('[internal/actors] [saveConfig] Error saving config:', error);
|
|
3453
3455
|
throw error;
|
|
3454
3456
|
}
|
|
3455
3457
|
};
|
|
3456
3458
|
// Init writes run one tab at a time (docs/MULTI_TAB.md).
|
|
3457
3459
|
await withSeedDbLock('init', context.filesDir, _saveConfig);
|
|
3458
|
-
logger$
|
|
3460
|
+
logger$7('[internal/actors] [saveConfig] saveConfig success');
|
|
3459
3461
|
}
|
|
3460
3462
|
|
|
3461
|
-
const logger$
|
|
3463
|
+
const logger$6 = debug('seedSdk:helpers:schema');
|
|
3462
3464
|
/** Filesystem-safe schema name: other characters become `_`, runs of `_` collapse, ends trimmed. */
|
|
3463
3465
|
const sanitizeSchemaName = (name) => name
|
|
3464
3466
|
.replace(/[^a-zA-Z0-9\s_-]/g, '_')
|
|
@@ -3512,7 +3514,7 @@ async function readSchemaFile(filePath) {
|
|
|
3512
3514
|
return schema;
|
|
3513
3515
|
}
|
|
3514
3516
|
catch (error) {
|
|
3515
|
-
logger$
|
|
3517
|
+
logger$6(`Could not read schema file ${filePath}:`, error);
|
|
3516
3518
|
return undefined;
|
|
3517
3519
|
}
|
|
3518
3520
|
}
|
|
@@ -3526,7 +3528,7 @@ async function listCompleteSchemaFiles() {
|
|
|
3526
3528
|
const workingDir = BaseFileManager.getWorkingDir();
|
|
3527
3529
|
try {
|
|
3528
3530
|
if (!(await BaseFileManager.pathExists(workingDir))) {
|
|
3529
|
-
logger$
|
|
3531
|
+
logger$6(`Working directory does not exist yet: ${workingDir}, returning empty schema list`);
|
|
3530
3532
|
return [];
|
|
3531
3533
|
}
|
|
3532
3534
|
const files = await fs.promises.readdir(workingDir);
|
|
@@ -3597,7 +3599,7 @@ async function loadAllSchemasFromDb() {
|
|
|
3597
3599
|
});
|
|
3598
3600
|
}
|
|
3599
3601
|
catch (error) {
|
|
3600
|
-
logger$
|
|
3602
|
+
logger$6(`Error resolving schema row ${row.name}:`, error);
|
|
3601
3603
|
}
|
|
3602
3604
|
}
|
|
3603
3605
|
return result;
|
|
@@ -3629,7 +3631,7 @@ var schema = /*#__PURE__*/Object.freeze({
|
|
|
3629
3631
|
readSchemaFile: readSchemaFile
|
|
3630
3632
|
});
|
|
3631
3633
|
|
|
3632
|
-
function toBytes(data) {
|
|
3634
|
+
function toBytes$1(data) {
|
|
3633
3635
|
if (typeof data === 'string')
|
|
3634
3636
|
return utf8ToBytes(data);
|
|
3635
3637
|
if (data instanceof Uint8Array)
|
|
@@ -3637,7 +3639,7 @@ function toBytes(data) {
|
|
|
3637
3639
|
return new Uint8Array(data);
|
|
3638
3640
|
}
|
|
3639
3641
|
const getContentHash = async (data) => {
|
|
3640
|
-
return bytesToHex(sha3_256(toBytes(data)));
|
|
3642
|
+
return bytesToHex(sha3_256(toBytes$1(data)));
|
|
3641
3643
|
};
|
|
3642
3644
|
/**
|
|
3643
3645
|
* Generate a deterministic ID from a seed string.
|
|
@@ -3661,7 +3663,7 @@ const getDeterministicId = (seed) => {
|
|
|
3661
3663
|
* Schema identity is the file id. Missing ids are assigned once, at first apply, and written into
|
|
3662
3664
|
* the stored file.
|
|
3663
3665
|
*/
|
|
3664
|
-
const logger$
|
|
3666
|
+
const logger$5 = debug('seedSdk:helpers:schemaApply');
|
|
3665
3667
|
const SCHEMA_FILE_FORMAT = 'https://seedprotocol.org/schemas/data-model/v1';
|
|
3666
3668
|
/**
|
|
3667
3669
|
* A complete schema file from any accepted shape: models and properties as objects, `version`
|
|
@@ -3881,8 +3883,8 @@ async function applySchemaFile(source, options = {}) {
|
|
|
3881
3883
|
if (!schemaRecord) {
|
|
3882
3884
|
warnOnEasNameCollisions(file);
|
|
3883
3885
|
warnOnInvalidNameMatch(file);
|
|
3884
|
-
const _mod_14 = await import('./Item-
|
|
3885
|
-
const _ns_14 = _mod_14.
|
|
3886
|
+
const _mod_14 = await import('./Item-BU9wIT7l.js');
|
|
3887
|
+
const _ns_14 = _mod_14.bS;
|
|
3886
3888
|
const { addModelsToDb, broadcastSchemasInvalidate } = _ns_14;
|
|
3887
3889
|
const modelFileIds = new Map(Object.entries(file.models).map(([name, model]) => [name, model.id]));
|
|
3888
3890
|
const propertyDefinitions = schemaFilePropertyDefinitions(file);
|
|
@@ -3920,10 +3922,10 @@ async function applySchemaFile(source, options = {}) {
|
|
|
3920
3922
|
broadcastSchemasInvalidate();
|
|
3921
3923
|
// A cached instance is otherwise never re-read from its row (createById returns it as is)
|
|
3922
3924
|
await refreshCachedProperties(updatedPropertyFileIds);
|
|
3923
|
-
logger$
|
|
3925
|
+
logger$5(`Applied schema ${file.metadata.name} v${file.version} (${file.id})`);
|
|
3924
3926
|
}
|
|
3925
3927
|
else {
|
|
3926
|
-
logger$
|
|
3928
|
+
logger$5(`Schema ${file.metadata.name} v${file.version} (${file.id}) already applied`);
|
|
3927
3929
|
}
|
|
3928
3930
|
const models = await createModelsForSchemaFile(file);
|
|
3929
3931
|
// Callers use the models next (an item create reads their properties): wait until each has them.
|
|
@@ -3954,7 +3956,7 @@ async function storeSchemaFile(file, onChangedContent) {
|
|
|
3954
3956
|
}
|
|
3955
3957
|
if (onChangedContent === 'bump') {
|
|
3956
3958
|
file.version = Math.max(...versions.map((info) => info.version)) + 1;
|
|
3957
|
-
logger$
|
|
3959
|
+
logger$5(`Schema ${file.metadata.name} content changed, storing it as v${file.version}`);
|
|
3958
3960
|
return writeSchemaFile(file);
|
|
3959
3961
|
}
|
|
3960
3962
|
console.warn(`[seed] schema ${file.metadata.name} v${file.version}: content changed but id ` +
|
|
@@ -3976,7 +3978,7 @@ async function unlinkModelsFromSchema(db, schemaId, modelNames) {
|
|
|
3976
3978
|
if (links.length === 0)
|
|
3977
3979
|
return;
|
|
3978
3980
|
await db.delete(modelSchemas).where(inArray(modelSchemas.id, links.map((link) => link.id)));
|
|
3979
|
-
logger$
|
|
3981
|
+
logger$5(`Unlinked models dropped from the schema (schema id ${schemaId}): ${modelNames.join(', ')}`);
|
|
3980
3982
|
}
|
|
3981
3983
|
/**
|
|
3982
3984
|
* A schema row's definition. A draft row's is its `schema_data`; a published row's is its
|
|
@@ -3995,7 +3997,7 @@ async function resolveSchemaRow(row, db = BaseDb.getAppDb()) {
|
|
|
3995
3997
|
return { ...data, models: normalizeSchemaModels(data.models) };
|
|
3996
3998
|
}
|
|
3997
3999
|
catch (error) {
|
|
3998
|
-
logger$
|
|
4000
|
+
logger$5(`Could not parse schema_data of ${row.name}:`, error);
|
|
3999
4001
|
return undefined;
|
|
4000
4002
|
}
|
|
4001
4003
|
};
|
|
@@ -4033,8 +4035,8 @@ async function resolveSchemaRow(row, db = BaseDb.getAppDb()) {
|
|
|
4033
4035
|
metadata: { ...schema.metadata, name: row.name },
|
|
4034
4036
|
};
|
|
4035
4037
|
if (row.id && db) {
|
|
4036
|
-
const _mod_15 = await import('./Item-
|
|
4037
|
-
const _ns_15 = _mod_15.
|
|
4038
|
+
const _mod_15 = await import('./Item-BU9wIT7l.js');
|
|
4039
|
+
const _ns_15 = _mod_15.bS;
|
|
4038
4040
|
const { loadModelsFromDbForSchema, mergeDbModelsIntoSchemaModels } = _ns_15;
|
|
4039
4041
|
const dbModels = await loadModelsFromDbForSchema(row.id, db);
|
|
4040
4042
|
if (Object.keys(dbModels).length > 0) {
|
|
@@ -4101,7 +4103,7 @@ async function writeNextSchemaVersion(schemaNameOrId, patch, change) {
|
|
|
4101
4103
|
],
|
|
4102
4104
|
};
|
|
4103
4105
|
const filePath = await writeSchemaFile(schemaFile);
|
|
4104
|
-
logger$
|
|
4106
|
+
logger$5(`Wrote schema ${schemaFile.metadata.name} v${version} at ${filePath}`);
|
|
4105
4107
|
if (change.apply !== false) {
|
|
4106
4108
|
await applySchemaFile(schemaFile, { store: false, modelRenames: change.modelRenames });
|
|
4107
4109
|
}
|
|
@@ -4404,7 +4406,7 @@ var SEEDPROTOCOL_Seed_Protocol_v1 = /*#__PURE__*/Object.freeze({
|
|
|
4404
4406
|
version: version
|
|
4405
4407
|
});
|
|
4406
4408
|
|
|
4407
|
-
const logger$
|
|
4409
|
+
const logger$4 = debug('seedSdk:client:actors:processSchemaFiles');
|
|
4408
4410
|
let processSchemaFilesChain = Promise.resolve();
|
|
4409
4411
|
function resolveSchemaFilePath(schemaFile) {
|
|
4410
4412
|
const path = BaseFileManager.getPathModule();
|
|
@@ -4436,7 +4438,7 @@ async function processSchemaFilesOnce(context) {
|
|
|
4436
4438
|
appliedNames.add(applied.schemaFile.metadata.name);
|
|
4437
4439
|
}
|
|
4438
4440
|
catch (error) {
|
|
4439
|
-
logger$
|
|
4441
|
+
logger$4('Error loading internal seed-protocol schema:', error);
|
|
4440
4442
|
}
|
|
4441
4443
|
// The app's schema (canonical) takes precedence over schemaFile
|
|
4442
4444
|
if (context.schema) {
|
|
@@ -4475,14 +4477,14 @@ async function processSchemaFilesOnce(context) {
|
|
|
4475
4477
|
loaded = await loadAllSchemasFromDb();
|
|
4476
4478
|
}
|
|
4477
4479
|
catch (error) {
|
|
4478
|
-
logger$
|
|
4480
|
+
logger$4('Error loading schemas from database (continuing anyway):', error);
|
|
4479
4481
|
}
|
|
4480
4482
|
for (const { schema } of loaded) {
|
|
4481
4483
|
try {
|
|
4482
4484
|
Object.assign(allModels, await createModelsForSchemaFile(schema));
|
|
4483
4485
|
}
|
|
4484
4486
|
catch (error) {
|
|
4485
|
-
logger$
|
|
4487
|
+
logger$4(`Error creating models for schema ${schema.metadata?.name}:`, error);
|
|
4486
4488
|
}
|
|
4487
4489
|
}
|
|
4488
4490
|
return allModels;
|
|
@@ -4493,7 +4495,7 @@ async function processSchemaFilesOnce(context) {
|
|
|
4493
4495
|
* (docs/MULTI_TAB.md), and so do overlapping inits in this tab.
|
|
4494
4496
|
*/
|
|
4495
4497
|
function processSchemaFiles(context) {
|
|
4496
|
-
logger$
|
|
4498
|
+
logger$4('processSchemaFiles started');
|
|
4497
4499
|
const run = processSchemaFilesChain.then(() => withSeedDbLock('init', context.filesDir, () => processSchemaFilesOnce(context)));
|
|
4498
4500
|
// The chain only orders runs; a failed run must not fail the next init too.
|
|
4499
4501
|
processSchemaFilesChain = run.then(() => { }, () => { });
|
|
@@ -4513,7 +4515,7 @@ async function applyPendingSchemaFiles(appliedNames) {
|
|
|
4513
4515
|
files = await listLatestSchemaFiles();
|
|
4514
4516
|
}
|
|
4515
4517
|
catch (error) {
|
|
4516
|
-
logger$
|
|
4518
|
+
logger$4('Error listing schema files (continuing anyway):', error);
|
|
4517
4519
|
return;
|
|
4518
4520
|
}
|
|
4519
4521
|
const rows = await db
|
|
@@ -4532,12 +4534,12 @@ async function applyPendingSchemaFiles(appliedNames) {
|
|
|
4532
4534
|
await applySchemaFile({ ...file, id: file.id ?? info.schemaFileId }, { store: false });
|
|
4533
4535
|
}
|
|
4534
4536
|
catch (error) {
|
|
4535
|
-
logger$
|
|
4537
|
+
logger$4(`Error applying schema file ${info.filePath}:`, error);
|
|
4536
4538
|
}
|
|
4537
4539
|
}
|
|
4538
4540
|
}
|
|
4539
4541
|
|
|
4540
|
-
const logger$
|
|
4542
|
+
const logger$3 = debug('seedSdk:db:canonicalizeModelTypes');
|
|
4541
4543
|
/**
|
|
4542
4544
|
* Model types in the local DB (`seeds.type`, `versions.seed_type`, `metadata.model_type`) are the
|
|
4543
4545
|
* EAS name of the model (ADR 0008): `html_page` for HTMLPage. Rows written before that hold other
|
|
@@ -4575,7 +4577,7 @@ async function canonicalizeModelTypes() {
|
|
|
4575
4577
|
await appDb.update(seeds).set({ type: next }).where(eq(seeds.type, type));
|
|
4576
4578
|
await appDb.update(versions).set({ seedType: next }).where(eq(versions.seedType, type));
|
|
4577
4579
|
await appDb.update(metadata$1).set({ modelType: next }).where(eq(metadata$1.modelType, type));
|
|
4578
|
-
logger$
|
|
4580
|
+
logger$3(`model type ${type} → ${next}`);
|
|
4579
4581
|
rewritten++;
|
|
4580
4582
|
}
|
|
4581
4583
|
return rewritten;
|
|
@@ -4588,7 +4590,7 @@ const runInBackground = () => {
|
|
|
4588
4590
|
.then(() => undefined)
|
|
4589
4591
|
.catch((error) => {
|
|
4590
4592
|
// Never fails init: readers still match old types by name, only exact type filters miss them.
|
|
4591
|
-
logger$
|
|
4593
|
+
logger$3('canonicalizing model types failed', error);
|
|
4592
4594
|
})
|
|
4593
4595
|
.finally(() => {
|
|
4594
4596
|
running = undefined;
|
|
@@ -4656,10 +4658,10 @@ db = BaseDb.getAppDb()) {
|
|
|
4656
4658
|
return normalizePublisher(resolvePublisherFromSeedRow(row));
|
|
4657
4659
|
}
|
|
4658
4660
|
|
|
4659
|
-
const logger$
|
|
4661
|
+
const logger$2 = debug('seedSdk:write:updateItemPropertyValue');
|
|
4660
4662
|
const updateItemPropertyValue = async ({ localId: localIdParam, propertyName, newValue, seedUid, seedLocalId, modelName, refSeedType, refResolvedValue, refResolvedDisplayValue, versionLocalId, versionUid, schemaUid, localStorageDir, dataType, refValueType, retract, }, options = {}) => {
|
|
4661
4663
|
if (!localIdParam && !seedLocalId) {
|
|
4662
|
-
logger$
|
|
4664
|
+
logger$2(`[db/write] [updateItemPropertyValue] no propertyLocalId or seedLocalId for property: ${propertyName}`);
|
|
4663
4665
|
return;
|
|
4664
4666
|
}
|
|
4665
4667
|
// Every write below binds parameters: store the value as given (no SQL escaping).
|
|
@@ -4727,7 +4729,7 @@ const updateItemPropertyValue = async ({ localId: localIdParam, propertyName, ne
|
|
|
4727
4729
|
(refSeedTypeFromDb ?? null) === (refSeedType ?? null) &&
|
|
4728
4730
|
(refResolvedValueFromDb ?? null) === (refResolvedValue ?? null) &&
|
|
4729
4731
|
!!retractFromDb === !!retractValue) {
|
|
4730
|
-
logger$
|
|
4732
|
+
logger$2(`[db/write] [updateItemPropertyValue] value is the same as most recent record for property: ${propertyNameFromDb}`);
|
|
4731
4733
|
return localId == null ? undefined : { localId, uid: uid ?? null, schemaUid: schemaUidFromDb ?? '' };
|
|
4732
4734
|
}
|
|
4733
4735
|
// This means we already have a local-only record so we should just update that one
|
|
@@ -4913,7 +4915,7 @@ const createMetadata = async (metadataValues, propertyRecordSchema, options) =>
|
|
|
4913
4915
|
propertyRecordSchema?.validation &&
|
|
4914
4916
|
metadataValues.propertyValue != null &&
|
|
4915
4917
|
metadataValues.propertyValue !== '') {
|
|
4916
|
-
const { SchemaValidationService } = await import('./SchemaValidationService-
|
|
4918
|
+
const { SchemaValidationService } = await import('./SchemaValidationService-Dc-lpFQX.js');
|
|
4917
4919
|
const validationService = new SchemaValidationService();
|
|
4918
4920
|
const validationResult = validationService.validatePropertyValue(metadataValues.propertyValue, normalizeDataType(propertyRecordSchema.dataType), propertyRecordSchema.validation, propertyRecordSchema.refValueType
|
|
4919
4921
|
? normalizeDataType(String(propertyRecordSchema.refValueType))
|
|
@@ -5100,8 +5102,8 @@ async function applyStorageValue(seedLocalId, propertyName, value, db, prepared)
|
|
|
5100
5102
|
const row = prepared
|
|
5101
5103
|
? { ...stored, refResolvedValue: prepared.fileName, schemaUid: prepared.schemaUid, refModelUid: prepared.refModelUid }
|
|
5102
5104
|
: stored;
|
|
5103
|
-
const _mod_16 = await import('./Item-
|
|
5104
|
-
const _ns_16 = _mod_16.
|
|
5105
|
+
const _mod_16 = await import('./Item-BU9wIT7l.js');
|
|
5106
|
+
const _ns_16 = _mod_16.bT;
|
|
5105
5107
|
const { currentPropertyRow } = _ns_16;
|
|
5106
5108
|
const existing = await currentPropertyRow({ seedLocalId }, {
|
|
5107
5109
|
propertyName,
|
|
@@ -5414,8 +5416,8 @@ function startItemOutbox() {
|
|
|
5414
5416
|
return {};
|
|
5415
5417
|
// Cached, shared with item creation, so a save doesn't wait on the EAS indexer each time.
|
|
5416
5418
|
// Not required for the row; publish resolves the schema again.
|
|
5417
|
-
const _mod_17 = await import('./Item-
|
|
5418
|
-
const _ns_17 = _mod_17.
|
|
5419
|
+
const _mod_17 = await import('./Item-BU9wIT7l.js');
|
|
5420
|
+
const _ns_17 = _mod_17.bU;
|
|
5419
5421
|
const { lookupPropertySchemaUid } = _ns_17;
|
|
5420
5422
|
return {
|
|
5421
5423
|
schemaUid: await lookupPropertySchemaUid(op.propertyName, value.propertyRecordSchema),
|
|
@@ -6350,7 +6352,7 @@ async function claimPendingCreates(tx, publisher) {
|
|
|
6350
6352
|
return claimed;
|
|
6351
6353
|
}
|
|
6352
6354
|
|
|
6353
|
-
const logger = debug('seedSdk:client');
|
|
6355
|
+
const logger$1 = debug('seedSdk:client');
|
|
6354
6356
|
const createClientManager = () => {
|
|
6355
6357
|
const actor = createActor(clientManagerMachine, {
|
|
6356
6358
|
input: {
|
|
@@ -6361,8 +6363,8 @@ const createClientManager = () => {
|
|
|
6361
6363
|
}
|
|
6362
6364
|
});
|
|
6363
6365
|
const actorSubscription = actor.subscribe((snapshot) => {
|
|
6364
|
-
logger('ClientManager snapshot.value:', snapshot.value);
|
|
6365
|
-
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);
|
|
6366
6368
|
});
|
|
6367
6369
|
actor.start();
|
|
6368
6370
|
return { actor, actorSubscription };
|
|
@@ -6378,7 +6380,7 @@ const notifyServiceListeners = () => {
|
|
|
6378
6380
|
listener(clientManager);
|
|
6379
6381
|
}
|
|
6380
6382
|
catch (error) {
|
|
6381
|
-
logger('subscribeService listener threw:', error);
|
|
6383
|
+
logger$1('subscribeService listener threw:', error);
|
|
6382
6384
|
}
|
|
6383
6385
|
}
|
|
6384
6386
|
};
|
|
@@ -6448,14 +6450,14 @@ const coalescedInit = createCoalescedAsync(async (options) => {
|
|
|
6448
6450
|
const isStopped = snapshot.status === 'stopped';
|
|
6449
6451
|
if (isStopped) {
|
|
6450
6452
|
// stop() (or a failed test) stopped it: start over with a new actor and a clean context.
|
|
6451
|
-
logger('Client manager was stopped, creating a new one');
|
|
6453
|
+
logger$1('Client manager was stopped, creating a new one');
|
|
6452
6454
|
({ actor: clientManager, actorSubscription: subscription } = createClientManager());
|
|
6453
6455
|
notifyServiceListeners();
|
|
6454
6456
|
}
|
|
6455
6457
|
// Clear any stale context from previous failed initializations
|
|
6456
6458
|
// This ensures we start with a clean slate before the new init updates it
|
|
6457
6459
|
if (!isStopped && currentState !== 'uninitialized') {
|
|
6458
|
-
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...`);
|
|
6459
6461
|
clientManager.send({
|
|
6460
6462
|
type: 'updateContext',
|
|
6461
6463
|
context: {
|
|
@@ -6556,14 +6558,14 @@ const clientInstance = {
|
|
|
6556
6558
|
},
|
|
6557
6559
|
setAddresses: async (addresses) => {
|
|
6558
6560
|
ensureInitialized();
|
|
6559
|
-
logger('setAddresses', addresses);
|
|
6561
|
+
logger$1('setAddresses', addresses);
|
|
6560
6562
|
clientManager.send({ type: 'setAddresses', addresses });
|
|
6561
6563
|
await waitFor(clientManager, (snapshot) => !snapshot.context.isSaving, { timeout: 10000 });
|
|
6562
6564
|
const saveError = clientManager.getSnapshot().context.saveError;
|
|
6563
6565
|
if (saveError) {
|
|
6564
6566
|
throw saveError instanceof Error ? saveError : new Error(String(saveError));
|
|
6565
6567
|
}
|
|
6566
|
-
logger('setAddresses success', addresses);
|
|
6568
|
+
logger$1('setAddresses success', addresses);
|
|
6567
6569
|
},
|
|
6568
6570
|
getAddresses: async () => {
|
|
6569
6571
|
ensureInitialized();
|
|
@@ -6677,6 +6679,1457 @@ const getClient = () => {
|
|
|
6677
6679
|
return clientInstance;
|
|
6678
6680
|
};
|
|
6679
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
|
+
|
|
6680
8133
|
/**
|
|
6681
8134
|
* Thrown by Item.create when values don't fit the model: unknown keys, wrong types, or values
|
|
6682
8135
|
* that break the property's validation rules. Nothing is left written: most values are checked
|
|
@@ -6726,5 +8179,45 @@ var errors = /*#__PURE__*/Object.freeze({
|
|
|
6726
8179
|
UnknownModelError: UnknownModelError
|
|
6727
8180
|
});
|
|
6728
8181
|
|
|
6729
|
-
|
|
6730
|
-
|
|
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
|