Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@contember/schema-utils

Package Overview
Dependencies
Maintainers
5
Versions
259
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@contember/schema-utils - npm Package Compare versions

Comparing version 1.2.0-alpha.18 to 1.2.0-alpha.19

dist/src/type-schema/settings.d.ts

2

dist/src/index.js

@@ -45,2 +45,3 @@ "use strict";

validation: {},
settings: {},
};

@@ -51,3 +52,4 @@ exports.schemaType = Typesafe.object({

validation: type_schema_1.validationSchema,
settings: type_schema_1.settingsSchema,
});
//# sourceMappingURL=index.js.map

4

dist/src/model/NamingHelper.js

@@ -8,3 +8,3 @@ "use strict";

exports.NamingHelper = void 0;
const crypto_1 = __importDefault(require("crypto"));
const node_crypto_1 = __importDefault(require("node:crypto"));
class NamingHelper {

@@ -41,3 +41,3 @@ static createForeignKeyIndexName(tableName, column) {

NamingHelper.createUniqueSuffix = (values) => {
const uniqueSuffix = crypto_1.default
const uniqueSuffix = node_crypto_1.default
.createHash('sha256')

@@ -44,0 +44,0 @@ .update(JSON.stringify(values), 'ascii')

export * from './acl';
export * from './condition';
export * from './model';
export * from './settings';
export * from './validation';
//# sourceMappingURL=index.d.ts.map

@@ -20,3 +20,4 @@ "use strict";

__exportStar(require("./model"), exports);
__exportStar(require("./settings"), exports);
__exportStar(require("./validation"), exports);
//# sourceMappingURL=index.js.map

@@ -7,3 +7,3 @@ "use strict";

const errors_1 = require("./errors");
const condition_1 = require("../type-schema/condition");
const type_schema_1 = require("../type-schema");
class AclValidator {

@@ -38,3 +38,3 @@ constructor(model) {

if (!roles.includes(inheritsFrom)) {
errorBuilder.for(inheritsFrom).add('Referenced role not exists.');
errorBuilder.for(inheritsFrom).add('ACL_UNDEFINED_ROLE', 'Referenced role not exists.');
}

@@ -55,3 +55,3 @@ }

if (!this.model.entities[variable.entityName]) {
errorBuilder.add(`Entity "${variable.entityName}" not found`);
errorBuilder.add('ACL_UNDEFINED_ENTITY', `Entity "${variable.entityName}" not found`);
return;

@@ -68,3 +68,3 @@ }

if (!this.model.entities[entityName]) {
errorBuilder.add(`Entity ${entityName} not found`);
errorBuilder.add('ACL_UNDEFINED_ENTITY', `Entity ${entityName} not found`);
continue;

@@ -94,3 +94,3 @@ }

if (!variables[ctx.value]) {
errorBuilder.for(...ctx.path).add(`Undefined variable ${ctx.value}`);
errorBuilder.for(...ctx.path).add('ACL_UNDEFINED_VARIABLE', `Undefined variable ${ctx.value}`);
}

@@ -100,6 +100,6 @@ }

try {
(0, condition_1.conditionSchema)(ctx.column.type)(ctx.value);
(0, type_schema_1.conditionSchema)(ctx.column.type)(ctx.value);
}
catch (e) {
errorBuilder.for(...ctx.path).add(`Invalid condition (${e.message}): ${JSON.stringify(ctx.value)}`);
errorBuilder.for(...ctx.path).add('ACL_INVALID_CONDITION', `Invalid condition (${e.message}): ${JSON.stringify(ctx.value)}`);
}

@@ -111,3 +111,3 @@ }

if (typeof ctx.value === 'string' && !variables[ctx.value]) {
errorBuilder.for(...ctx.path).add(`Undefined variable ${ctx.value}`);
errorBuilder.for(...ctx.path).add('ACL_UNDEFINED_VARIABLE', `Undefined variable ${ctx.value}`);
}

@@ -117,3 +117,3 @@ return ctx.value;

handleUndefinedField: ctx => {
errorBuilder.for(...ctx.path).add(`Undefined field ${ctx.name} on entity ${ctx.entity.name}`);
errorBuilder.for(...ctx.path).add('ACL_UNDEFINED_FIELD', `Undefined field ${ctx.name} on entity ${ctx.entity.name}`);
return ctx.value;

@@ -140,3 +140,3 @@ },

if (!entity.fields[field]) {
errorBuilder.add(`Field ${field} not found on entity ${entity.name}`);
errorBuilder.add('ACL_UNDEFINED_FIELD', `Field ${field} not found on entity ${entity.name}`);
}

@@ -151,3 +151,3 @@ this.validatePredicate(permissions[field], predicates, errorBuilder.for(field));

if (!predicates[predicate]) {
errorBuilder.add(`Predicate ${predicate} not found`);
errorBuilder.add('ACL_UNDEFINED_PREDICATE', `Predicate ${predicate} not found`);
}

@@ -154,0 +154,0 @@ }

@@ -0,4 +1,6 @@

export declare type ValidationErrorCode = 'MODEL_NAME_MISMATCH' | 'MODEL_UNDEFINED_FIELD' | 'MODEL_UNDEFINED_ENTITY' | 'MODEL_RELATION_REQUIRED' | 'MODEL_INVALID_RELATION_DEFINITION' | 'MODEL_INVALID_COLUMN_DEFINITION' | 'MODEL_INVALID_VIEW_USAGE' | 'MODEL_INVALID_IDENTIFIER' | 'MODEL_NAME_COLLISION' | 'ACL_INVALID_CONDITION' | 'ACL_UNDEFINED_VARIABLE' | 'ACL_UNDEFINED_FIELD' | 'ACL_UNDEFINED_PREDICATE' | 'ACL_UNDEFINED_ROLE' | 'ACL_UNDEFINED_ENTITY' | 'VALIDATION_UNDEFINED_ENTITY' | 'VALIDATION_UNDEFINED_FIELD' | 'VALIDATION_NOT_IMPLEMENTED';
export interface ValidationError {
path: (string | number)[];
message: string;
code: ValidationErrorCode;
}

@@ -10,4 +12,4 @@ export declare class ErrorBuilder {

for(...path: string[]): ErrorBuilder;
add(message: string): void;
add(code: ValidationErrorCode, message: string): void;
}
//# sourceMappingURL=errors.d.ts.map

@@ -12,4 +12,4 @@ "use strict";

}
add(message) {
this.errors.push({ path: this.path, message });
add(code, message) {
this.errors.push({ path: this.path, message, code });
}

@@ -16,0 +16,0 @@ }

@@ -32,3 +32,3 @@ "use strict";

if (entity.name !== entityName) {
entityErrors.add(`Entity name "${entity.name}" does not match the name in a map "${entityName}"`);
entityErrors.add('MODEL_NAME_MISMATCH', `Entity name "${entity.name}" does not match the name in a map "${entityName}"`);
}

@@ -44,3 +44,3 @@ this.validateEntity(entity, entityErrors);

if (field.name !== fieldName) {
fieldErrors.add(`Field name "${field.name}" does not match the name in a map "${fieldName}"`);
fieldErrors.add('MODEL_NAME_MISMATCH', `Field name "${field.name}" does not match the name in a map "${fieldName}"`);
}

@@ -54,3 +54,3 @@ }

if (constraint.name !== constraintName) {
uniqueErrors.add(`Constraint name ${constraint.name} does not match the name in a map "${constraintName}"`);
uniqueErrors.add('MODEL_NAME_MISMATCH', `Constraint name ${constraint.name} does not match the name in a map "${constraintName}"`);
continue;

@@ -60,3 +60,3 @@ }

if (!fields.has(field)) {
uniqueErrors.add(`Referenced field ${field} in a constraint does not exists`);
uniqueErrors.add('MODEL_UNDEFINED_FIELD', `Referenced field ${field} in a constraint does not exists`);
}

@@ -73,3 +73,3 @@ }

if (field.sequence && field.nullable) {
errors.add('Column with sequence cannot be nullable.');
errors.add('MODEL_INVALID_COLUMN_DEFINITION', 'Column with sequence cannot be nullable.');
}

@@ -84,3 +84,3 @@ }

if (!targetEntity) {
return errors.add(`Target entity ${targetEntityName} not found`);
return errors.add('MODEL_UNDEFINED_ENTITY', `Target entity ${targetEntityName} not found`);
}

@@ -94,3 +94,3 @@ if (((it) => 'orderBy' in it)(field)) {

if (!orderByField) {
errors.add(`Invalid orderBy of ${entityName}::${field.name}: field ${pathStr} is not defined`);
errors.add('ACL_UNDEFINED_FIELD', `Invalid orderBy of ${entityName}::${field.name}: field ${pathStr} is not defined`);
return;

@@ -101,3 +101,3 @@ }

if (!targetEntity) {
errors.add(`Invalid orderBy of ${entityName}::${field.name}: field ${pathStr} is not a relation`);
errors.add('MODEL_RELATION_REQUIRED', `Invalid orderBy of ${entityName}::${field.name}: field ${pathStr} is not a relation`);
return;

@@ -112,7 +112,7 @@ }

if (field.type === schema_1.Model.RelationType.ManyHasMany) {
return errors.add('Many-has-many relation is not allowed on a view entity.');
return errors.add('MODEL_INVALID_VIEW_USAGE', 'Many-has-many relation is not allowed on a view entity.');
}
if (!targetEntity.view &&
field.type === schema_1.Model.RelationType.OneHasMany) {
return errors.add('One-has-many relation fields on views must point to a view entity.');
return errors.add('MODEL_INVALID_VIEW_USAGE', 'One-has-many relation fields on views must point to a view entity.');
}

@@ -122,3 +122,3 @@ if (!targetEntity.view &&

!('joiningColumn' in field)) {
return errors.add('One-has-one relation fields on views must be owning or point to a view entity.');
return errors.add('MODEL_INVALID_VIEW_USAGE', 'One-has-one relation fields on views must be owning or point to a view entity.');
}

@@ -131,28 +131,28 @@ }

if (!targetField) {
return errors.add(`${relationDescription} not exists`);
return errors.add('MODEL_UNDEFINED_FIELD', `${relationDescription} not exists`);
}
if (!isRelation(targetField)) {
return errors.add(`${relationDescription} not a relation`);
return errors.add('MODEL_RELATION_REQUIRED', `${relationDescription} not a relation`);
}
if (targetField.target !== entityName) {
return errors.add(`${relationDescription} back reference to entity ${entityName} expected, but ${targetField.target} given`);
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference to entity ${entityName} expected, but ${targetField.target} given`);
}
if (!(0, model_1.isOwningRelation)(targetField)) {
errors.add(`${relationDescription} not an owning relation`);
errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} not an owning relation`);
return;
}
if (!targetField.inversedBy) {
return errors.add(`${relationDescription} inverse relation is not set`);
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} inverse relation is not set`);
}
if (targetField.inversedBy !== field.name) {
return errors.add(`${relationDescription} back reference ${entityName}::${field.name} exepcted, ${targetField.target}::${targetField.inversedBy} given`);
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference ${entityName}::${field.name} exepcted, ${targetField.target}::${targetField.inversedBy} given`);
}
if (field.type === schema_1.Model.RelationType.OneHasOne && targetField.type !== schema_1.Model.RelationType.OneHasOne) {
return errors.add(`${relationDescription} "OneHasOne" type expected, "${targetField.type}" given`);
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} "OneHasOne" type expected, "${targetField.type}" given`);
}
if (field.type === schema_1.Model.RelationType.OneHasMany && targetField.type !== schema_1.Model.RelationType.ManyHasOne) {
return errors.add(`${relationDescription} "ManyHasOne" type expected, "${targetField.type}" given`);
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} "ManyHasOne" type expected, "${targetField.type}" given`);
}
if (field.type === schema_1.Model.RelationType.ManyHasMany && targetField.type !== schema_1.Model.RelationType.ManyHasMany) {
return errors.add(`${relationDescription} "ManyHasMany" type expected, "${targetField.type}" given`);
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} "ManyHasMany" type expected, "${targetField.type}" given`);
}

@@ -164,3 +164,3 @@ }

if ('joiningColumn' in field) {
return errors.add(`View entity ${targetEntity.name} cannot be referenced from an owning relation. Try switching the owning side.`);
return errors.add('MODEL_INVALID_VIEW_USAGE', `View entity ${targetEntity.name} cannot be referenced from an owning relation. Try switching the owning side.`);
}

@@ -172,27 +172,27 @@ }

if (!targetField) {
return errors.add(`${relationDescription} not exists`);
return errors.add('MODEL_UNDEFINED_FIELD', `${relationDescription} not exists`);
}
if (!isRelation(targetField)) {
return errors.add(`${relationDescription} not a relation`);
return errors.add('MODEL_RELATION_REQUIRED', `${relationDescription} not a relation`);
}
if (targetField.target !== entityName) {
return errors.add(`${relationDescription} back reference to entity ${entityName} expected, but ${targetField.target} given`);
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference to entity ${entityName} expected, but ${targetField.target} given`);
}
if (!(0, model_1.isInverseRelation)(targetField)) {
return errors.add(`${relationDescription} not an inverse relation`);
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} not an inverse relation`);
}
if (!targetField.ownedBy) {
return errors.add(`${relationDescription} owning relation is not set`);
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} owning relation is not set`);
}
if (targetField.ownedBy !== field.name) {
return errors.add(`${relationDescription} back reference ${entityName}::${field.name} exepcted, ${targetField.target}::${targetField.ownedBy} given`);
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference ${entityName}::${field.name} exepcted, ${targetField.target}::${targetField.ownedBy} given`);
}
if (field.type === schema_1.Model.RelationType.OneHasOne && targetField.type !== schema_1.Model.RelationType.OneHasOne) {
return errors.add(`${relationDescription} "OneHasOne" type expected, "${targetField.type}" given`);
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} "OneHasOne" type expected, "${targetField.type}" given`);
}
if (field.type === schema_1.Model.RelationType.ManyHasOne && targetField.type !== schema_1.Model.RelationType.OneHasMany) {
return errors.add(`${relationDescription} "ManyHasOne" type expected, "${targetField.type}" given`);
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} "ManyHasOne" type expected, "${targetField.type}" given`);
}
if (field.type === schema_1.Model.RelationType.ManyHasMany && targetField.type !== schema_1.Model.RelationType.ManyHasMany) {
return errors.add(`${relationDescription} "ManyHasMany" type expected, "${targetField.type}" given`);
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} "ManyHasMany" type expected, "${targetField.type}" given`);
}

@@ -204,6 +204,6 @@ }

if (!value.match(IDENTIFIER_PATTERN)) {
return errorBuilder.add(`${value} must match pattern ${IDENTIFIER_PATTERN.source}`);
return errorBuilder.add('MODEL_INVALID_IDENTIFIER', `${value} must match pattern ${IDENTIFIER_PATTERN.source}`);
}
if (RESERVED_WORDS.includes(value)) {
errorBuilder.add(`${value} is reserved word`);
errorBuilder.add('MODEL_INVALID_IDENTIFIER', `${value} is reserved word`);
}

@@ -214,2 +214,3 @@ }

const aliasedTypes = new Map();
const entityNames = new Set(entities.map(it => it.name));
for (const entity of entities) {

@@ -220,3 +221,3 @@ const description = `table name ${entity.tableName} of entity ${entity.name}`;

.for(entity.name)
.add(`${description} collides with a ${relationNames[entity.tableName]}`);
.add('MODEL_NAME_COLLISION', `${description} collides with a ${relationNames[entity.tableName]}`);
}

@@ -226,2 +227,9 @@ else {

}
if (entity.name.endsWith('Meta')) {
const baseName = entity.name.substring(0, entity.name.length - 4);
if (entityNames.has(baseName)) {
errorBuilder.for(entity.name)
.add('MODEL_NAME_COLLISION', `entity ${entity.name} collides with entity ${baseName}, because a GraphQL type with "Meta" suffix is created for every entity`);
}
}
}

@@ -237,3 +245,3 @@ for (const entity of entities) {

.for(relation.name)
.add(`${description} collides with a ${relationNames[joiningTable.tableName]}.` +
.add('MODEL_NAME_COLLISION', `${description} collides with a ${relationNames[joiningTable.tableName]}.` +
'Consider using plural for a relation name or change the joining table name using .joiningTable(...) in schema definition.');

@@ -252,3 +260,3 @@ }

.for(column.name)
.add(`Type alias ${column.typeAlias} already exists for base type ${column.type}`);
.add('MODEL_NAME_COLLISION', `Type alias ${column.typeAlias} already exists for base type ${column.type}`);
}

@@ -269,3 +277,3 @@ aliasedTypes.set(column.typeAlias, column.type);

if (relationNames[index.name]) {
entityErrorBuilder.add(`${description} collides with ${relationNames[index.name]}`);
entityErrorBuilder.add('MODEL_NAME_COLLISION', `${description} collides with ${relationNames[index.name]}`);
}

@@ -279,3 +287,3 @@ else {

if (relationNames[unique.name]) {
entityErrorBuilder.add(`${description} collides with ${relationNames[unique.name]}`);
entityErrorBuilder.add('MODEL_NAME_COLLISION', `${description} collides with ${relationNames[unique.name]}`);
}

@@ -282,0 +290,0 @@ else {

import { Schema } from '@contember/schema';
import { ValidationError } from './errors';
import { ValidationError, ValidationErrorCode } from './errors';
export interface SchemaValidatorSkippedErrors {
code: ValidationErrorCode;
path?: string;
}
export declare class SchemaValidator {
static validate(schema: Schema): ValidationError[];
static validate(schema: Schema, skippedErrors?: SchemaValidatorSkippedErrors[]): ValidationError[];
}
//# sourceMappingURL=SchemaValidator.d.ts.map

@@ -8,3 +8,3 @@ "use strict";

class SchemaValidator {
static validate(schema) {
static validate(schema, skippedErrors = []) {
const modelValidator = new ModelValidator_1.ModelValidator(schema.model);

@@ -16,3 +16,8 @@ const modelErrors = modelValidator.validate();

const validationErrors = validationValidator.validate(schema.validation);
return [...aclErrors, ...modelErrors, ...validationErrors];
const allErrors = [...aclErrors, ...modelErrors, ...validationErrors];
if (skippedErrors.length === 0) {
return allErrors;
}
return allErrors.filter(err => !skippedErrors.some(rule => err.code === rule.code
&& (!rule.path || err.path.join('.') === rule.path)));
}

@@ -19,0 +24,0 @@ }

@@ -16,3 +16,3 @@ "use strict";

if (!entity) {
entityErrorBuilder.add('Entity not found');
entityErrorBuilder.add('VALIDATION_UNDEFINED_ENTITY', 'Entity not found');
}

@@ -30,3 +30,3 @@ else {

if (!field) {
fieldErrorBuilder.add('Field not found');
fieldErrorBuilder.add('VALIDATION_UNDEFINED_FIELD', 'Field not found');
}

@@ -52,3 +52,3 @@ else {

if (errorMessage) {
return errorBuilder.add(errorMessage);
return errorBuilder.add('VALIDATION_NOT_IMPLEMENTED', errorMessage);
}

@@ -105,3 +105,3 @@ this.validateValidator(errorBuilder.for('validator'), rule.validator, entity, field);

if (isRelation) {
errorBuilder.add('Rules depending on relations are currently not supported.');
errorBuilder.add('VALIDATION_NOT_IMPLEMENTED', 'Rules depending on relations are currently not supported.');
return undefined;

@@ -108,0 +108,0 @@ }

@@ -6,57 +6,58 @@ "use strict";

const src_1 = require("../../../src");
const model = {
enums: {},
entities: {
Foo: {
fields: {
id: {
columnName: 'id',
name: 'id',
nullable: false,
type: schema_1.Model.ColumnType.Uuid,
columnType: 'uuid',
(0, vitest_1.test)('index name collision', () => {
const model = {
enums: {},
entities: {
Foo: {
fields: {
id: {
columnName: 'id',
name: 'id',
nullable: false,
type: schema_1.Model.ColumnType.Uuid,
columnType: 'uuid',
},
},
name: 'Foo',
primary: 'id',
primaryColumn: 'id',
tableName: 'foo',
unique: {},
eventLog: {
enabled: true,
},
indexes: {
test: { fields: ['id'], name: 'test' },
},
},
name: 'Foo',
primary: 'id',
primaryColumn: 'id',
tableName: 'foo',
unique: {},
eventLog: {
enabled: true,
},
indexes: {
test: { fields: ['id'], name: 'test' },
},
},
Bar: {
fields: {
id: {
columnName: 'id',
name: 'id',
nullable: false,
type: schema_1.Model.ColumnType.Uuid,
columnType: 'uuid',
Bar: {
fields: {
id: {
columnName: 'id',
name: 'id',
nullable: false,
type: schema_1.Model.ColumnType.Uuid,
columnType: 'uuid',
},
},
name: 'Bar',
primary: 'id',
primaryColumn: 'id',
tableName: 'bar',
unique: {
test: { fields: ['id'], name: 'test' },
},
eventLog: {
enabled: true,
},
indexes: {
foo: { fields: ['id'], name: 'foo' },
},
},
name: 'Bar',
primary: 'id',
primaryColumn: 'id',
tableName: 'bar',
unique: {
test: { fields: ['id'], name: 'test' },
},
eventLog: {
enabled: true,
},
indexes: {
foo: { fields: ['id'], name: 'foo' },
},
},
},
};
(0, vitest_1.test)('index name collision', () => {
};
const validator = new src_1.ModelValidator(model);
vitest_1.assert.deepStrictEqual(validator.validate(), [
{
code: 'MODEL_NAME_COLLISION',
message: 'index name foo of entity Bar collides with table name foo of entity Foo',

@@ -66,2 +67,3 @@ path: ['entities', 'Bar'],

{
code: 'MODEL_NAME_COLLISION',
message: 'unique index name test of entity Bar collides with index name test of entity Foo',

@@ -72,2 +74,71 @@ path: ['entities', 'Bar'],

});
(0, vitest_1.test)('"meta" collision', () => {
const model = {
enums: {},
entities: {
Foo: {
fields: {
id: {
columnName: 'id',
name: 'id',
nullable: false,
type: schema_1.Model.ColumnType.Uuid,
columnType: 'uuid',
},
},
name: 'Foo',
primary: 'id',
primaryColumn: 'id',
tableName: 'foo',
unique: {},
eventLog: { enabled: true },
indexes: {},
},
FooMeta: {
fields: {
id: {
columnName: 'id',
name: 'id',
nullable: false,
type: schema_1.Model.ColumnType.Uuid,
columnType: 'uuid',
},
},
name: 'FooMeta',
primary: 'id',
primaryColumn: 'id',
tableName: 'foo_meta',
unique: {},
eventLog: { enabled: true },
indexes: {},
},
BarMeta: {
fields: {
id: {
columnName: 'id',
name: 'id',
nullable: false,
type: schema_1.Model.ColumnType.Uuid,
columnType: 'uuid',
},
},
name: 'BarMeta',
primary: 'id',
primaryColumn: 'id',
tableName: 'bar',
unique: {},
eventLog: { enabled: true },
indexes: {},
},
},
};
const validator = new src_1.ModelValidator(model);
vitest_1.assert.deepStrictEqual(validator.validate(), [
{
code: 'MODEL_NAME_COLLISION',
message: 'entity FooMeta collides with entity Foo, because a GraphQL type with "Meta" suffix is created for every entity',
path: ['entities', 'FooMeta'],
},
]);
});
//# sourceMappingURL=modelValidator.test.js.map
{
"name": "@contember/schema-utils",
"version": "1.2.0-alpha.18",
"version": "1.2.0-alpha.19",
"license": "Apache-2.0",

@@ -11,4 +11,4 @@ "main": "dist/src/index.js",

"dependencies": {
"@contember/schema": "^1.2.0-alpha.18",
"@contember/typesafe": "^1.2.0-alpha.18"
"@contember/schema": "^1.2.0-alpha.19",
"@contember/typesafe": "^1.2.0-alpha.19"
},

@@ -15,0 +15,0 @@ "devDependencies": {

import { Schema } from '@contember/schema'
import { emptyModelSchema } from './model'
import * as Typesafe from '@contember/typesafe'
import { aclSchema, modelSchema, validationSchema } from './type-schema'
import { aclSchema, modelSchema, validationSchema, settingsSchema } from './type-schema'

@@ -18,2 +18,3 @@ export * from './model'

validation: {},
settings: {},
}

@@ -25,2 +26,3 @@

validation: validationSchema,
settings: settingsSchema,
})

@@ -1,2 +0,2 @@

import crypto from 'crypto'
import crypto from 'node:crypto'

@@ -3,0 +3,0 @@ export class NamingHelper {

export * from './acl'
export * from './condition'
export * from './model'
export * from './settings'
export * from './validation'

@@ -5,4 +5,3 @@ import { Acl, Model } from '@contember/schema'

import { ErrorBuilder, ValidationError } from './errors'
import { conditionSchema } from '../type-schema/condition'
import { anyJson } from '@contember/typesafe'
import { conditionSchema } from '../type-schema'

@@ -46,3 +45,3 @@

if (!roles.includes(inheritsFrom)) {
errorBuilder.for(inheritsFrom).add('Referenced role not exists.')
errorBuilder.for(inheritsFrom).add('ACL_UNDEFINED_ROLE', 'Referenced role not exists.')
}

@@ -65,3 +64,3 @@ }

if (!this.model.entities[variable.entityName]) {
errorBuilder.add(`Entity "${variable.entityName}" not found`)
errorBuilder.add('ACL_UNDEFINED_ENTITY', `Entity "${variable.entityName}" not found`)
return

@@ -79,3 +78,3 @@ }

if (!this.model.entities[entityName]) {
errorBuilder.add(`Entity ${entityName} not found`)
errorBuilder.add('ACL_UNDEFINED_ENTITY', `Entity ${entityName} not found`)
continue

@@ -139,3 +138,3 @@ }

if (!variables[ctx.value]) {
errorBuilder.for(...ctx.path).add(`Undefined variable ${ctx.value}`)
errorBuilder.for(...ctx.path).add('ACL_UNDEFINED_VARIABLE', `Undefined variable ${ctx.value}`)
}

@@ -146,3 +145,3 @@ } else {

} catch (e: any) {
errorBuilder.for(...ctx.path).add(`Invalid condition (${e.message}): ${JSON.stringify(ctx.value)}`)
errorBuilder.for(...ctx.path).add('ACL_INVALID_CONDITION', `Invalid condition (${e.message}): ${JSON.stringify(ctx.value)}`)
}

@@ -154,3 +153,3 @@ }

if (typeof ctx.value === 'string' && !variables[ctx.value]) {
errorBuilder.for(...ctx.path).add(`Undefined variable ${ctx.value}`)
errorBuilder.for(...ctx.path).add('ACL_UNDEFINED_VARIABLE', `Undefined variable ${ctx.value}`)
}

@@ -160,3 +159,3 @@ return ctx.value

handleUndefinedField: ctx => {
errorBuilder.for(...ctx.path).add(`Undefined field ${ctx.name} on entity ${ctx.entity.name}`)
errorBuilder.for(...ctx.path).add('ACL_UNDEFINED_FIELD', `Undefined field ${ctx.name} on entity ${ctx.entity.name}`)
return ctx.value

@@ -195,3 +194,3 @@ },

if (!entity.fields[field]) {
errorBuilder.add(`Field ${field} not found on entity ${entity.name}`)
errorBuilder.add('ACL_UNDEFINED_FIELD', `Field ${field} not found on entity ${entity.name}`)
}

@@ -211,5 +210,5 @@ this.validatePredicate(permissions[field], predicates, errorBuilder.for(field))

if (!predicates[predicate]) {
errorBuilder.add(`Predicate ${predicate} not found`)
errorBuilder.add('ACL_UNDEFINED_PREDICATE', `Predicate ${predicate} not found`)
}
}
}

@@ -0,4 +1,27 @@

export type ValidationErrorCode =
| 'MODEL_NAME_MISMATCH'
| 'MODEL_UNDEFINED_FIELD'
| 'MODEL_UNDEFINED_ENTITY'
| 'MODEL_RELATION_REQUIRED'
| 'MODEL_INVALID_RELATION_DEFINITION'
| 'MODEL_INVALID_COLUMN_DEFINITION'
| 'MODEL_INVALID_VIEW_USAGE'
| 'MODEL_INVALID_IDENTIFIER'
| 'MODEL_NAME_COLLISION'
| 'ACL_INVALID_CONDITION'
| 'ACL_UNDEFINED_VARIABLE'
| 'ACL_UNDEFINED_FIELD'
| 'ACL_UNDEFINED_PREDICATE'
| 'ACL_UNDEFINED_ROLE'
| 'ACL_UNDEFINED_ENTITY'
| 'VALIDATION_UNDEFINED_ENTITY'
| 'VALIDATION_UNDEFINED_FIELD'
| 'VALIDATION_NOT_IMPLEMENTED'
export interface ValidationError {
path: (string | number)[]
message: string
code: ValidationErrorCode
}

@@ -13,5 +36,5 @@

add(message: string): void {
this.errors.push({ path: this.path, message })
add(code: ValidationErrorCode, message: string): void {
this.errors.push({ path: this.path, message, code })
}
}

@@ -33,3 +33,3 @@ import { Model } from '@contember/schema'

if (entity.name !== entityName) {
entityErrors.add(`Entity name "${entity.name}" does not match the name in a map "${entityName}"`)
entityErrors.add('MODEL_NAME_MISMATCH', `Entity name "${entity.name}" does not match the name in a map "${entityName}"`)
}

@@ -47,3 +47,3 @@ this.validateEntity(entity, entityErrors)

if (field.name !== fieldName) {
fieldErrors.add(`Field name "${field.name}" does not match the name in a map "${fieldName}"`)
fieldErrors.add('MODEL_NAME_MISMATCH', `Field name "${field.name}" does not match the name in a map "${fieldName}"`)
}

@@ -62,3 +62,3 @@ }

if (constraint.name !== constraintName) {
uniqueErrors.add(`Constraint name ${constraint.name} does not match the name in a map "${constraintName}"`)
uniqueErrors.add('MODEL_NAME_MISMATCH', `Constraint name ${constraint.name} does not match the name in a map "${constraintName}"`)
continue

@@ -68,3 +68,3 @@ }

if (!fields.has(field)) {
uniqueErrors.add(`Referenced field ${field} in a constraint does not exists`)
uniqueErrors.add('MODEL_UNDEFINED_FIELD', `Referenced field ${field} in a constraint does not exists`)
}

@@ -81,3 +81,3 @@ }

if (field.sequence && field.nullable) {
errors.add('Column with sequence cannot be nullable.')
errors.add('MODEL_INVALID_COLUMN_DEFINITION', 'Column with sequence cannot be nullable.')
}

@@ -92,3 +92,3 @@ }

if (!targetEntity) {
return errors.add(`Target entity ${targetEntityName} not found`)
return errors.add('MODEL_UNDEFINED_ENTITY', `Target entity ${targetEntityName} not found`)
}

@@ -103,3 +103,3 @@ if (((it: Model.AnyRelation): it is Model.AnyRelation & Model.OrderableRelation => 'orderBy' in it)(field)) {

if (!orderByField) {
errors.add(`Invalid orderBy of ${entityName}::${field.name}: field ${pathStr} is not defined`)
errors.add('ACL_UNDEFINED_FIELD', `Invalid orderBy of ${entityName}::${field.name}: field ${pathStr} is not defined`)
return

@@ -110,3 +110,3 @@ }

if (!targetEntity) {
errors.add(`Invalid orderBy of ${entityName}::${field.name}: field ${pathStr} is not a relation`)
errors.add('MODEL_RELATION_REQUIRED', `Invalid orderBy of ${entityName}::${field.name}: field ${pathStr} is not a relation`)
return

@@ -121,3 +121,3 @@ }

if (field.type === Model.RelationType.ManyHasMany) {
return errors.add('Many-has-many relation is not allowed on a view entity.')
return errors.add('MODEL_INVALID_VIEW_USAGE', 'Many-has-many relation is not allowed on a view entity.')
}

@@ -128,3 +128,3 @@ if (

) {
return errors.add('One-has-many relation fields on views must point to a view entity.')
return errors.add('MODEL_INVALID_VIEW_USAGE', 'One-has-many relation fields on views must point to a view entity.')
}

@@ -136,3 +136,3 @@ if (

) {
return errors.add('One-has-one relation fields on views must be owning or point to a view entity.')
return errors.add('MODEL_INVALID_VIEW_USAGE', 'One-has-one relation fields on views must be owning or point to a view entity.')
}

@@ -145,29 +145,29 @@ }

if (!targetField) {
return errors.add(`${relationDescription} not exists`)
return errors.add('MODEL_UNDEFINED_FIELD', `${relationDescription} not exists`)
}
if (!isRelation(targetField)) {
return errors.add(`${relationDescription} not a relation`)
return errors.add('MODEL_RELATION_REQUIRED', `${relationDescription} not a relation`)
}
if (targetField.target !== entityName) {
return errors.add(`${relationDescription} back reference to entity ${entityName} expected, but ${targetField.target} given`)
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference to entity ${entityName} expected, but ${targetField.target} given`)
}
if (!isOwningRelation(targetField)) {
errors.add(`${relationDescription} not an owning relation`)
errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} not an owning relation`)
return
}
if (!targetField.inversedBy) {
return errors.add(`${relationDescription} inverse relation is not set`)
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} inverse relation is not set`)
}
if (targetField.inversedBy !== field.name) {
return errors.add(`${relationDescription} back reference ${entityName}::${field.name} exepcted, ${targetField.target}::${targetField.inversedBy} given`)
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference ${entityName}::${field.name} exepcted, ${targetField.target}::${targetField.inversedBy} given`)
}
if (field.type === Model.RelationType.OneHasOne && targetField.type !== Model.RelationType.OneHasOne) {
return errors.add(`${relationDescription} "OneHasOne" type expected, "${targetField.type}" given`)
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} "OneHasOne" type expected, "${targetField.type}" given`)
}
if (field.type === Model.RelationType.OneHasMany && targetField.type !== Model.RelationType.ManyHasOne) {
return errors.add(`${relationDescription} "ManyHasOne" type expected, "${targetField.type}" given`)
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} "ManyHasOne" type expected, "${targetField.type}" given`)
}
if (field.type === Model.RelationType.ManyHasMany && targetField.type !== Model.RelationType.ManyHasMany) {
return errors.add(`${relationDescription} "ManyHasMany" type expected, "${targetField.type}" given`)
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} "ManyHasMany" type expected, "${targetField.type}" given`)
}

@@ -178,3 +178,3 @@ } else {

if ('joiningColumn' in field) {
return errors.add(`View entity ${targetEntity.name} cannot be referenced from an owning relation. Try switching the owning side.`)
return errors.add('MODEL_INVALID_VIEW_USAGE', `View entity ${targetEntity.name} cannot be referenced from an owning relation. Try switching the owning side.`)
}

@@ -186,27 +186,27 @@ }

if (!targetField) {
return errors.add(`${relationDescription} not exists`)
return errors.add('MODEL_UNDEFINED_FIELD', `${relationDescription} not exists`)
}
if (!isRelation(targetField)) {
return errors.add(`${relationDescription} not a relation`)
return errors.add('MODEL_RELATION_REQUIRED', `${relationDescription} not a relation`)
}
if (targetField.target !== entityName) {
return errors.add(`${relationDescription} back reference to entity ${entityName} expected, but ${targetField.target} given`)
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference to entity ${entityName} expected, but ${targetField.target} given`)
}
if (!isInverseRelation(targetField)) {
return errors.add(`${relationDescription} not an inverse relation`)
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} not an inverse relation`)
}
if (!targetField.ownedBy) {
return errors.add(`${relationDescription} owning relation is not set`)
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} owning relation is not set`)
}
if (targetField.ownedBy !== field.name) {
return errors.add(`${relationDescription} back reference ${entityName}::${field.name} exepcted, ${targetField.target}::${targetField.ownedBy} given`)
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} back reference ${entityName}::${field.name} exepcted, ${targetField.target}::${targetField.ownedBy} given`)
}
if (field.type === Model.RelationType.OneHasOne && targetField.type !== Model.RelationType.OneHasOne) {
return errors.add(`${relationDescription} "OneHasOne" type expected, "${targetField.type}" given`)
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} "OneHasOne" type expected, "${targetField.type}" given`)
}
if (field.type === Model.RelationType.ManyHasOne && targetField.type !== Model.RelationType.OneHasMany) {
return errors.add(`${relationDescription} "ManyHasOne" type expected, "${targetField.type}" given`)
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} "ManyHasOne" type expected, "${targetField.type}" given`)
}
if (field.type === Model.RelationType.ManyHasMany && targetField.type !== Model.RelationType.ManyHasMany) {
return errors.add(`${relationDescription} "ManyHasMany" type expected, "${targetField.type}" given`)
return errors.add('MODEL_INVALID_RELATION_DEFINITION', `${relationDescription} "ManyHasMany" type expected, "${targetField.type}" given`)
}

@@ -219,6 +219,6 @@ }

if (!value.match(IDENTIFIER_PATTERN)) {
return errorBuilder.add(`${value} must match pattern ${IDENTIFIER_PATTERN.source}`)
return errorBuilder.add('MODEL_INVALID_IDENTIFIER', `${value} must match pattern ${IDENTIFIER_PATTERN.source}`)
}
if (RESERVED_WORDS.includes(value)) {
errorBuilder.add(`${value} is reserved word`)
errorBuilder.add('MODEL_INVALID_IDENTIFIER', `${value} is reserved word`)
}

@@ -230,2 +230,3 @@ }

const aliasedTypes = new Map<string, Model.ColumnType>()
const entityNames = new Set(entities.map(it => it.name))
for (const entity of entities) {

@@ -236,6 +237,14 @@ const description = `table name ${entity.tableName} of entity ${entity.name}`

.for(entity.name)
.add(`${description} collides with a ${relationNames[entity.tableName]}`)
.add('MODEL_NAME_COLLISION', `${description} collides with a ${relationNames[entity.tableName]}`)
} else {
relationNames[entity.tableName] = description
}
if (entity.name.endsWith('Meta')) {
const baseName = entity.name.substring(0, entity.name.length - 4)
if (entityNames.has(baseName)) {
errorBuilder.for(entity.name)
.add('MODEL_NAME_COLLISION', `entity ${entity.name} collides with entity ${baseName}, because a GraphQL type with "Meta" suffix is created for every entity`)
}
}
}

@@ -251,3 +260,3 @@ for (const entity of entities) {

.for(relation.name)
.add(
.add('MODEL_NAME_COLLISION',
`${description} collides with a ${relationNames[joiningTable.tableName]}.` +

@@ -267,3 +276,3 @@ 'Consider using plural for a relation name or change the joining table name using .joiningTable(...) in schema definition.',

.for(column.name)
.add(`Type alias ${column.typeAlias} already exists for base type ${column.type}`)
.add('MODEL_NAME_COLLISION', `Type alias ${column.typeAlias} already exists for base type ${column.type}`)
}

@@ -284,3 +293,3 @@ aliasedTypes.set(column.typeAlias, column.type)

if (relationNames[index.name]) {
entityErrorBuilder.add(`${description} collides with ${relationNames[index.name]}`)
entityErrorBuilder.add('MODEL_NAME_COLLISION', `${description} collides with ${relationNames[index.name]}`)
} else {

@@ -293,3 +302,3 @@ relationNames[index.name] = description

if (relationNames[unique.name]) {
entityErrorBuilder.add(`${description} collides with ${relationNames[unique.name]}`)
entityErrorBuilder.add('MODEL_NAME_COLLISION', `${description} collides with ${relationNames[unique.name]}`)
} else {

@@ -296,0 +305,0 @@ relationNames[unique.name] = description

import { Schema } from '@contember/schema'
import { ValidationError } from './errors'
import { ValidationError, ValidationErrorCode } from './errors'
import { AclValidator } from './AclValidator'

@@ -7,4 +7,9 @@ import { ModelValidator } from './ModelValidator'

export interface SchemaValidatorSkippedErrors {
code: ValidationErrorCode
path?: string
}
export class SchemaValidator {
public static validate(schema: Schema): ValidationError[] {
public static validate(schema: Schema, skippedErrors: SchemaValidatorSkippedErrors[] = []): ValidationError[] {
const modelValidator = new ModelValidator(schema.model)

@@ -19,5 +24,13 @@ const modelErrors = modelValidator.validate()

const validationErrors = validationValidator.validate(schema.validation)
return [...aclErrors, ...modelErrors, ...validationErrors]
const allErrors = [...aclErrors, ...modelErrors, ...validationErrors]
if (skippedErrors.length === 0) {
return allErrors
}
return allErrors.filter(
err => !skippedErrors.some(
rule => err.code === rule.code
&& (!rule.path || err.path.join('.') === rule.path),
),
)
}
}

@@ -14,3 +14,3 @@ import { Model, Validation } from '@contember/schema'

if (!entity) {
entityErrorBuilder.add('Entity not found')
entityErrorBuilder.add('VALIDATION_UNDEFINED_ENTITY', 'Entity not found')
} else {

@@ -32,3 +32,3 @@ this.validateEntityRules(entityErrorBuilder, schema[entityName], entity)

if (!field) {
fieldErrorBuilder.add('Field not found')
fieldErrorBuilder.add('VALIDATION_UNDEFINED_FIELD', 'Field not found')
} else {

@@ -65,3 +65,3 @@ this.validateFieldRules(fieldErrorBuilder, entitySchema[fieldName], entity, field)

if (errorMessage) {
return errorBuilder.add(errorMessage)
return errorBuilder.add('VALIDATION_NOT_IMPLEMENTED', errorMessage)
}

@@ -152,3 +152,3 @@ this.validateValidator(errorBuilder.for('validator'), rule.validator, entity, field)

if (isRelation) {
errorBuilder.add('Rules depending on relations are currently not supported.')
errorBuilder.add('VALIDATION_NOT_IMPLEMENTED', 'Rules depending on relations are currently not supported.')
return undefined

@@ -155,0 +155,0 @@ }

@@ -6,57 +6,59 @@ import { assert, test } from 'vitest'

const model: Model.Schema = {
enums: {},
entities: {
Foo: {
fields: {
id: {
columnName: 'id',
name: 'id',
nullable: false,
type: Model.ColumnType.Uuid,
columnType: 'uuid',
test('index name collision', () => {
const model: Model.Schema = {
enums: {},
entities: {
Foo: {
fields: {
id: {
columnName: 'id',
name: 'id',
nullable: false,
type: Model.ColumnType.Uuid,
columnType: 'uuid',
},
},
name: 'Foo',
primary: 'id',
primaryColumn: 'id',
tableName: 'foo',
unique: {},
eventLog: {
enabled: true,
},
indexes: {
test: { fields: ['id'], name: 'test' },
},
},
name: 'Foo',
primary: 'id',
primaryColumn: 'id',
tableName: 'foo',
unique: {},
eventLog: {
enabled: true,
},
indexes: {
test: { fields: ['id'], name: 'test' },
},
},
Bar: {
fields: {
id: {
columnName: 'id',
name: 'id',
nullable: false,
type: Model.ColumnType.Uuid,
columnType: 'uuid',
Bar: {
fields: {
id: {
columnName: 'id',
name: 'id',
nullable: false,
type: Model.ColumnType.Uuid,
columnType: 'uuid',
},
},
name: 'Bar',
primary: 'id',
primaryColumn: 'id',
tableName: 'bar',
unique: {
test: { fields: ['id'], name: 'test' },
},
eventLog: {
enabled: true,
},
indexes: {
foo: { fields: ['id'], name: 'foo' },
},
},
name: 'Bar',
primary: 'id',
primaryColumn: 'id',
tableName: 'bar',
unique: {
test: { fields: ['id'], name: 'test' },
},
eventLog: {
enabled: true,
},
indexes: {
foo: { fields: ['id'], name: 'foo' },
},
},
},
}
test('index name collision', () => {
}
const validator = new ModelValidator(model)
assert.deepStrictEqual(validator.validate(), [
{
code: 'MODEL_NAME_COLLISION',
message: 'index name foo of entity Bar collides with table name foo of entity Foo',

@@ -66,2 +68,3 @@ path: ['entities', 'Bar'],

{
code: 'MODEL_NAME_COLLISION',
message: 'unique index name test of entity Bar collides with index name test of entity Foo',

@@ -72,1 +75,72 @@ path: ['entities', 'Bar'],

})
test('"meta" collision', () => {
const model: Model.Schema = {
enums: {},
entities: {
Foo: {
fields: {
id: {
columnName: 'id',
name: 'id',
nullable: false,
type: Model.ColumnType.Uuid,
columnType: 'uuid',
},
},
name: 'Foo',
primary: 'id',
primaryColumn: 'id',
tableName: 'foo',
unique: {},
eventLog: { enabled: true },
indexes: {},
},
FooMeta: {
fields: {
id: {
columnName: 'id',
name: 'id',
nullable: false,
type: Model.ColumnType.Uuid,
columnType: 'uuid',
},
},
name: 'FooMeta',
primary: 'id',
primaryColumn: 'id',
tableName: 'foo_meta',
unique: {},
eventLog: { enabled: true },
indexes: {},
},
BarMeta: {
fields: {
id: {
columnName: 'id',
name: 'id',
nullable: false,
type: Model.ColumnType.Uuid,
columnType: 'uuid',
},
},
name: 'BarMeta',
primary: 'id',
primaryColumn: 'id',
tableName: 'bar',
unique: {},
eventLog: { enabled: true },
indexes: {},
},
},
}
const validator = new ModelValidator(model)
assert.deepStrictEqual(validator.validate(), [
{
code: 'MODEL_NAME_COLLISION',
message: 'entity FooMeta collides with entity Foo, because a GraphQL type with "Meta" suffix is created for every entity',
path: ['entities', 'FooMeta'],
},
])
})

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc