@composedb/devtools
Version:
Development tools for ComposeDB projects.
634 lines (633 loc) • 29.2 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;
}
import { makeExecutableSchema } from '@graphql-tools/schema';
import { getDirectives, MapperKind, mapSchema } from '@graphql-tools/utils';
import { isEnumType, isInterfaceType, isListType, isNonNullType, isObjectType, isScalarType } from 'graphql';
import { NODE_INTERFACE_NAME } from '../constants.js';
import { getScalarSchema } from './scalars.js';
import { typeDefinitions } from './type-definitions.js';
const ACCOUNT_RELATIONS = {
LIST: {
type: 'list'
},
NONE: {
type: 'none'
},
SET: {
type: 'set',
fields: []
},
SINGLE: {
type: 'single'
}
};
var _def = /*#__PURE__*/ new WeakMap(), _schema = /*#__PURE__*/ new WeakMap();
export class SchemaParser {
parse() {
mapSchema(_class_private_field_get(this, _schema), {
[MapperKind.ENUM_TYPE]: (type)=>{
if (type.name !== 'ModelAccountRelation') {
_class_private_field_get(this, _def).enums[type.name] = type.getValues().map((v)=>v.name);
}
return type;
},
[MapperKind.INTERFACE_TYPE]: (type)=>{
if (type.name === NODE_INTERFACE_NAME) {
return type;
}
const directives = getDirectives(_class_private_field_get(this, _schema), type);
const object = this._parseObject(type, directives);
_class_private_field_get(this, _def).objects[type.name] = object;
const model = this._parseModelDirective(type, directives, object);
if (model == null) {
throw new Error(`Missing @createModel or @loadModel directive for interface ${type.name}`);
} else {
_class_private_field_get(this, _def).models[type.name] = model;
}
return type;
},
[MapperKind.OBJECT_TYPE]: (type)=>{
const directives = getDirectives(_class_private_field_get(this, _schema), type);
const indices = this._parseIndices(directives);
const object = this._parseObject(type, directives);
object.indices = indices;
_class_private_field_get(this, _def).objects[type.name] = object;
const model = this._parseModelDirective(type, directives, object);
if (model != null) {
_class_private_field_get(this, _def).models[type.name] = model;
} else if (indices.length > 0) {
throw new Error('Indices added to type that is not a model');
}
return type;
},
[MapperKind.UNION_TYPE]: ()=>{
throw new Error('GraphQL unions are not supported');
}
});
const modelsNames = Object.keys(_class_private_field_get(this, _def).models);
if (modelsNames.length === 0) {
throw new Error('No models found in Composite Definition Schema');
}
// Once all models are defined, we need to validate the model names used in relations
for (const name of modelsNames){
// Validate model names in model relations
const model = _class_private_field_get(this, _def).models[name];
if (model.action === 'create') {
for (const [key, relation] of Object.entries(model.relations)){
if (relation.type === 'document' && relation.model !== null) {
if (relation.model === NODE_INTERFACE_NAME) {
relation.model = null;
} else {
this._validateRelatedModel(key, relation.model);
}
}
}
}
// Validate model names in object views
const object = _class_private_field_get(this, _def).objects[name];
if (object == null) {
throw new Error(`Missing object definition for model ${name}`);
}
for (const [key, field] of Object.entries(object.properties)){
if (field.type === 'view' && field.viewType === 'relation' && field.relation.model !== null) {
if (field.relation.model === NODE_INTERFACE_NAME) {
field.relation.model = null;
} else {
this._validateRelatedModel(key, field.relation.model);
}
}
}
}
return _class_private_field_get(this, _def);
}
_parseIndices(directives) {
return directives.flatMap((d)=>{
if (d.name === 'createIndex' && d.args) {
const fields = d.args.fields;
return [
{
fields
}
];
} else {
return [];
}
});
}
_validateRelatedModel(key, modelName) {
const relatedModel = _class_private_field_get(this, _def).models[modelName];
if (relatedModel == null) {
throw new Error(`Missing related model ${modelName} for relation defined on field ${key} of object ${modelName}`);
}
}
_parseModelDirective(type, directives, object) {
const createModel = directives.find((d)=>d.name === 'createModel');
const loadModel = directives.find((d)=>d.name === 'loadModel');
if (loadModel != null) {
const id = loadModel.args?.id;
if (id == null) {
throw new Error(`Missing id value for @loadModel directive on object ${type.name}`);
}
if (createModel != null) {
throw new Error(`Unsupported @createModel and @loadModel directives on same object ${type.name}`);
}
return {
action: 'load',
interface: isInterfaceType(type),
id
};
}
if (createModel != null) {
const isInterface = isInterfaceType(type);
const args = createModel.args ?? {};
const accountRelationType = args.accountRelation ?? 'LIST';
const accountRelation = ACCOUNT_RELATIONS[isInterface ? 'NONE' : accountRelationType];
if (accountRelation == null) {
throw new Error(`Unsupported accountRelation value ${accountRelationType} for @createModel directive on object ${type.name}`);
}
const accountRelationValue = {
...accountRelation
};
if (accountRelationValue.type === 'set') {
const accountRelationFields = args.accountRelationFields;
if (accountRelationFields == null) {
throw new Error(`Missing accountRelationFields argument for @createModel directive on object ${type.name}`);
}
if (accountRelationFields.length === 0) {
throw new Error(`The accountRelationFields argument must specify at least one field for @createModel directive on object ${type.name}`);
}
const object = _class_private_field_get(this, _def).objects[type.name];
if (object == null) {
throw new Error(`Missing object definition for ${type.name}`);
}
// Check properties are defined and valid in the JSON schema for the specified fields
for (const field of accountRelationFields){
const property = object.properties[field];
if (property == null) {
throw new Error(`Missing property ${field} defined in accountRelationFields argument for @createModel directive on object ${type.name}`);
}
if (!property.required) {
throw new Error(`Property ${field} defined in accountRelationFields argument for @createModel directive on object ${type.name} must have a required value`);
}
if (property.type !== 'enum' && property.type !== 'scalar') {
throw new Error(`Property ${field} defined in accountRelationFields argument for @createModel directive on object ${type.name} must use an enum or scalar type`);
}
}
accountRelationValue.fields = accountRelationFields;
}
if (args.description == null || args.description === '') {
throw new Error(`Missing description value for @createModel directive on object ${type.name}`);
}
const inheritedImmutableFields = type.getInterfaces().flatMap((interfaceObj)=>{
const fields = interfaceObj.getFields();
return Object.values(fields).filter((field)=>{
const { directives } = field.astNode;
return directives.some((directive)=>directive.name.value === 'immutable');
}).map((field)=>field.name);
});
return {
action: 'create',
interface: isInterfaceType(type),
implements: type.getInterfaces().map((i)=>i.name),
immutableFields: Array.from(new Set(Object.keys(object.properties).filter((key)=>object.properties[key].immutable === true).concat(inheritedImmutableFields))),
description: args.description,
accountRelation: accountRelationValue,
relations: object.relations
};
}
}
_parseObject(type, directives) {
const { definition, references, relations } = this._parseObjectFields(type, directives);
return {
// implements: type.getInterfaces().map((i) => i.name),
properties: definition,
references: Array.from(new Set(references)),
relations,
indices: []
};
}
_parseObjectFields(type, directives) {
const objectFields = type.getFields();
const fields = {};
let references = [];
const relations = {};
const hasCreateModel = directives.some((directive)=>directive.name === 'createModel') ?? false;
for (const [key, value] of Object.entries(objectFields)){
const directives = getDirectives(_class_private_field_get(this, _schema), value);
const [innerType, required] = isNonNullType(value.type) ? [
value.type.ofType,
true
] : [
value.type,
false
];
const relation = this._parseRelations(type.name, key, innerType, directives);
if (relation != null) {
relations[key] = relation;
}
const view = this._parseViews(type.name, key, innerType, directives, objectFields);
if (view != null) {
fields[key] = view;
} else if (isListType(innerType)) {
const list = this._parseListType(type.name, key, innerType, required, directives, hasCreateModel);
fields[key] = list.definition;
references = [
...references,
...list.references
];
} else {
const listDirective = directives.find((d)=>d.name === 'list');
if (listDirective != null) {
throw new Error(`Unexpected @list directive on field ${key} of object ${type.name}`);
}
const item = this._parseItemType(type.name, key, value.type, directives, hasCreateModel);
fields[key] = item.definition;
references = [
...references,
...item.references
];
}
}
return {
definition: fields,
references,
relations
};
}
_parseRelations(objectName, fieldName, type, directives) {
for (const directive of directives){
switch(directive.name){
case 'accountReference':
if (!isScalarType(type) || type.name !== 'DID') {
throw new Error(`Unsupported @accountReference directive on field ${fieldName} of object ${objectName}, @accountReference can only be set on a DID scalar`);
}
return {
type: 'account'
};
case 'documentReference':
if (!isScalarType(type) || type.name !== 'StreamID') {
throw new Error(`Unsupported @documentReference directive on field ${fieldName} of object ${objectName}, @documentReference can only be set on a StreamID scalar`);
}
return {
type: 'document',
model: directive.args?.model ?? null
};
}
}
}
_parseViews(objectName, fieldName, type, directives, objectFields) {
for (const directive of directives){
switch(directive.name){
case 'documentAccount':
if (!isScalarType(type) || type.name !== 'DID') {
throw new Error(`Unsupported @documentAccount directive on field ${fieldName} of object ${objectName}, @documentAccount can only be set on a DID scalar`);
}
return {
type: 'view',
required: true,
immutable: false,
viewType: 'documentAccount'
};
case 'documentVersion':
if (!isScalarType(type) || type.name !== 'CommitID') {
throw new Error(`Unsupported @documentVersion directive on field ${fieldName} of object ${objectName}, @documentVersion can only be set on a CommitID scalar`);
}
return {
type: 'view',
required: true,
immutable: false,
viewType: 'documentVersion'
};
case 'relationDocument':
{
if (!isInterfaceType(type) && !isObjectType(type)) {
throw new Error(`Unsupported @relationDocument directive on field ${fieldName} of object ${objectName}, @relationDocument can only be set on a referenced object`);
}
const property = directive.args?.property;
if (property == null) {
throw new Error(`Missing property argument for @relationDocument directive on field ${fieldName} of object ${objectName}`);
}
if (objectFields[property] == null) {
throw new Error(`Missing referenced property ${property} for @relationDocument directive on field ${fieldName} of object ${objectName}`);
}
return {
type: 'view',
required: false,
immutable: false,
viewType: 'relation',
relation: {
source: 'document',
model: type.name === NODE_INTERFACE_NAME ? null : type.name,
property
}
};
}
case 'relationFrom':
{
if (!isListType(type) || !(isInterfaceType(type.ofType) || isObjectType(type.ofType))) {
throw new Error(`Unsupported @relationFrom directive on field ${fieldName} of object ${objectName}, @relationFrom can only be set on a list of referenced object`);
}
const model = type.ofType.name === NODE_INTERFACE_NAME ? null : type.ofType.name;
const property = directive.args?.property;
if (property == null) {
throw new Error(`Missing property argument for @relationFrom directive on field ${fieldName} of object ${objectName}`);
}
return {
type: 'view',
required: true,
immutable: false,
viewType: 'relation',
relation: {
source: 'queryConnection',
model,
property
}
};
}
case 'relationCountFrom':
{
if (!isScalarType(type) || type.name !== 'Int') {
throw new Error(`Unsupported @relationCountFrom directive on field ${fieldName} of object ${objectName}, @relationCountFrom can only be set on a Int scalar`);
}
const model = directive.args?.model;
if (model == null) {
throw new Error(`Missing model argument for @relationCountFrom directive on field ${fieldName} of object ${objectName}`);
}
const property = directive.args?.property;
if (property == null) {
throw new Error(`Missing property argument for @relationCountFrom directive on field ${fieldName} of object ${objectName}`);
}
return {
type: 'view',
required: true,
immutable: false,
viewType: 'relation',
relation: {
source: 'queryCount',
model,
property
}
};
}
case 'relationSetFrom':
{
if (!isObjectType(type)) {
throw new Error(`Unsupported @relationSetFrom directive on field ${fieldName} of object ${objectName}, @relationSetFrom can only be set on a referenced object`);
}
const property = directive.args?.property;
if (property == null) {
throw new Error(`Missing property argument for @relationSetFrom directive on field ${fieldName} of object ${objectName}`);
}
return {
type: 'view',
required: false,
viewType: 'relation',
relation: {
source: 'set',
model: type.name,
property
}
};
}
}
}
}
_parseListType(objectName, fieldName, type, required, directives, hasCreateModel) {
const list = directives.find((d)=>d.name === 'list');
if (list == null) {
throw new Error(`Missing @list directive on list field ${fieldName} of object ${objectName}`);
}
if (list.args?.maxLength == null) {
throw new Error(`Missing maxLength value for @list directive on field ${fieldName} of object ${objectName}`);
}
const item = this._parseItemType(objectName, fieldName, type.ofType, directives, hasCreateModel);
const definition = {
type: 'list',
required,
item: item.definition,
maxLength: list.args.maxLength
};
if (list.args?.minLength != null) {
definition.minLength = list.args.minLength;
}
return {
definition,
references: item.references
};
}
_parseItemType(objectName, fieldName, type, directives, hasCreateModel) {
const required = isNonNullType(type);
const immutable = directives.some((item)=>item.name === 'immutable');
if (immutable && !hasCreateModel) {
throw new Error(`Unsupported immutable directive for ${fieldName} on nested object ${objectName}`);
}
const innerType = required ? type.ofType : type;
if (isListType(innerType)) {
throw new Error(`Unsupported nested list on field ${fieldName} of object ${objectName}`);
}
const referenceType = this._getReferenceFieldType(innerType);
if (referenceType != null) {
return {
definition: {
type: referenceType,
required,
immutable,
name: innerType.name
},
references: [
innerType.name
]
};
}
if (isScalarType(innerType)) {
return {
definition: {
type: 'scalar',
required,
immutable,
schema: this._parseScalarSchema(objectName, fieldName, innerType, directives)
},
references: []
};
}
throw new Error(`Unsupported type ${innerType.name} on field ${fieldName} of object ${objectName}`);
}
_parseScalarSchema(objectName, fieldName, type, directives) {
const schema = getScalarSchema(type);
const boolean = directives.find((d)=>d.name === 'boolean');
const float = directives.find((d)=>d.name === 'float');
const int = directives.find((d)=>d.name === 'int');
const string = directives.find((d)=>d.name === 'string');
switch(schema.type){
case 'boolean':
{
const mismatch = [
float,
int,
string
].find(Boolean);
if (mismatch) {
throw new Error(`Unexpected @${mismatch.name} directive with type ${type.name} on field ${fieldName} of object ${objectName}`);
}
break;
}
case 'integer':
{
const mismatch = [
boolean,
float,
string
].find(Boolean);
if (mismatch) {
throw new Error(`Unexpected @${mismatch.name} directive with type ${type.name} on field ${fieldName} of object ${objectName}`);
}
return this._validateIntegerSchema(objectName, fieldName, schema, int);
}
case 'number':
{
const mismatch = [
boolean,
int,
string
].find(Boolean);
if (mismatch) {
throw new Error(`Unexpected @${mismatch.name} directive with type ${type.name} on field ${fieldName} of object ${objectName}`);
}
return this._validateNumberSchema(objectName, fieldName, schema, float);
}
case 'string':
{
const mismatch = [
boolean,
float,
int
].find(Boolean);
if (mismatch) {
throw new Error(`Unexpected @${mismatch.name} directive with type ${type.name} on field ${fieldName} of object ${objectName}`);
}
return this._validateStringSchema(objectName, fieldName, schema, string);
}
}
return schema;
}
_validateIntegerSchema(objectName, fieldName, schema, directive) {
const args = directive?.args;
return args ? this._validateNumberArguments(objectName, fieldName, schema, args) : schema;
}
_validateNumberSchema(objectName, fieldName, schema, directive) {
const args = directive?.args;
return args ? this._validateNumberArguments(objectName, fieldName, schema, args) : schema;
}
_validateNumberArguments(objectName, fieldName, schema, args) {
if (args.max != null) {
schema.maximum = args.max;
}
if (args.min != null) {
schema.minimum = args.min;
}
if (args.default != null) {
if (args.max != null && args.default > args.max) {
throw new Error(`Default value is higher than max constraint on field ${fieldName} of object ${objectName}`);
}
if (args.min != null && args.default < args.min) {
throw new Error(`Default value is lower than min constraint on field ${fieldName} of object ${objectName}`);
}
schema.default = args.default;
}
return schema;
}
_validateStringSchema(objectName, fieldName, schema, string) {
const defaultValue = string?.args?.default ?? schema.default;
const maxLength = string?.args?.maxLength ?? schema.maxLength;
const minLength = string?.args?.minLength ?? schema.minLength;
if (maxLength == null) {
if (string == null) {
throw new Error(`Missing @string directive on string field ${fieldName} of object ${objectName}`);
}
throw new Error(`Missing maxLength value for @string directive on field ${fieldName} of object ${objectName}`);
}
schema.maxLength = maxLength;
if (minLength != null) {
schema.minLength = minLength;
}
if (defaultValue != null) {
if (defaultValue.length > maxLength) {
throw new Error(`Length of default value is higher than maxLength constraint on field ${fieldName} of object ${objectName}`);
}
if (minLength != null && defaultValue.length < minLength) {
throw new Error(`Length of default value is lower than minLength constraint on field ${fieldName} of object ${objectName}`);
}
schema.default = defaultValue;
}
return schema;
}
_getReferenceFieldType(type) {
if (isEnumType(type)) {
return 'enum';
}
if (isObjectType(type)) {
return 'object';
}
}
constructor(schema){
_class_private_field_init(this, _def, {
writable: true,
value: {
enums: {},
models: {},
objects: {}
}
});
_class_private_field_init(this, _schema, {
writable: true,
value: void 0
});
_class_private_field_set(this, _schema, makeExecutableSchema({
typeDefs: [
typeDefinitions,
schema
]
}));
}
}
export function parseSchema(schema) {
return new SchemaParser(schema).parse();
}