UNPKG

@composedb/devtools

Version:

Development tools for ComposeDB projects.

464 lines (463 loc) 18.3 kB
function _check_private_redeclaration(obj, privateCollection) { if (privateCollection.has(obj)) { throw new TypeError("Cannot initialize the same private elements twice on an object"); } } function _class_apply_descriptor_get(receiver, descriptor) { if (descriptor.get) { return descriptor.get.call(receiver); } return descriptor.value; } function _class_apply_descriptor_set(receiver, descriptor, value) { if (descriptor.set) { descriptor.set.call(receiver, value); } else { if (!descriptor.writable) { throw new TypeError("attempted to set read only private field"); } descriptor.value = value; } } function _class_extract_field_descriptor(receiver, privateMap, action) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to " + action + " private field on non-instance"); } return privateMap.get(receiver); } function _class_private_field_get(receiver, privateMap) { var descriptor = _class_extract_field_descriptor(receiver, privateMap, "get"); return _class_apply_descriptor_get(receiver, descriptor); } function _class_private_field_init(obj, privateMap, value) { _check_private_redeclaration(obj, privateMap); privateMap.set(obj, value); } function _class_private_field_set(receiver, privateMap, value) { var descriptor = _class_extract_field_descriptor(receiver, privateMap, "set"); _class_apply_descriptor_set(receiver, descriptor, value); return value; } function _define_property(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } import { Model } from '@ceramicnetwork/stream-model'; import { StreamID } from '@ceramicnetwork/streamid'; import { cloneDeep, merge } from 'lodash-es'; import createObjectHash from 'object-hash'; import { decodeSignedMap, encodeSignedMap } from './formats/json.js'; import { createRuntimeDefinition } from './formats/runtime.js'; import { createAbstractCompositeDefinition } from './schema/compiler.js'; import { createIntermediaryCompositeDefinition } from './schema/resolution.js'; import { assertAuthenticatedDID, assertSupportedReadModelController, isSignedCommitContainer } from './schema/validation.js'; const MODEL_GENESIS_OPTS = { anchor: true, publish: true }; function toStrictDefinition(definition) { const emptyViews = { account: {}, root: {}, models: {} }; const views = definition.views ? { ...emptyViews, ...definition.views } : emptyViews; const indices = definition.indices ? definition.indices : {}; return { aliases: {}, commonEmbeds: [], ...definition, views, indices }; } function isSupportedVersion(supported, check) { const [supportedMajor] = supported.split('.'); const [checkMajor] = check.split('.'); return supportedMajor === checkMajor; } function assertSupportedVersion(supported, check) { if (!isSupportedVersion(supported, check)) { throw new Error(`Unsupported Composite version ${check}, expected version ${supported}`); } } function assertModelsHaveCommits(models, commits) { for (const id of Object.keys(models)){ if (commits[id] == null) { throw new Error(`Missing commits for model ${id}`); } } } /** @internal */ export function setDefinitionAliases(definition, aliases, replace = false) { const existing = replace ? {} : definition.aliases; definition.aliases = { ...existing, ...aliases }; return definition; } /** @internal */ export function setDefinitionCommonEmbeds(definition, names, replace = false) { const existing = replace ? [] : definition.commonEmbeds; definition.commonEmbeds = Array.from(new Set([ ...existing, ...names ])); return definition; } /** @internal */ export function setDefinitionViews(definition, views, replace = false) { const existing = replace ? {} : definition.views; definition.views = merge(existing, views); return definition; } async function loadModelsFromCommits(ceramic, modelsCommits) { const definitions = await Promise.all(Object.values(modelsCommits).map(async (commits)=>{ const modelCommitsValues = commits; const [genesis, ...updates] = modelCommitsValues; const model = await ceramic.createStreamFromGenesis(Model.STREAM_TYPE_ID, genesis, MODEL_GENESIS_OPTS); await Promise.all(modelCommitsValues.filter(isSignedCommitContainer).map(async (commit)=>{ await assertSupportedReadModelController(model, commit); })); for (const commit of updates){ await ceramic.applyCommit(model.id, commit); } return model; })); return Object.keys(modelsCommits).reduce((acc, id, index)=>{ const model = definitions[index]; if (model == null) { throw new Error(`Missing Model for ID ${id}`); } const modelID = model.id.toString(); if (modelID !== id) { throw new Error(`Unexpected Model stream ID: expected ${id}, got ${modelID}`); } acc[modelID] = model.content; return acc; }, {}); } var _commits = /*#__PURE__*/ new WeakMap(), _definition = /*#__PURE__*/ new WeakMap(), _hash = /*#__PURE__*/ new WeakMap(); /** * The Composite class provides APIs for managing composites (sets of Model streams) through their * development lifecycle, including the creation of new Models, import and export of existing * composites encoded as JSON, and compilation to the runtime format used by the * {@linkcode client.ComposeClient ComposeClient class}. * * Composite instances are **immutable**, so methods affecting the contents of the internal * composite definition will **return new instances** of the Composite class. * * Composite class is exported by the {@linkcode devtools} module. * * ```sh * import { Composite } from '@composedb/devtools' * ``` */ export class Composite { /** * Create new model streams based on the provided `schema` and group them in a composite * wrapped in a Composite instance. */ static async create(params) { const { commonEmbeds, models: abstractModels } = createAbstractCompositeDefinition(params.schema); const { commits, views, ...definition } = await createIntermediaryCompositeDefinition(params.ceramic, abstractModels); const composite = new Composite({ commits, definition: { ...definition, version: Composite.VERSION, commonEmbeds, views: { models: views } } }); // By default, add models to the index if (params.index !== false) { await composite.startIndexingOn(params.ceramic); } return composite; } /** * Create a Composite instance by merging existing composites. */ static from(composites, options) { const [first, ...rest] = composites; if (first == null) { throw new Error('Missing composites to compose'); } const composite = first instanceof Composite ? first : new Composite(first); return composite.merge(rest, options); } /** * Create a Composite instance from a JSON-encoded `CompositeDefinition`. */ static async fromJSON(params) { const { models, ...definition } = params.definition; const commits = decodeSignedMap(models); const composite = new Composite({ commits, definition: { ...definition, models: await loadModelsFromCommits(params.ceramic, commits) } }); // Only add models to the index if explicitly requested if (params.index) { await composite.startIndexingOn(params.ceramic); } return composite; } /** * Create a Composite instance from a set of Model streams already present on a Ceramic node. */ static async fromModels(params) { const definition = { version: Composite.VERSION, models: {} }; const commits = {}; await Promise.all(params.models.map(async (id)=>{ const [model, streamCommits] = await Promise.all([ Model.load(params.ceramic, id), params.ceramic.loadStreamCommits(id) ]); definition.models[id] = model.content; commits[id] = streamCommits.map((c)=>c.value).filter(isSignedCommitContainer); for (const commit of commits[id]){ await assertSupportedReadModelController(model, commit); } })); const composite = new Composite({ commits, definition }); // Only add models to the index if explicitly requested if (params.index) { await composite.startIndexingOn(params.ceramic); } return composite; } /** * Stable hash of the internal definition, mostly used for comparisons. */ get hash() { if (_class_private_field_get(this, _hash) == null) { _class_private_field_set(this, _hash, createObjectHash(_class_private_field_get(this, _definition))); } return _class_private_field_get(this, _hash); } /** * StreamID of the Models used in the Composite. */ get modelIDs() { return Object.keys(_class_private_field_get(this, _definition).models); } /** * Get the StreamID of the given model `alias` if present in the Composite. */ getModelID(alias) { const found = Object.entries(_class_private_field_get(this, _definition).aliases).find(([_, modelAlias])=>modelAlias === alias); return found ? found[0] : null; } /** * Copy a given set of Models identified by their stream ID, name or alias into a new Composite. */ copy(models) { if (models.length === 0) { throw new Error('Missing models to copy'); } const { commits, definition } = this.toParams(); const def = toStrictDefinition(definition); const nameIDs = Object.entries(def.models).reduce((acc, [id, model])=>{ acc[model.name] = id; return acc; }, {}); const aliasIDs = Object.entries(def.aliases).reduce((acc, [id, alias])=>{ acc[alias] = id; return acc; }, {}); const nextCommits = {}; const nextModels = {}; const nextAliases = {}; const nextViews = { account: {}, root: {}, models: {} }; for (const model of models){ const id = aliasIDs[model] ?? nameIDs[model] ?? model; if (def.models[id] == null) { throw new Error(`Model not found: ${model}`); } nextCommits[id] = commits[id]; nextModels[id] = def.models[id]; if (def.aliases[id] != null) { nextAliases[id] = def.aliases[id]; } // TODO: check relations to other models, ensure there's no missing model in the subset // if (def.views.models[id] != null) { // nextViews.models[id] = def.views.models[id] // } // TODO: account and root views } return new Composite({ commits: nextCommits, definition: { version: def.version, commonEmbeds: def.commonEmbeds, models: nextModels, aliases: nextAliases, views: nextViews } }); } /** * Check if the composite is equal to the other one provided as input. */ equals(other) { const otherHash = other instanceof Composite ? other.hash : createObjectHash(toStrictDefinition(other.definition)); return this.hash === otherHash; } /** * Merge the composite with the other one(s) into a new Composite. */ merge(other, options = {}) { const nextParams = this.toParams(); const nextDefinition = toStrictDefinition(nextParams.definition); const collectedEmbeds = new Set(); for (const composite of Array.isArray(other) ? other : [ other ]){ const { commits, definition } = composite instanceof Composite ? composite.toParams() : composite; assertSupportedVersion(nextDefinition.version, definition.version); assertModelsHaveCommits(definition.models, commits); const def = toStrictDefinition(definition); // Shallow merges Object.assign(nextParams.commits, commits); Object.assign(nextDefinition.models, definition.models); Object.assign(nextDefinition.aliases, def.aliases); // Deep merge of views merge(nextDefinition.views, def.views); // Concatenation of indices for (const [id, indices] of Object.entries(def.indices)){ nextDefinition.indices[id] = (nextDefinition.indices[id] ?? []).concat(indices); } // Union set of common embeds for (const name of def.commonEmbeds){ collectedEmbeds.add(name); } } if (options.aliases != null) { setDefinitionAliases(nextDefinition, options.aliases); } const commonEmbeds = options.commonEmbeds ?? 'none'; if (commonEmbeds === 'all') { setDefinitionCommonEmbeds(nextDefinition, collectedEmbeds); } else if (Array.isArray(commonEmbeds)) { setDefinitionCommonEmbeds(nextDefinition, commonEmbeds, true); } if (options.views != null) { setDefinitionViews(nextDefinition, options.views); } return new Composite({ ...nextParams, definition: nextDefinition }); } /** * Set aliases for the Models in the composite, merging with existing ones unless `replace` is * `true`, and return a new Composite. */ setAliases(aliases, replace = false) { const params = this.toParams(); const definition = setDefinitionAliases(toStrictDefinition(params.definition), aliases, replace); return new Composite({ ...params, definition }); } /** * Set common embeds for the Models in the composite, merging with existing ones unless `replace` * is `true`, and return a new Composite. */ setCommonEmbeds(names, replace = false) { const params = this.toParams(); const definition = setDefinitionCommonEmbeds(toStrictDefinition(params.definition), names, replace); return new Composite({ ...params, definition }); } /** * Set views for the Models in the composite, merging with existing ones unless `replace` is * `true`, and return a new Composite. */ setViews(views, replace = false) { const params = this.toParams(); const definition = setDefinitionViews(toStrictDefinition(params.definition), views, replace); return new Composite({ ...params, definition }); } /** * Configure the Ceramic node to index the models defined in the composite. An authenticated DID * set as admin in the Ceramic node configuration must be attached to the Ceramic instance. */ async startIndexingOn(ceramic) { assertAuthenticatedDID(ceramic); const definedIndices = []; for (const [id, model] of Object.entries(_class_private_field_get(this, _definition).models)){ if (model.version === '1.0' || !model.interface) { definedIndices.push({ streamID: StreamID.fromString(id), indices: _class_private_field_get(this, _definition).indices?.[id] ?? [] }); } } await ceramic.admin.startIndexingModelData(definedIndices); } /** * Return a JSON-encoded `CompositeDefinition` structure that can be shared and reused. */ toJSON() { return { version: _class_private_field_get(this, _definition).version, models: encodeSignedMap(_class_private_field_get(this, _commits)), indices: _class_private_field_get(this, _definition).indices, aliases: _class_private_field_get(this, _definition).aliases, views: _class_private_field_get(this, _definition).views, commonEmbeds: _class_private_field_get(this, _definition).commonEmbeds }; } /** * Return a deep clone of the internal {@linkcode CompositeParams} for safe external access. */ toParams() { return { commits: cloneDeep(_class_private_field_get(this, _commits)), definition: cloneDeep(_class_private_field_get(this, _definition)) }; } /** * Return a `RuntimeCompositeDefinition` to be used at runtime by the * {@linkcode client.ComposeClient ComposeClient}. */ toRuntime() { return createRuntimeDefinition(_class_private_field_get(this, _definition)); } constructor(params){ _class_private_field_init(this, _commits, { writable: true, value: void 0 }); _class_private_field_init(this, _definition, { writable: true, value: void 0 }); _class_private_field_init(this, _hash, { writable: true, value: void 0 }); assertSupportedVersion(Composite.VERSION, params.definition.version); assertModelsHaveCommits(params.definition.models, params.commits); _class_private_field_set(this, _commits, cloneDeep(params.commits)); _class_private_field_set(this, _definition, toStrictDefinition(cloneDeep(params.definition))); } } /** * Current version of the composites format. */ _define_property(Composite, "VERSION", '1.1');