@composedb/devtools
Version:
Development tools for ComposeDB projects.
464 lines (463 loc) • 18.3 kB
JavaScript
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');