@itwin/ecschema-metadata
Version:
ECObjects core concepts in typescript
411 lines • 20.1 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import { Logger } from "@itwin/core-bentley";
import { SchemaMatchType } from "../ECObjects";
import { SchemaKey } from "../SchemaKey";
import { FullSchemaQueries } from "./FullSchemaQueries";
import { IncrementalSchemaLocater } from "./IncrementalSchemaLocater";
import { SchemaItemQueries } from "./SchemaItemQueries";
import { SchemaParser } from "./SchemaParser";
import { ecsqlQueries } from "./SchemaStubQueries";
const LOGGER_CATEGORY = "IncrementalSchemaLoading.Performance";
/**
* An abstract [[IncrementalSchemaLocater]] implementation for loading
* EC [Schema] instances from an iModelDb using ECSql queries.
* @internal
*/
export class ECSqlSchemaLocater extends IncrementalSchemaLocater {
/**
* Gets the [[ECSqlSchemaLocaterOptions]] used by this locater.
*/
get options() {
return super.options;
}
/**
* Initializes a new ECSqlSchemaLocater instance.
* @param options The options used by this Schema locater.
*/
constructor(options) {
super(options);
}
/**
* Gets the [[SchemaProps]] for the given schema key. This is the full schema json with all elements that are defined
* in the schema. The schema locater calls this after the stub has been loaded to fully load the schema in the background.
* @param schemaKey The [[SchemaKey]] of the schema to be resolved.
* @param context The [[SchemaContext]] to use for resolving references.
* @internal
*/
async getSchemaJson(schemaKey, context) {
// If the meta schema is an earlier version than 4.0.3, we can't use the ECSql query interface to get the schema
// information required to load the schema entirely. In this case, we fallback to use the ECSchema RPC interface
// to fetch the whole schema json.
if (!await this.supportPartialSchemaLoading(context))
return this.getSchemaProps(schemaKey);
const queryStart = Date.now();
const schemaProps = this.options.useMultipleQueries
? await this.getFullSchemaMultipleQueries(schemaKey, context)
: await this.getFullSchema(schemaKey, context);
const queryDuration = Date.now() - queryStart;
Logger.logTrace(LOGGER_CATEGORY, `Recieved SchemaProps for ${schemaKey.name} in ${queryDuration}ms`, {
schemaName: schemaKey.name,
queryMode: this.options.useMultipleQueries ? "parallel" : "single",
duration: queryDuration,
});
return schemaProps;
}
;
/**
* Gets the [[SchemaProps]] without schemaItems for the given schema name.
* @param schemaName The name of the Schema.
* @param context The [[SchemaContext]] to use for resolving references.
* @returns
* @internal
*/
async getSchemaNoItems(schemaName, context) {
const schemaRows = await this.executeQuery(FullSchemaQueries.schemaNoItemsQuery, { parameters: { schemaName } });
const schemaRow = schemaRows[0];
if (schemaRow === undefined)
return undefined;
const schema = JSON.parse(schemaRow.schema);
const schemaInfos = await this._schemaInfoCache.getSchemasByContext(context) ?? [];
return SchemaParser.parse(schema, schemaInfos);
}
/**
* Checks if the [[SchemaContext]] has the right Meta Schema version to support the incremental schema loading.
* @param context The schema context to lookup the meta schema.
* @returns true if the context has a supported meta schema version, false otherwise.
*/
async supportPartialSchemaLoading(context) {
const metaSchemaKey = new SchemaKey("ECDbMeta", 4, 0, 3);
const metaSchemaInfo = await context.getSchemaInfo(metaSchemaKey, SchemaMatchType.LatestWriteCompatible);
return metaSchemaInfo !== undefined;
}
;
/**
* Gets all the Schema's Entity classes as [[EntityClassProps]] JSON objects.
* @param schemaName The name of the Schema.
* @param context The [[SchemaContext]] to which the schema belongs.
* @returns A promise that resolves to a EntityClassProps array. Maybe empty of no entities are found.
* @internal
*/
async getEntities(schema, context, queryOverride) {
const query = queryOverride ?? FullSchemaQueries.entityQuery;
return this.querySchemaItem(context, schema, query, "EntityClass");
}
/**
* Gets all the Schema's Mixin classes as [[MixinProps]] JSON objects.
* @param schemaName The name of the Schema.
* @param context The SchemaContext to which the schema belongs.
* @returns A promise that resolves to a MixinProps array. Maybe empty of no entities are found.
* @internal
*/
async getMixins(schema, context, queryOverride) {
const query = queryOverride ?? FullSchemaQueries.mixinQuery;
return this.querySchemaItem(context, schema, query, "Mixin");
}
/**
* Gets all the Schema's Relationship classes as [[RelationshipClassProps]] JSON objects.
* @param schemaName The name of the Schema.
* @param context The SchemaContext to which the schema belongs.
* @returns A promise that resolves to a RelationshipClassProps array. Maybe empty if no items are found.
* @internal
*/
async getRelationships(schema, context, queryOverride) {
const query = queryOverride ?? FullSchemaQueries.relationshipClassQuery;
return this.querySchemaItem(context, schema, query, "RelationshipClass");
}
/**
* Gets all the Schema's CustomAttributeClass items as [[CustomAttributeClassProps]] JSON objects.
* @param schemaName The name of the Schema.
* @param context The SchemaContext to which the schema belongs.
* @returns A promise that resolves to a CustomAttributeClassProps array. Maybe empty if not items are found.
* @internal
*/
async getCustomAttributeClasses(schema, context, queryOverride) {
const query = queryOverride ?? FullSchemaQueries.customAttributeQuery;
return this.querySchemaItem(context, schema, query, "CustomAttributeClass");
}
/**
* Gets all the Schema's StructClass items as [[StructClassProps]] JSON objects.
* @param schemaName The name of the Schema.
* @param context The SchemaContext to which the schema belongs.
* @returns A promise that resolves to a StructClassProps array. Maybe empty if not items are found.
* @internal
*/
async getStructs(schema, context, queryOverride) {
const query = queryOverride ?? FullSchemaQueries.structQuery;
return this.querySchemaItem(context, schema, query, "StructClass");
}
/**
* Gets all the Schema's KindOfQuantity items as [[KindOfQuantityProps]] JSON objects.
* @param schema The name of the Schema.
* @param context The SchemaContext to which the schema belongs.
* @returns A promise that resolves to a KindOfQuantityProps array. Maybe empty if not items are found.
* @internal
*/
async getKindOfQuantities(schema, context) {
return this.querySchemaItem(context, schema, SchemaItemQueries.kindOfQuantity(true), "KindOfQuantity");
}
/**
* Gets all the Schema's PropertyCategory items as [[PropertyCategoryProps]] JSON objects.
* @param schema The name of the Schema.
* @param context The SchemaContext to which the schema belongs.
* @returns A promise that resolves to a PropertyCategoryProps array. Maybe empty if not items are found.
* @internal
*/
async getPropertyCategories(schema, context) {
return this.querySchemaItem(context, schema, SchemaItemQueries.propertyCategory(true), "PropertyCategory");
}
/**
* Gets all the Schema's Enumeration items as [[EnumerationProps]] JSON objects.
* @param schema The name of the Schema.
* @param context The SchemaContext to which the schema belongs.
* @returns A promise that resolves to a EnumerationProps array. Maybe empty if not items are found.
* @internal
*/
async getEnumerations(schema, context) {
return this.querySchemaItem(context, schema, SchemaItemQueries.enumeration(true), "Enumeration");
}
/**
* Gets all the Schema's Unit items as [[SchemaItemUnitProps]] JSON objects.
* @param schema The name of the Schema.
* @param context The SchemaContext to which the schema belongs.
* @returns A promise that resolves to a SchemaItemUnitProps array. Maybe empty if not items are found.
* @internal
*/
async getUnits(schema, context) {
return this.querySchemaItem(context, schema, SchemaItemQueries.unit(true), "Unit");
}
/**
* Gets all the Schema's InvertedUnit items as [[InvertedUnitProps]] JSON objects.
* @param schema The name of the Schema.
* @param context The SchemaContext to which the schema belongs.
* @returns A promise that resolves to a InvertedUnitProps array. Maybe empty if not items are found.
* @internal
*/
async getInvertedUnits(schema, context) {
return this.querySchemaItem(context, schema, SchemaItemQueries.invertedUnit(true), "InvertedUnit");
}
/**
* Gets all the Schema's Constant items as [[ConstantProps]] JSON objects.
* @param schema The name of the Schema.
* @param context The SchemaContext to which the schema belongs.
* @returns A promise that resolves to a ConstantProps array. Maybe empty if not items are found.
* @internal
*/
async getConstants(schema, context) {
return this.querySchemaItem(context, schema, SchemaItemQueries.constant(true), "Constant");
}
/**
* Gets all the Schema's UnitSystem items as [[UnitSystemProps]] JSON objects.
* @param schema The name of the Schema.
* @param context The SchemaContext to which the schema belongs.
* @returns A promise that resolves to a UnitSystemProps array. Maybe empty if not items are found.
* @internal
*/
async getUnitSystems(schema, context) {
return this.querySchemaItem(context, schema, SchemaItemQueries.unitSystem(true), "UnitSystem");
}
/**
* Gets all the Schema's Phenomenon items as [[PhenomenonProps]] JSON objects.
* @param schema The name of the Schema.
* @param context The SchemaContext to which the schema belongs.
* @returns A promise that resolves to a PhenomenonProps array. Maybe empty if not items are found.
* @internal
*/
async getPhenomenon(schema, context) {
return this.querySchemaItem(context, schema, SchemaItemQueries.phenomenon(true), "Phenomenon");
}
/**
* Gets all the Schema's Format items as [[SchemaItemFormatProps]] JSON objects.
* @param schema The name of the Schema.
* @param context The SchemaContext to which the schema belongs.
* @returns A promise that resolves to a SchemaItemFormatProps array. Maybe empty if not items are found.
* @internal
*/
async getFormats(schema, context) {
return this.querySchemaItem(context, schema, SchemaItemQueries.format(true), "Format");
}
/**
* Gets [[SchemaInfo]] objects for all schemas including their direct schema references.
* @internal
*/
async loadSchemaInfos() {
const schemaRows = await this.executeQuery(ecsqlQueries.schemaInfoQuery);
return schemaRows.map((schemaRow) => ({
alias: schemaRow.alias,
description: schemaRow.description,
label: schemaRow.label,
schemaKey: SchemaKey.parseString(`${schemaRow.name}.${schemaRow.version}`),
references: Array.from(JSON.parse(schemaRow.references), parseSchemaReference),
}));
}
/**
* Gets the [[SchemaProps]] to create the basic schema skeleton. Depending on which options are set, the schema items or class hierarchy
* can be included in the initial fetch.
* @param schemaKey The [[SchemaKey]] of the schema to be resolved.
* @returns A promise that resolves to the schema partials, which is an array of [[SchemaProps]].
* @internal
*/
async getSchemaPartials(schemaKey, context) {
const queryStart = Date.now();
const itemRows = await this.executeQuery(ecsqlQueries.schemaStubQuery, {
parameters: { schemaName: schemaKey.name }
});
const queryDuration = Date.now() - queryStart;
Logger.logTrace(LOGGER_CATEGORY, `Recieved PartialSchema for ${schemaKey.name} in ${queryDuration}ms`, {
schemaName: schemaKey.name,
itemCount: itemRows.length,
duration: queryDuration,
});
if (itemRows.length === 0)
return undefined;
const schemaPartials = [];
const addSchema = async (key) => {
const stub = await this.createSchemaProps(key, context);
schemaPartials.push(stub);
if (stub.references) {
for (const referenceProps of stub.references) {
if (!schemaPartials.some((schema) => schema.name === referenceProps.name)) {
await addSchema(SchemaKey.parseString(`${referenceProps.name}.${referenceProps.version}`));
}
}
}
return stub;
};
const addItems = async (schemaName, itemInfo) => {
let schemaStub = schemaPartials.find((schema) => schema.name === schemaName);
if (!schemaStub) {
schemaStub = await addSchema(SchemaKey.parseString(`${schemaName}.0.0.0`));
}
let items = schemaStub.items;
if (!items) {
Object.assign(schemaStub, items = { items: {} });
}
const existingItem = items[itemInfo.name] || {};
Object.assign(items, { [itemInfo.name]: Object.assign(existingItem, itemInfo) });
};
const reviver = (_key, value) => {
return value === null ? undefined : value;
};
await addSchema(schemaKey);
const schemaInfos = await this._schemaInfoCache.getSchemasByContext(context) ?? [];
const stubItems = itemRows.map((itemRow) => {
return JSON.parse(itemRow.item, reviver);
});
await parseSchemaItemStubs(schemaKey.name, stubItems, addItems, schemaInfos);
return schemaPartials;
}
async querySchemaItem(context, schemaName, query, schemaType) {
const start = Date.now();
const itemRows = await this.executeQuery(query, { parameters: { schemaName } });
const queryDuration = Date.now() - start;
Logger.logTrace(LOGGER_CATEGORY, `Recieved rows of ${schemaType} items for ${schemaName} in ${queryDuration}ms`, {
schemaName,
itemCount: itemRows.length,
itemType: schemaType,
duration: queryDuration,
});
if (itemRows.length === 0)
return [];
const items = itemRows.map((itemRow) => {
return "string" === typeof itemRow.item ? JSON.parse(itemRow.item) : itemRow.item;
});
const schemaInfos = await this._schemaInfoCache.getSchemasByContext(context) ?? [];
return await SchemaParser.parseSchemaItems(items, schemaName, schemaInfos) ?? [];
}
async getFullSchema(schemaKey, context) {
const schemaRows = await this.executeQuery(FullSchemaQueries.schemaQuery, { parameters: { schemaName: schemaKey.name } });
const schemaRow = schemaRows[0];
if (schemaRow === undefined)
return undefined;
// Map SchemaItemRow array, [{item: SchemaItemProps}], to array of SchemaItemProps.
const schema = JSON.parse(schemaRow.schema);
if (schema.items) {
schema.items = schema.items.map((itemRow) => { return itemRow.item; });
}
const schemaInfos = await this._schemaInfoCache.getSchemasByContext(context) ?? [];
return SchemaParser.parse(schema, schemaInfos);
}
async getFullSchemaMultipleQueries(schemaKey, context) {
const schema = await this.getSchemaNoItems(schemaKey.name, context);
if (!schema)
return undefined;
const items = schema.items || (schema.items = {});
await Promise.all([
this.getEntities(schemaKey.name, context),
this.getMixins(schemaKey.name, context),
this.getStructs(schemaKey.name, context),
this.getRelationships(schemaKey.name, context),
this.getCustomAttributeClasses(schemaKey.name, context),
this.getKindOfQuantities(schemaKey.name, context),
this.getPropertyCategories(schemaKey.name, context),
this.getEnumerations(schemaKey.name, context),
this.getUnits(schemaKey.name, context),
this.getInvertedUnits(schemaKey.name, context),
this.getUnitSystems(schemaKey.name, context),
this.getConstants(schemaKey.name, context),
this.getPhenomenon(schemaKey.name, context),
this.getFormats(schemaKey.name, context)
]).then((itemResults) => {
const flatItemList = itemResults.reduce((acc, result) => acc.concat(result));
flatItemList.forEach((schemaItem) => {
if (!schemaItem.name) {
// This should never be happen, as we query the schema items by name from the database, but since the SchemaProps
// have name optional, we need the check here to make the compiler happy.
throw new Error(`SchemaItem with no name encountered in schema ${schemaKey.name}`);
}
items[schemaItem.name] = schemaItem;
});
});
return schema;
}
}
function parseSchemaReference(referenceName) {
return { schemaKey: SchemaKey.parseString(referenceName) };
}
async function parseSchemaItemStubs(schemaName, itemRows, addItemsHandler, schemaInfos) {
if (!itemRows || itemRows.length === 0) {
return;
}
const parseBaseClasses = async (baseClasses) => {
if (!baseClasses || baseClasses.length < 2)
return;
for (let index = baseClasses.length - 1; index >= 0;) {
const currentItem = baseClasses[index--];
const baseClassItem = baseClasses[index];
const baseClassName = baseClassItem ? `${baseClassItem.schema}.${baseClassItem.name}` : undefined;
const schemaItem = await SchemaParser.parseItem(currentItem, currentItem.schema, schemaInfos);
await addItemsHandler(currentItem.schema, {
...schemaItem,
name: schemaItem.name,
schemaItemType: schemaItem.schemaItemType,
baseClass: baseClassName,
});
}
};
for (const itemRow of itemRows) {
const schemaItem = await SchemaParser.parseItem(itemRow, schemaName, schemaInfos);
await addItemsHandler(schemaName, {
...schemaItem,
name: schemaItem.name,
schemaItemType: schemaItem.schemaItemType,
mixins: itemRow.mixins
? itemRow.mixins.map(mixin => { return `${mixin.schema}.${mixin.name}`; })
: undefined,
});
await parseBaseClasses(itemRow.baseClasses);
for (const mixinRow of itemRow.mixins || []) {
const mixinItem = await SchemaParser.parseItem(mixinRow, mixinRow.schema, schemaInfos);
await addItemsHandler(mixinRow.schema, {
...mixinItem,
name: mixinItem.name,
schemaItemType: mixinItem.schemaItemType,
});
await parseBaseClasses(mixinRow.baseClasses);
}
}
}
//# sourceMappingURL=ECSqlSchemaLocater.js.map