feat: improve monorepo dev experience

This commit is contained in:
2023-10-27 22:45:44 +02:00
parent f618725598
commit c6ab4ae48b
124 changed files with 2647 additions and 2857 deletions

View File

@@ -43,7 +43,7 @@
"test:unit": "cross-env NODE_CONFIG_ENV=elasticsearch ALLOW_NO_TRANSPORT=true STAPPS_LOG_LEVEL=0 mocha --exit"
},
"dependencies": {
"@elastic/elasticsearch": "8.4.0",
"@elastic/elasticsearch": "8.10.0",
"@openstapps/core": "workspace:*",
"@openstapps/core-tools": "workspace:*",
"@openstapps/logger": "workspace:*",
@@ -56,6 +56,8 @@
"@types/nodemailer": "6.4.7",
"@types/promise-queue": "2.2.0",
"@types/uuid": "8.3.4",
"ajv": "8.12.0",
"ajv-formats": "2.1.1",
"body-parser": "1.20.2",
"cors": "2.8.5",
"cosmiconfig": "8.1.3",
@@ -102,16 +104,6 @@
"tsup": "6.7.0",
"typescript": "5.1.6"
},
"tsup": {
"entry": [
"src/cli.ts"
],
"sourcemap": true,
"clean": true,
"target": "es2022",
"format": "esm",
"outDir": "lib"
},
"prettier": "@openstapps/prettier-config",
"eslintConfig": {
"extends": [

View File

@@ -14,6 +14,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {
SCConfigFile,
SCNotFoundErrorResponse,
SCRequestBodyTooLargeErrorResponse,
SCSyntaxErrorResponse,
@@ -23,8 +24,7 @@ import {Logger} from '@openstapps/logger';
import cors from 'cors';
import {Express} from 'express';
import morgan from 'morgan';
import path from 'path';
import {DEFAULT_TIMEOUT, isTestEnvironment, mailer, plugins, validator} from './common.js';
import {DEFAULT_TIMEOUT, isTestEnvironment, mailer, plugins} from './common.js';
import {getPrometheusMiddleware} from './middleware/prometheus.js';
import {MailQueue} from './notification/mail-queue.js';
import {bulkAddRouter} from './routes/bulk-add-route.js';
@@ -39,7 +39,7 @@ import {virtualPluginRoute} from './routes/virtual-plugin-route.js';
import {BulkStorage} from './storage/bulk-storage.js';
import {DatabaseConstructor} from './storage/database.js';
import {backendConfig} from './config.js';
import {fileURLToPath} from 'url';
import {createValidator} from './validator.js';
/**
* Configure the backend
@@ -143,19 +143,10 @@ export async function configureApp(app: Express, databases: {[name: string]: Dat
request.on('data', chunkGatherer).on('end', endCallback);
});
// validate config file
const directory = path.dirname(fileURLToPath(import.meta.url));
await validator.addSchemas(
path.join(directory, '..', 'node_modules', '@openstapps', 'core', 'lib', 'schema'),
);
// validate the config file
const configValidation = validator.validate(backendConfig, 'SCConfigFile');
// validation failed
if (configValidation.errors.length > 0) {
const configFileValid = createValidator<SCConfigFile>('SCConfigFile');
if (!configFileValid(backendConfig)) {
throw new Error(
`Validation of config file failed. Errors were: ${JSON.stringify(configValidation.errors)}`,
`Validation of config file failed. Errors were: ${JSON.stringify(configFileValid.errors)}`,
);
}

View File

@@ -14,7 +14,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {SCPluginMetaData} from '@openstapps/core';
import {Validator} from '@openstapps/core-tools';
import {BackendTransport} from './notification/backend-transport.js';
/**
@@ -22,11 +21,6 @@ import {BackendTransport} from './notification/backend-transport.js';
*/
export const mailer = BackendTransport.getTransportInstance();
/**
* A validator instance to check if something is a valid JSON object (e.g. a request or a thing)
*/
export const validator = new Validator();
/**
* Provides information if the backend is executed in the "test" (non-production) environment
*/

View File

@@ -19,12 +19,12 @@ import {
SCRoute,
SCValidationErrorResponse,
} from '@openstapps/core';
import {ValidationError} from '@openstapps/core-tools/src/types/validator.js';
import {Logger} from '@openstapps/logger';
import {Application, Router} from 'express';
import PromiseRouter from 'express-promise-router';
import {isTestEnvironment, validator} from '../common.js';
import {isTestEnvironment} from '../common.js';
import {isHttpMethod} from './http-types.js';
import {createValidator} from '../validator.js';
/**
* Creates a router from a route class and a handler function which implements the logic
@@ -44,6 +44,8 @@ export function createRoute<REQUESTTYPE, RETURNTYPE>(
): Router {
// create router
const router = PromiseRouter({mergeParams: true});
const requestValidator = createValidator<REQUESTTYPE>(routeClass.requestBodyName);
const responseValidator = createValidator<RETURNTYPE>(routeClass.responseBodyName);
// create route
// the given type has no index signature so we have to cast to get the IRouteHandler when a HTTP method is given
@@ -56,11 +58,8 @@ export function createRoute<REQUESTTYPE, RETURNTYPE>(
// create a route handler for the given HTTP method
route[verb](async (request, response) => {
try {
// validate request
const requestValidation = validator.validate(request.body, routeClass.requestBodyName);
if (requestValidation.errors.length > 0) {
const error = new SCValidationErrorResponse(requestValidation.errors, isTestEnvironment);
if (!requestValidator(request.body)) {
const error = new SCValidationErrorResponse(requestValidator.errors as any, isTestEnvironment);
response.status(error.statusCode);
response.json(error);
await Logger.error(error);
@@ -68,17 +67,13 @@ export function createRoute<REQUESTTYPE, RETURNTYPE>(
return;
}
// hand over request to handler with path parameters
const handlerResponse = await handler(request.body, request.app, request.params);
// validate response generated by handler
const responseErrors: ValidationError[] = validator.validate(
handlerResponse,
routeClass.responseBodyName,
).errors;
if (responseErrors.length > 0) {
const validationError = new SCValidationErrorResponse(responseErrors, isTestEnvironment);
if (!responseValidator(handlerResponse)) {
const validationError = new SCValidationErrorResponse(
responseValidator.errors as any,
isTestEnvironment,
);
// The validation error is not caused by faulty user input, but through an error that originates somewhere in
// the backend, therefore we use this "stacked" error.
const internalServerError = new SCInternalServerErrorResponse(validationError, isTestEnvironment);

View File

@@ -17,8 +17,9 @@
import {SCInternalServerErrorResponse, SCPluginMetaData, SCValidationErrorResponse} from '@openstapps/core';
import {Request} from 'express';
import got from 'got';
import {isTestEnvironment, validator} from '../common.js';
import {isTestEnvironment} from '../common.js';
import {backendConfig} from '../config.js';
import {validator} from '../validator.js';
/**
* Generic route function used to proxy actual requests to plugins
@@ -28,10 +29,9 @@ import {backendConfig} from '../config.js';
*/
export async function virtualPluginRoute(request: Request, plugin: SCPluginMetaData): Promise<object> {
try {
const requestValidation = validator.validate(request.body, plugin.requestSchema);
if (requestValidation.errors.length > 0) {
if (!validator.validate(request.body, plugin.requestSchema)) {
// noinspection ExceptionCaughtLocallyJS
throw new SCValidationErrorResponse(requestValidation.errors, isTestEnvironment);
throw new SCValidationErrorResponse(validator.errors as any, isTestEnvironment);
}
// send the request to the plugin (forward the body) and save the response
const response = await got.post(plugin.route.replaceAll(/^\//gi, ''), {
@@ -43,10 +43,9 @@ export async function virtualPluginRoute(request: Request, plugin: SCPluginMetaD
responseType: 'json',
});
const responseBody = response.body;
const responseValidation = validator.validate(responseBody, plugin.responseSchema);
if (responseValidation.errors.length > 0) {
if (!validator.validate(responseBody, plugin.responseSchema)) {
// noinspection ExceptionCaughtLocallyJS
throw new SCValidationErrorResponse(responseValidation.errors, isTestEnvironment);
throw new SCValidationErrorResponse(validator.errors as any, isTestEnvironment);
}
return responseBody as object;
} catch (error) {

View File

@@ -0,0 +1,25 @@
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import schema from '@openstapps/core?json-schema';
export const validator = new Ajv.default({
schemas: [schema],
verbose: true,
allowUnionTypes: true,
});
addFormats.default(validator, {
formats: ['date-time', 'time', 'uuid', 'duration'],
mode: 'fast',
});
/**
* Create a validator function
* @example
* import schema from '@openstapps/core#schema:SCThings'
* createValidator<SCThings>(schema)
*/
export function createValidator<T>(schemaName: string): Ajv.ValidateFunction<T> {
return validator.compile({
$ref: `#/definitions/${schemaName}`,
});
}

View File

@@ -0,0 +1,10 @@
import {defineConfig} from 'tsup';
export default defineConfig({
entry: ['src/cli.ts'],
sourcemap: true,
clean: true,
target: 'es2022',
format: 'esm',
outDir: 'lib',
});

View File

@@ -40,5 +40,10 @@
"turbo-ignore": "1.10.6",
"typedoc": "0.24.8",
"typescript": "5.1.6"
},
"pnpm": {
"patchedDependencies": {
"@elastic/elasticsearch@8.10.0": "patches/@elastic__elasticsearch@8.10.0.patch"
}
}
}

View File

@@ -43,7 +43,7 @@
"@types/cli-progress": "3.11.0",
"@types/express": "4.17.17",
"@types/fs-extra": "9.0.13",
"@types/json-schema": "7.0.11",
"@types/json-schema": "7.0.14",
"@types/junit-report-builder": "3.0.0",
"@types/mocha": "10.0.1",
"@types/node": "18.15.3",

View File

@@ -38,7 +38,7 @@
"@openstapps/logger": "workspace:*",
"@types/body-parser": "1.19.2",
"@types/express": "4.17.17",
"@types/json-schema": "7.0.11",
"@types/json-schema": "7.0.14",
"@types/morgan": "1.9.4",
"body-parser": "1.20.2",
"express": "4.18.2",

View File

@@ -49,6 +49,7 @@
"@openstapps/easy-ast": "workspace:*",
"@openstapps/logger": "workspace:*",
"ajv": "8.12.0",
"ajv-formats": "2.1.1",
"better-ajv-errors": "1.2.0",
"commander": "10.0.0",
"deepmerge": "4.3.1",
@@ -60,11 +61,10 @@
"humanize-string": "3.0.0",
"json-schema": "0.4.0",
"mustache": "4.2.0",
"openapi-types": "12.1.0",
"openapi-types": "12.1.3",
"plantuml-encoder": "1.4.0",
"re2": "1.18.2",
"toposort": "2.0.2",
"ts-json-schema-generator": "1.2.0"
"ts-json-schema-generator": "1.4.0"
},
"devDependencies": {
"@openstapps/eslint-config": "workspace:*",
@@ -73,7 +73,7 @@
"@types/chai": "4.3.5",
"@types/fs-extra": "9.0.13",
"@types/glob": "8.0.1",
"@types/json-schema": "7.0.11",
"@types/json-schema": "7.0.14",
"@types/mocha": "10.0.1",
"@types/mustache": "4.2.2",
"@types/node": "18.15.3",

View File

@@ -17,15 +17,17 @@ import {Command} from 'commander';
import {existsSync, readFileSync, writeFileSync} from 'fs';
import path from 'path';
import {lightweightDefinitionsFromPath, lightweightProjectFromPath} from '@openstapps/easy-ast';
import {openapi3Template} from './resources/openapi-303-template.js';
import {gatherRouteInformation, generateOpenAPIForRoute} from './routes.js';
import {Converter, getValidatableTypesInPath} from './schema.js';
import {openapi3Template} from '../../openapi-generator/src/openapi-303-template.js';
import {
gatherRouteInformation,
generateOpenAPIForRoute,
} from '../../openapi-generator/src/generator/routes.js';
import {Converter, getValidatableTypesInPath, mergeSchemas} from './schema.js';
import {createDiagram, createDiagramFromString} from './uml/create-diagram.js';
import {UMLConfig} from './uml/uml-config.js';
import {capitalize} from './util/string.js';
import {validateFiles, writeReport} from './validate.js';
import {fileURLToPath} from 'url';
import {mkdir, readFile} from 'fs/promises';
import {readFile} from 'fs/promises';
// handle unhandled promise rejections
process.on('unhandledRejection', async (reason: unknown) => {
@@ -52,56 +54,10 @@ commander.command('prototype <srcBundle> <out>').action(async (sourcePath, out)
commander
.command('openapi <srcPath> <outDirPath>')
.action(async (relativeSourceBundlePath, relativeOutDirectoryPath) => {
// get absolute paths
const sourcePath = path.resolve(relativeSourceBundlePath);
const outDirectoryPath = path.resolve(relativeOutDirectoryPath);
const outDirectorySchemasPath = path.join(outDirectoryPath, 'schema');
.action(async (relativeSourceBundlePath, relativeOutDirectoryPath) => {});
// get information about routes
const routes = await gatherRouteInformation(sourcePath);
routes.sort((a, b) => a.route.urlPath.localeCompare(b.route.urlPath));
// change url path parameters to openapi notation
for (const routeWithMetaInformation of routes) {
routeWithMetaInformation.route.urlPath = routeWithMetaInformation.route.urlPath.replaceAll(
/:\w+/g,
(match: string) => `{${match.replace(':', '')}}`,
);
}
// keep openapi tags for routes that actually share url fragments
let tagsToKeep = routes.map(routeWithMetaInformation =>
capitalize(routeWithMetaInformation.route.urlPath.split('/')[1]),
);
tagsToKeep = tagsToKeep.filter(
(element, i, array) => array.indexOf(element) === i && array.lastIndexOf(element) !== i,
);
// initialize json output
const output = openapi3Template;
// generate documentation for all routes
for (const routeWithMetaInformation of routes) {
routeWithMetaInformation.tags = [capitalize(routeWithMetaInformation.route.urlPath.split('/')[1])];
output.paths[routeWithMetaInformation.route.urlPath] = generateOpenAPIForRoute(
routeWithMetaInformation,
path.relative(relativeOutDirectoryPath, outDirectorySchemasPath),
tagsToKeep,
);
}
// write openapi object to file (prettified)
writeFileSync(path.join(outDirectoryPath, 'openapi.json'), JSON.stringify(output, undefined, 2));
Logger.ok(`OpenAPI representation resources written to ${outDirectoryPath} .`);
});
commander.command('schema <srcPath> <schemaPath>').action(async (relativeSourcePath, relativeSchemaPath) => {
// get absolute paths
commander.command('schema <srcPath> <schemaPath>').action(async (relativeSourcePath, schemaPath) => {
const absoluteSourcePath = path.resolve(relativeSourcePath);
const schemaPath = path.resolve(relativeSchemaPath);
// initialize new core converter
const coreConverter = new Converter(absoluteSourcePath);
@@ -111,10 +67,6 @@ commander.command('schema <srcPath> <schemaPath>').action(async (relativeSourceP
Logger.info(`Found ${validatableTypes.length} type(s) to generate schemas for.`);
await mkdir(schemaPath, {
recursive: true,
});
Logger.info(`Trying to find a package.json for ${absoluteSourcePath}.`);
let packagePath = absoluteSourcePath;
@@ -134,53 +86,12 @@ commander.command('schema <srcPath> <schemaPath>').action(async (relativeSourceP
Logger.log(`Using ${coreVersion} as version for schemas.`);
// generate and write JSONSchema files for validatable types
for (const type of validatableTypes) {
const schema = coreConverter.getSchema(type, coreVersion);
const stringifiedSchema = JSON.stringify(schema, undefined, 2);
const file = path.join(schemaPath, `${type}.json`);
// write schema to file
writeFileSync(file, stringifiedSchema);
Logger.info(`Generated schema for ${type} and saved to ${file}.`);
}
const schema = mergeSchemas(validatableTypes.map(type => coreConverter.getSchema(type, coreVersion)));
writeFileSync(schemaPath, JSON.stringify(schema, undefined, 2));
Logger.ok(`Generated schemas for ${validatableTypes.length} type(s).`);
});
commander
.command('validate <schemaPath> <testPath> [reportPath]')
.action(async (relativeSchemaPath, relativeTestPath, relativeReportPath) => {
// get absolute paths
const schemaPath = path.resolve(relativeSchemaPath);
const testPath = path.resolve(relativeTestPath);
const errorsPerFile = await validateFiles(schemaPath, testPath);
let unexpected = false;
for (const file in errorsPerFile) {
if (!errorsPerFile.hasOwnProperty(file)) {
continue;
}
unexpected = unexpected || errorsPerFile[file].some(error => !error.expected);
}
if (relativeReportPath !== undefined) {
const reportPath = path.resolve(relativeReportPath);
await writeReport(reportPath, errorsPerFile);
}
if (unexpected) {
await Logger.error('Unexpected errors occurred during validation');
process.exit(1);
} else {
Logger.ok('Successfully finished validation.');
}
});
commander
.command('plantuml <srcPath> <plantumlserver>')
.option('--definitions <definitions>', 'Shows these specific definitions (class, interface or enum)', it =>

View File

@@ -1,26 +0,0 @@
declare module 'better-ajv-errors' {
import type {ErrorObject} from 'ajv';
export interface IOutputError {
start: {line: number; column: number; offset: number};
// Optional for required
end?: {line: number; column: number; offset: number};
error: string;
suggestion?: string;
}
export interface IInputOptions {
format?: 'cli' | 'js';
indent?: number | null;
/** Raw JSON used when highlighting error location */
json?: string | null;
}
export default function <S, T, Options extends IInputOptions>(
schema: S,
data: T,
errors: Array<ErrorObject>,
options?: Options,
): Options extends {format: 'js'} ? Array<IOutputError> : string;
}

View File

@@ -1,11 +1,10 @@
export * from './validate.js';
export * from './types/validator.js';
export * from './uml/uml-config.js';
export * from './uml/create-diagram.js';
export * from './routes.js';
export * from './types/routes.js';
export * from '../../openapi-generator/src/generator/routes.js';
export * from '../../openapi-generator/src/generator/types/routes.js';
export * from './schema.js';
export * from './types/schema.js';

View File

@@ -1,161 +0,0 @@
/*
* Copyright (C) 2018-2021 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {OpenAPIV3} from 'openapi-types';
import {
isLightweightClass,
lightweightProjectFromPath,
LightweightProjectWithIndex,
} from '@openstapps/easy-ast';
import {RouteInstanceWithMeta, RouteWithMetaInformation} from './types/routes.js';
import {rejectNil} from './util/collections.js';
import {capitalize} from './util/string.js';
import path from 'path';
/**
* Gather relevant information of routes
*
* This gathers the information for all routes that implement the abstract class SCAbstractRoute.
* Furthermore it instantiates every route and adds it to the information.
*/
export async function gatherRouteInformation(path: string): Promise<RouteWithMetaInformation[]> {
const project = new LightweightProjectWithIndex(lightweightProjectFromPath(path));
// find all classes that implement the SCAbstractRoute
return rejectNil(
await Promise.all(
Object.values(project.definitions)
.filter(isLightweightClass)
.map(async node => {
if (!node.extendedDefinitions?.some(it => it.referenceName === 'SCAbstractRoute')) {
return undefined;
}
const instantiatedRoute = (await project.instantiateDefinitionByName(
node.name,
)) as RouteInstanceWithMeta;
// instantiate all errors
instantiatedRoute.errors = await Promise.all(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
instantiatedRoute.errorNames.map(async (error: any) =>
// eslint-disable-next-line @typescript-eslint/ban-types
Object.assign((await project.instantiateDefinitionByName(error.name)) as object, {
name: error.name,
}),
),
);
instantiatedRoute.responseBodyDescription =
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
project.definitions[instantiatedRoute.responseBodyName]?.comment?.shortSummary!;
instantiatedRoute.requestBodyDescription =
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
project.definitions[instantiatedRoute.requestBodyName]?.comment?.shortSummary!;
return {
description: {
shortText: node.comment?.shortSummary,
text: node.comment?.description,
},
name: node.name!,
route: instantiatedRoute,
};
}),
),
);
}
/**
* Generate documentation snippet for one route
* @param routeWithInfo A route instance with its meta information
* @param outDirectorySchemasPath Path to directory that will contain relevant schemas for the route
* @param tagsToKeep Tags / keywords that can be used for grouping routes
*/
export function generateOpenAPIForRoute(
routeWithInfo: RouteWithMetaInformation,
outDirectorySchemasPath: string,
tagsToKeep: string[],
): OpenAPIV3.PathItemObject {
const route = routeWithInfo.route;
const openapiPath: OpenAPIV3.PathItemObject = {};
openapiPath[route.method.toLowerCase() as OpenAPIV3.HttpMethods] = {
summary: capitalize(routeWithInfo.description.shortText?.replace(/(Route to |Route for )/gim, '')),
description: routeWithInfo.description.text,
requestBody: {
description: route.responseBodyDescription ?? undefined,
content: {
'application/json': {
schema: {
$ref: path.join(outDirectorySchemasPath, `${route.requestBodyName}.json`),
},
},
},
},
parameters: [
{
name: 'X-StApps-Version',
in: 'header',
schema: {
type: 'string',
example: '2.0.0',
},
required: true,
},
],
responses: {},
tags: routeWithInfo.tags?.filter(value => tagsToKeep.includes(value)),
};
openapiPath[route.method.toLowerCase() as OpenAPIV3.HttpMethods]!.responses![route.statusCodeSuccess] = {
description: route.responseBodyDescription,
content: {
'application/json': {
schema: {
$ref: path.join(outDirectorySchemasPath, `${route.responseBodyName}.json`),
},
},
},
};
for (const error of route.errors) {
openapiPath[route.method.toLowerCase() as OpenAPIV3.HttpMethods]!.responses![error.statusCode] = {
description:
error.message ?? capitalize(error.name.replaceAll(/([A-Z][a-z])/g, ' $1').replace('SC ', '')),
content: {
'application/json': {
schema: {
$ref: path.join(outDirectorySchemasPath, `${error.name}.json`),
},
},
},
};
}
if (typeof route.obligatoryParameters === 'object') {
for (const [parameter, schemaDefinition] of Object.entries(route.obligatoryParameters)) {
const openapiParameter: OpenAPIV3.ParameterObject = {
in: 'path',
name: parameter,
required: true,
schema: {
// TODO make this less of a hack and search copied schemas for the first occurring definition
$ref: `schema/SCSearchResponse.json#/definitions/${schemaDefinition}`,
},
};
openapiPath[route.method.toLowerCase() as OpenAPIV3.HttpMethods]?.parameters?.push(openapiParameter);
}
}
return openapiPath;
}

View File

@@ -12,17 +12,18 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Ajv from 'ajv';
import {JSONSchema7 as JSONSchema} from 'json-schema';
import {Config, DEFAULT_CONFIG, Definition, SchemaGenerator} from 'ts-json-schema-generator';
import {createFormatter} from 'ts-json-schema-generator';
import {createParser} from 'ts-json-schema-generator';
import {createProgram} from 'ts-json-schema-generator';
import Ajv, {JSONSchemaType} from 'ajv';
import {
Config,
DEFAULT_CONFIG,
SchemaGenerator,
createParser,
createFormatter,
createProgram,
} from 'ts-json-schema-generator';
import {getTsconfigPath} from './common.js';
import {definitionsOf, lightweightProjectFromPath} from '@openstapps/easy-ast';
import {isSchemaWithDefinitions} from './util/guards.js';
import path from 'path';
import re2 from 're2';
/**
* StAppsCore converter
@@ -30,74 +31,35 @@ import re2 from 're2';
* Converts TypeScript source files to JSON schema files
*/
export class Converter {
/**
* Generator instance
*/
private readonly generator: SchemaGenerator;
/**
* Schema validator instance
*/
private readonly schemaValidator: Ajv.default;
/**
* Create a new converter
* @param projectPath Path to the project
* @param sourcePath Path to optionally point to a different directory of / or single source file
*/
constructor(projectPath: string, sourcePath?: string) {
// set config for schema generator
const config: Config = {
...DEFAULT_CONFIG,
path: sourcePath,
sortProps: true,
topRef: false,
tsconfig: path.join(getTsconfigPath(projectPath), 'tsconfig.json'),
type: 'SC',
};
// create TypeScript program from config
const program = createProgram(config);
// create generator
this.generator = new SchemaGenerator(program, createParser(program, config), createFormatter(config));
// create Ajv instance
this.schemaValidator = new Ajv.default({code: {regExp: re2 as never}});
this.schemaValidator = new Ajv.default();
}
/**
* Get schema for specific StAppsCore type
* @param type Type to get the schema for
* @param version Version to set for the schema
* @param _version Version to set for the schema
* @returns Generated schema
*/
getSchema(type: string, version: string): JSONSchema {
// generate schema for this file/type
const schema: JSONSchema = this.generator.createSchema(type);
// set id of schema
schema.$id = `https://core.stapps.tu-berlin.de/v${version}/lib/schema/${type}.json`;
if (isSchemaWithDefinitions(schema)) {
const selfReference = {
...schema,
};
delete selfReference.$schema;
delete selfReference.definitions;
delete selfReference.$id;
// add self reference to definitions
schema.definitions![`SC${type}`] = {
...(selfReference as unknown as Definition),
};
}
if (!this.schemaValidator.validateSchema(schema)) {
throw new Error(`Generated schema for ${type} is invalid!`);
}
getSchema(type: string, _version: string): JSONSchemaType<unknown> {
const schema = this.generator.createSchema(type) as JSONSchemaType<unknown>;
this.schemaValidator.validateSchema(schema, true);
return schema;
}
}
@@ -110,3 +72,14 @@ export function getValidatableTypesInPath(path: string): string[] {
.filter(type => !!type.comment?.tags?.find(it => it.name === 'validatable'))
.map(type => type.name);
}
/**
* Merge multiple schemas
*/
export function mergeSchemas(schemas: JSONSchemaType<unknown>[]): JSONSchemaType<unknown> {
const completeSchema = {definitions: {}} as JSONSchemaType<unknown>;
for (const schema of schemas) {
Object.assign(completeSchema.definitions!, schema.definitions);
}
return completeSchema;
}

View File

@@ -1,97 +0,0 @@
/*
* Copyright (C) 2018-2021 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type SCRoute = any;
export interface RouteInstanceWithMeta extends SCRoute {
/**
* Possible errors on a route
*/
errors: SCErrorResponse[];
/**
* Description of the request body
*/
requestBodyDescription: string;
/**
* Description of the response body
*/
responseBodyDescription: string;
}
/**
* A route instance with its relevant meta information
*/
export interface RouteWithMetaInformation {
/**
* Description of the route
*/
description: {
/**
* Short text of the description - title
*/
shortText?: string;
/**
* Text of the description
*/
text?: string;
};
/**
* Name of the route
*/
name: string;
/**
* Instance of the route
*/
route: RouteInstanceWithMeta;
/**
* Possible tags/keywords the route can be associated with
*/
tags?: [string];
}
/**
* A node with its relevant meta information
*/
export interface NodeWithMetaInformation {
/**
* Module the node belongs to
*/
module: string;
/**
* Type of the node
*/
type: string;
}
/**
* A generic error that can be returned by the backend if somethings fails during the processing of a request
*/
export interface SCErrorResponse extends Error {
/**
* Additional data that describes the error
*/
additionalData?: unknown;
/**
* HTTP status code to return this error with
*/
statusCode: number;
}

View File

@@ -12,9 +12,3 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* Creates sentence cased string
*/
export function capitalize(string?: string): string {
return `${string?.charAt(0).toUpperCase()}${string?.slice(1).toLowerCase()}`;
}

View File

@@ -1,314 +0,0 @@
/*
* Copyright (C) 2018-2021 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Logger} from '@openstapps/logger';
import Ajv from 'ajv';
import betterAjvErrors, {IOutputError} from 'better-ajv-errors';
import type {PathLike} from 'fs';
import {readFile, writeFile} from 'fs/promises';
import {JSONSchema7} from 'json-schema';
import mustache from 'mustache';
import {Schema} from 'ts-json-schema-generator';
import {ExpectedValidationErrors, ValidationError, ValidationResult} from './types/validator.js';
import {isThingWithType} from './util/guards.js';
import path from 'path';
import re2 from 're2';
import {glob} from 'glob';
import {fileURLToPath} from 'url';
/**
* StAppsCore validator
*/
export class Validator {
/**
* JSON Schema Validator
*/
private readonly ajv = new Ajv.default({
verbose: true,
allowUnionTypes: true,
code: {regExp: re2 as never},
});
/**
* Map of schema names to schemas
*/
private readonly schemas: {[type: string]: Schema} = {};
/**
* A wrapper function for Ajv that transforms the error into the compatible old error
* @param schema the schema that will be validated against
* @param instance the instance that will be validated
*/
private ajvValidateWrapper(schema: JSONSchema7, instance: unknown): ValidationResult {
return fromAjvResult(this.ajv.validate(schema, instance), schema, instance, this.ajv);
}
/**
* Feed the schema files to the validator
* @param schemaDirectory Path to directory that contains schema files
*/
public async addSchemas(schemaDirectory: string): Promise<string[]> {
const searchGlob = path.posix.join(schemaDirectory.replaceAll(path.sep, path.posix.sep), '*.json');
const schemaFiles = await glob(searchGlob);
if (schemaFiles.length === 0) {
throw new Error(`No schema files in ${schemaDirectory.toString()}!`);
}
Logger.log(`Adding schemas from ${schemaDirectory} to validator.`);
await Promise.all(
schemaFiles.map(async (file: string) => {
// read schema file
const buffer = await readFile(file);
// add schema to map
this.schemas[path.basename(file, '.json')] = JSON.parse(buffer.toString());
Logger.info(`Added ${file} to validator.`);
}),
);
return schemaFiles;
}
/**
* Validates anything against a given schema name or infers schema name from object
* @param instance Instance to validate
* @param schema Name of schema to validate instance against or the schema itself
*/
public validate(instance: unknown, schema?: string | Schema): ValidationResult {
if (schema === undefined) {
if (isThingWithType(instance)) {
// schema name can be inferred from type string
const schemaSuffix = (instance as {type: string}).type
.split(' ')
.map((part: string) => {
return part.slice(0, 1).toUpperCase() + part.slice(1);
})
.join('');
const schemaName = `SC${schemaSuffix}`;
return this.validate(instance, schemaName);
}
throw new Error('Instance.type does not exist.');
}
if (typeof schema === 'string') {
// if you want to access a schema that is contained in the validator object
if (typeof this.schemas[schema] !== 'object') {
throw new TypeError(`No schema available for ${schema}.`);
}
// schema will be cached
return this.ajvValidateWrapper(this.schemas[schema], instance);
}
return this.ajvValidateWrapper(schema, instance);
}
}
/**
* Creates a ValidationResult from ajv
*
* Implemented for compatibility purposes
* @param result the result, now a ValidationResult
* @param schema the schema that has been validated against
* @param instance the data that has been validated
* @param ajvInstance the ajv instance with which the validation was done
*/
function fromAjvResult(
result: boolean | PromiseLike<unknown>,
schema: JSONSchema7,
instance: unknown,
ajvInstance: Ajv.default,
): ValidationResult {
const betterErrorObject: IOutputError[] | undefined = betterAjvErrors(
schema,
instance,
ajvInstance.errors ?? [],
// eslint-disable-next-line unicorn/no-null
{format: 'js', indent: null},
);
return {
errors:
ajvInstance.errors?.map((ajvError, index) => {
const error: ValidationError = {
dataPath: ajvError.instancePath,
instance: instance,
message: betterErrorObject?.[index]?.error ?? ajvError.message,
name: ajvError.keyword,
schemaPath: ajvError.schemaPath,
suggestion: betterErrorObject?.[index]?.suggestion,
};
// (validationError as ValidationError).humanReadableError = betterErrorCLI?.[index] as unknown as string;
return error;
}) ?? [],
valid: typeof result === 'boolean' ? result : false,
};
}
/**
* Validate all test files in the given resources directory against schema files in the given (schema) directory
* @param schemaDirectory The directory where the JSON schema files are
* @param resourcesDirectory The directory where the test files are
*/
export async function validateFiles(
schemaDirectory: string,
resourcesDirectory: string,
): Promise<ExpectedValidationErrors> {
// instantiate new validator
const v = new Validator();
await v.addSchemas(schemaDirectory);
// get a list of files to test
const testFiles = await glob(
path.posix.join(resourcesDirectory.replaceAll(path.sep, path.posix.sep), '*.json'),
{absolute: true},
);
if (testFiles.length === 0) {
throw new Error(`No test files in ${resourcesDirectory}!`);
}
Logger.log(`Found ${testFiles.length} file(s) to test.`);
// map of errors per file
const errors: ExpectedValidationErrors = {};
await Promise.all(
testFiles.map(async (testFile: string) => {
const testFileName = path.basename(testFile);
const buffer = await readFile(testFile);
// read test description from file
const testDescription = JSON.parse(buffer.toString());
// validate instance from test description
const result = v.validate(testDescription.instance, testDescription.schema);
// list of expected errors for this test description
const expectedErrors: string[] = [...testDescription.errorNames];
// number of unexpected errors
let unexpectedErrors = 0;
if (result.errors.length > 0) {
errors[testFileName] = [];
// iterate over errors
for (const error of result.errors) {
const errorIndex = expectedErrors.indexOf(error.name);
let expected = false;
if (errorIndex >= 0) {
expectedErrors.splice(errorIndex, 1);
expected = true;
} else {
unexpectedErrors++;
await Logger.error(`Unexpected error ${error.name} in ${testFile}`);
}
// add error to list of errors
errors[testFileName].push({
...error,
expected,
});
}
}
if (expectedErrors.length > 0) {
for (const error of expectedErrors) {
await Logger.error(`Extraneous expected error '${error}' in ${testFile}.`);
errors[testFileName].push({
dataPath: 'undefined',
expected: false,
instance: undefined,
// instance: testDescription.instance,
message: 'undefined',
name: `expected ${error}`,
schemaPath: 'undefined',
suggestion: 'undefined',
});
}
} else if (unexpectedErrors === 0) {
Logger.info(`Successfully validated ${testFile}.`);
}
}),
);
return errors;
}
/**
* Write a report for errors that occurred in validation
* @param reportPath Path to write report to
* @param errors Errors that occurred in validation
*/
export async function writeReport(reportPath: PathLike, errors: ExpectedValidationErrors): Promise<void> {
let buffer = await readFile(
path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'resources', 'file.html.mustache'),
);
const fileTemplate = buffer.toString();
buffer = await readFile(
path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'resources', 'error.html.mustache'),
);
const errorTemplate = buffer.toString();
let output = '';
for (const fileName in errors) {
if (!errors.hasOwnProperty(fileName)) {
continue;
}
let fileOutput = '';
for (const [index, error] of errors[fileName].entries()) {
fileOutput += mustache.render(errorTemplate, {
idx: index + 1,
instance: JSON.stringify(error.instance, undefined, 2),
message: error.message,
name: error.name,
schemaPath: error.schemaPath,
status: error.expected ? 'alert-success' : 'alert-danger',
suggestion: error.suggestion,
});
}
output += mustache.render(fileTemplate, {
errors: fileOutput,
testFile: fileName,
});
}
buffer = await readFile(
path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'resources', 'report.html.mustache'),
);
const reportTemplate = buffer.toString();
await writeFile(
reportPath,
mustache.render(reportTemplate, {
report: output,
timestamp: new Date().toISOString(),
}),
);
Logger.ok(`Wrote report to ${reportPath}.`);
}

View File

@@ -0,0 +1,199 @@
# @openstapps/core-tools
[![pipeline status](https://img.shields.io/gitlab/pipeline/openstapps/core-tools.svg?style=flat-square)](https://gitlab.com/openstapps/core-tools/commits/master)
[![npm](https://img.shields.io/npm/v/@openstapps/core-tools.svg?style=flat-square)](https://npmjs.com/package/@openstapps/core-tools)
[![license)](https://img.shields.io/npm/l/@openstapps/core-tools.svg?style=flat-square)](https://www.gnu.org/licenses/gpl-3.0.en.html)
[![documentation](https://img.shields.io/badge/documentation-online-blue.svg?style=flat-square)](https://openstapps.gitlab.io/core-tools)
Tools to convert and validate StAppsCore
## What are the tools for?
The StAppsCore Converter is a tool for converting SC-types (TypeScript) into JSON schema files.
JSON schema files are needed for run-time validation of SC-type objects, as this is a tedious task to do using SC-types defined in TypeScript (not possible without additional coding). That said, StAppsCore Converter practically prepares SC-types to be used for object validation (determining whether a JavaScript/JSON object is a valid object of the corresponding SC-type) using StAppsCore Validator.
The StAppsCore Validator is a tool for run-time validation of objects (determining whether a JavaScript/JSON object is a valid object of the corresponding SC-type. It consumes JSON schema files from StAppsCore as the definitions of SC-types against which are validated concrete (actual) objects (as an example SCDish object in the example below).
## Installation
Installation of the npm package (using `npm install`) makes the tool available as an executable with the name `openstapps-core-tools`.
## How to use the converter?
Add `@validatable` to the Typedoc comment of the types that you want to convert to JSONSchema.
The command `openstapps-core-tools` can then be called using these arguments:
```shell
openstapps-core-tools schema <srcPath> <schemaPath>
```
where:
- `<srcPath>` is path to the project (where used `*.ts` files are, e.g. `src/core`,
- `<schemaPath>` is directory to save output files to, e.g. `lib/schema`.
Complete command with the example arguments is then:
```shell
openstapps-core-tools schema src/core lib/schema
```
Inside of a script in `package.json` or if the npm package is installed globally, the tool `stapps-convert` can be called without its local path (`node_modules/.bin`):
```shell
openstapps-core-tools schema src/core lib/schema
```
## How to use the validator?
### Using the validator programatically
```typescript
import {Validator} from '@openstapps/core-tools/lib/validate';
import {SCDish, SCThingType} from '@openstapps/core';
import {ValidatorResult} from 'jsonschema';
import {join} from 'path';
const objectToValidate: SCDish = {
type: SCThingType.Dish,
// more properties
};
// instantiate a new validator
const validator = new Validator();
// make the validator read the schema files
validator.addSchemas(join('node_modules', '@openstapps', 'core', 'lib', 'schema')).then(() => {
// validate an object
const result: ValidatorResult = validator.validate(objectToValidate, 'SCDish');
});
```
#### Using validateFiles function
The JSON files passed to the validateFiles method have an added layer.
That layer encapsulates the actual JSON data of the object to be verified and adds a property to enable true negative testing.
Your basic JSON object:
```json
{
"property1": "value1",
"property2": "value2",
...
}
```
JSON for validateFiles:
```json
{
"errorNames": [],
"instance": {
"property1": "value1",
"property2": "value2",
...
},
"schema": "NameOfSchema"
}
```
Where `errorNames` holds the string values of the name property of the expected ValidationErrors from JSON Schema. Empty array means no errors are expected.
`schema` holds the name of the schema to validate the instance against.
### How to use validator as a CLI tool (executable)?
The command `openstapps-core-tools` can then be called using these arguments:
```shell
openstapps-core-tools validate <schemaPath> <testPath> [reportPath]
```
where:
- `<schemaPath>` is a directory where JSON schema files are, e.g. `lib/schema`,
- `<testPath>` is a directory where test files are, e.g. `src/test/resources`,
- `[reportPath]` is a file where the HTML report of the validation will be saved to, e.g. `report.html` (optional argument - if it's not provided no report will be written).
Command with the example arguments is then for example:
```shell
openstapps-core-tools validate lib/schema src/test/resources
```
Inside of a script in `package.json` or if the npm package is installed globally, the tool `openstapps-validate` can be called without its local path (`node_modules/.bin`):
```shell
openstapps-core-tools validate lib/schema src/test/resources report.html
```
## Generate openapi JSON file for routes
To generate a openapi JSON file that represents the routes according to openapi version 3.0.3 use the following command.
```shell
openstapps-core-tools openapi PATH/TO/CORE/lib PATH/TO/PUT/FILES/TO
```
## How to use the UML generator
The UML Generator generates PlantUML from the project reflection of the source files. By default it will include externals, which will take considerably longer to execute, you can disable this behaviour via an option. It can help you to visually explore the data model or document a specific part.
You can either use the public PlantUML-server or start your own local instance. To run, restart or stop the container use the scripts provided in the `package.json`.
### Generating from source-files
```shell
openstapps-core-tools plantuml PATH/TO/SOURCEFILES http://PLANTUMLSERVER
```
Executing this command will generate a `.svg` file in your current working directory.
Multiple options can be set to enhance the diagram. By default all additional information other than the definitions are disabled. You can use:
- `--showProperties` to show all mandatory attributes of the classes and interfaces.
- `--showOptionalProperties` to show all mandatory attributes of the classes and interfaces. `--showProperties` must be set!
- `--showInheritedProperties` to show all inherited attributes of the classes and interfaces. `--showProperties` must be set!
- `--showEnumValues` to show all enumeration and type (enumeration-like) values
- `--showInheritance` to show the hierarchy of the classes and interfaces. Inherited attributes will only be shown in their parent.
- `--showAssociations` to show all references of classes and interfaces between one another
- `--excludeExternals` to exclude external definitions
- `--definitions <definitons>` to show only specific definitions to reduce the output of the diagram. `<definitions>` is a comma seperated list of definitions.
- `--outputFileName <fileName>` for a custom file name, the file extension will be added automatically (.svg). Otherwise a generic file with a timestamp will be generated into the execution directory. If a file with the same name already exists it will be overwritten!
The best way to explore models is to enable `--showInheritance` and `--showAssociations`. Start with just one definition in your `--definition <definitions>`-list, generate the diagram, look at it, add a new definition that you have seen to your command and generate anew.
#### Examples
Show the class hierarchy of the whole project:
```shell
openstapps-core-tools plantuml PATH/TO/SRCDIR http://PLANTUMLSERVER --showInheritance
```
Show the dish-module:
```shell
openstapps-core-tools plantuml ../core http://localhost:8080 --showProperties --showOptionalProperties --showInheritance --showAssociations --showEnumValues --definitions SCDish,SCThingThatCanBeOfferedOffer
```
### Generating from existing file
The plantuml code is persisted inside the generated file at the very bottom. You can tweak the model by using the function to generate UML from a PlantUML-file(simple text file). Extract the code (starting from `@startuml` to `@enduml`), edit it manually and execute this function.
```shell
openstapps-core-tools plantuml-file /PATH/TO/Project.plantuml http://PLANTUMLSERVER OptionalCustomFileName
```
Example-File-Content of Project.plantuml
```
@startuml
interface MyClass{
myProperty: string
}
@enduml
```

View File

@@ -0,0 +1,14 @@
// @ts-check
import {writeFile, readFile} from 'fs/promises';
const schemaNames = Object.keys(
JSON.parse(await readFile('schema/core.schema.json', 'utf8')).definitions,
).filter(it => /^[a-z][0-9a-z<>]*$/i.test(it));
const source =
"import type * as core from '@openstapps/core';\n\n" +
'export interface SchemaMap {\n' +
schemaNames.map(name => ` '${name}': core.${name.replaceAll('<', '<core.')};`).join('\n') +
'\n}\n';
await writeFile('schema/core.schema.d.ts', source, 'utf8');

View File

@@ -0,0 +1,80 @@
{
"name": "@openstapps/core-validator",
"description": "Validator for @openstapps/core",
"version": "3.0.0",
"type": "module",
"license": "GPL-3.0-only",
"repository": "git@gitlab.com:openstapps/openstapps.git",
"author": "Thea Schöbl <dev@theaninova.de>",
"keywords": [
"StApps",
"StAppsCore",
"converter",
"core",
"validator"
],
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"files": [
"lib",
"schema",
"Dockerfile",
"README.md",
"CHANGELOG.md"
],
"scripts": {
"build": "tsup-node --dts",
"docs": "typedoc --json ./docs/docs.json --options ../../typedoc.base.json src/index.ts",
"format": "prettier . -c --ignore-path ../../.gitignore",
"format:fix": "prettier --write . --ignore-path ../../.gitignore",
"lint": "eslint --ext .ts src/",
"lint:fix": "eslint --fix --ext .ts src/",
"test": "c8 mocha"
},
"dependencies": {
"@openstapps/core": "workspace:*",
"ajv": "8.12.0",
"ajv-formats": "2.1.1"
},
"devDependencies": {
"@openstapps/core-tools": "workspace:*",
"@openstapps/eslint-config": "workspace:*",
"@openstapps/prettier-config": "workspace:*",
"@openstapps/tsconfig": "workspace:*",
"@types/chai": "4.3.5",
"@types/fs-extra": "9.0.13",
"@types/glob": "8.0.1",
"@types/json-schema": "7.0.14",
"@types/mocha": "10.0.1",
"@types/mustache": "4.2.2",
"@types/node": "18.15.3",
"c8": "7.14.0",
"chai": "4.3.7",
"mocha": "10.2.0",
"mocha-junit-reporter": "2.2.0",
"nock": "13.3.1",
"ts-node": "10.9.1",
"tsup": "6.7.0",
"typedoc": "0.24.8",
"typescript": "5.1.6"
},
"tsup": {
"entry": [
"src/app.ts",
"src/index.ts"
],
"sourcemap": true,
"clean": true,
"format": "esm",
"outDir": "lib"
},
"prettier": "@openstapps/prettier-config",
"eslintConfig": {
"extends": [
"@openstapps"
]
},
"eslintIgnore": [
"resources"
]
}

View File

@@ -0,0 +1,2 @@
core.schema.json
core.schema.d.ts

View File

@@ -0,0 +1,48 @@
import Ajv, {AnySchema} from 'ajv';
import addFormats from 'ajv-formats';
import schema from '../schema/core.schema.json';
import {SchemaMap} from '../schema/core.schema.js';
export type RemoveNeverProperties<T> = {
[K in Exclude<
keyof T,
{
// eslint-disable-next-line @typescript-eslint/ban-types
[P in keyof T]: T[P] extends Function ? P : never;
}[keyof T]
>]: T[K];
};
export type IncludeProperty<T extends object, E> = RemoveNeverProperties<{
[K in keyof T]: T[K] extends E ? T[K] : never;
}>;
type NameOf<I extends SchemaMap[keyof SchemaMap]> = keyof IncludeProperty<SchemaMap, I>;
/**
* StAppsCore validator
*/
export class Validator {
private readonly ajv: Ajv.default;
constructor(additionalSchemas: AnySchema[] = []) {
this.ajv = new Ajv.default({
schemas: [schema, ...additionalSchemas],
verbose: true,
allowUnionTypes: true,
});
addFormats.default(this.ajv, {
formats: ['date-time', 'time', 'uuid', 'duration'],
mode: 'fast',
});
}
/**
* Validates anything against a given schema name or infers schema name from object
* @param instance Instance to validate
* @param schema Name of schema to validate instance against or the schema itself
*/
public validate<T>(instance: unknown, schema: NameOf<T>): instance is T {
return this.ajv.validate(schema as string, instance);
}
}

View File

@@ -0,0 +1,7 @@
{
"extends": "@openstapps/tsconfig",
"compilerOptions": {
"noUnusedLocals": false,
"stripInternal": true
}
}

View File

@@ -32,15 +32,12 @@
"CHANGELOG.md"
],
"scripts": {
"build": "tsup-node --dts && pnpm run mappings && pnpm run schema && pnpm run openapi && cp api-doc.html lib/api-doc.html",
"build": "tsup-node --dts && cp api-doc.html lib/api-doc.html",
"docs": "typedoc --json ./docs/docs.json --options ../../typedoc.base.json src/index.ts",
"format": "prettier . -c --ignore-path ../../.gitignore",
"format:fix": "prettier --write . --ignore-path ../../.gitignore",
"lint": "eslint --ext .ts src/",
"lint:fix": "eslint --fix --ext .ts src/",
"mappings": "openstapps-es-mapping-generator mapping ../core/src -i minlength,pattern,see,tjs-format -m lib/mappings/mappings.json -a lib/mappings/aggregations.json",
"openapi": "openstapps-core-tools openapi lib lib && node -e \"assert(JSON.parse(require('fs').readFileSync('lib/openapi.json', 'utf8')).paths['/search'] !== undefined)\"",
"schema": "node --max-old-space-size=8192 --stack-size=10240 ./node_modules/@openstapps/core-tools/lib/app.js schema src lib/schema",
"test": "c8 mocha"
},
"dependencies": {
@@ -56,12 +53,14 @@
"@openstapps/easy-ast": "workspace:*",
"@openstapps/es-mapping-generator": "workspace:*",
"@openstapps/eslint-config": "workspace:*",
"@openstapps/json-schema-generator": "workspace:*",
"@openstapps/logger": "workspace:*",
"@openstapps/openapi-generator": "workspace:*",
"@openstapps/prettier-config": "workspace:*",
"@openstapps/tsconfig": "workspace:*",
"@types/chai": "4.3.5",
"@types/json-patch": "0.0.30",
"@types/json-schema": "7.0.11",
"@types/json-schema": "7.0.14",
"@types/mocha": "10.0.1",
"@types/node": "18.15.3",
"c8": "7.14.0",
@@ -97,21 +96,11 @@
{
"definedTags": [
"internal",
"aggregatable",
"float",
"indexable",
"integer",
"keyword",
"sortable",
"text",
"date",
"validatable",
"filterable",
"inheritTags",
"elasticsearch",
"minLength",
"pattern",
"typeparam",
"TJS-format"
"integer",
"format"
]
}
]

View File

@@ -14,7 +14,6 @@
*/
import {Polygon} from 'geojson';
import {SCTranslations} from '../general/i18n.js';
import {SCMap} from '../general/map.js';
import {SCLanguageSetting, SCSetting, SCUserGroupSetting} from '../things/setting.js';
import {SCAuthorizationProviderType} from './authorization.js';
import {SCFeatureConfiguration} from './feature.js';
@@ -89,7 +88,7 @@ export interface SCAppConfiguration {
*
* Mapping route -> page config
*/
aboutPages: SCMap<SCAboutPage>;
aboutPages: Record<string, SCAboutPage>;
/**
* Polygon that encapsulates the main campus

View File

@@ -12,7 +12,6 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {SCMap, SCRestrictedMap} from '../general/map.js';
import {SCUuid} from '../general/uuid.js';
import {SCSearchSortType} from '../protocol/search/sort.js';
import {SCThingType} from '../things/abstract/thing.js';
@@ -105,7 +104,7 @@ export type SCSearchContext = 'default' | 'dining' | 'place';
/**
* A boosting configuration for one context
*/
export type SCBackendConfigurationSearchBoostingContext = SCRestrictedMap<
export type SCBackendConfigurationSearchBoostingContext = Record<
SCSearchContext,
SCBackendConfigurationSearchBoostingType[]
>;
@@ -128,10 +127,11 @@ export interface SCBackendConfigurationSearchBoostingType {
* Value of the field that should be boosted by the given number
* For example `"SS 2019": 2`
*/
fields?: SCMap<SCMap<number>>;
fields?: Record<string, Record<string, number>>;
/**
* Type of things the factor should be applied to
* Type discriminator
* @elasticsearch type
*/
type: SCThingType;
}
@@ -184,7 +184,7 @@ export interface SCBackendInternalConfiguration {
/**
* Configuration of the database
*/
export interface SCBackendConfigurationDatabaseConfiguration extends SCMap<unknown> {
export interface SCBackendConfigurationDatabaseConfiguration extends Record<string, unknown> {
/**
* Name of the database used by the backend
*/

View File

@@ -12,20 +12,18 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {SCMap} from '../general/map.js';
import {SCAuthorizationProviderType} from './authorization.js';
export interface SCFeatureConfiguration {
/**
* Map of extern services mapped by their name (statically)
*/
extern?: SCMap<SCFeatureConfigurationExtern>;
extern?: Record<string, SCFeatureConfigurationExtern>;
/**
* Map of plugins registered with the backend mapped by their name.
*/
plugins?: SCMap<SCFeatureConfigurationPlugin>;
plugins?: Record<string, SCFeatureConfigurationPlugin>;
}
export interface SCFeatureConfigurationPlugin {

View File

@@ -96,7 +96,8 @@ export interface SCMonitoringMinimumLengthCondition {
length: number;
/**
* Type of the condition
* Type discriminator
* @elasticsearch type
*/
type: 'MinimumLength';
}
@@ -111,7 +112,8 @@ export interface SCMonitoringMaximumLengthCondition {
length: number;
/**
* Type of the condition
* Type discriminator
* @elasticsearch type
*/
type: 'MaximumLength';
}

View File

@@ -18,13 +18,13 @@
export interface SCLanguage {
/**
* The two letter ISO 639-1 Code of the Language
* @filterable
* @elasticsearch filterable
*/
code: SCLanguageCode;
/**
* The Fulltext name of the Language
* @filterable
* @elasticsearch filterable
*/
name: SCLanguageName;
}

View File

@@ -1,44 +0,0 @@
/*
* Copyright (C) 2019-2022 Open StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* Capsulation for a map with a string as key with values of type `T`
*
* !!! BEWARE !!!
* Can't be refactored to a `Map<K, V>`, because it can't be serialized via JSON.stringify(map)
* @typeparam T Can be any type.
*/
export interface SCMap<T> {
/**
* One value for each key
*/
[key: string]: T;
}
/**
* Restricted map with keys, limited to values of `U`, and corresponding values of type `T`
*
* !!! BEWARE !!!
* Can't be refactored to a `Map<K, V>`, because it can't be serialized via JSON.stringify(map)
* Also note, that this is a type not an interface
* @typeparam U Must be a type the `in` operator can be applied to and contains only strings or numbers
* @typeparam T Can be any type
*/
export type SCRestrictedMap<U extends string | number, T> = {
/**
* One value for each key
*/
[key in U]: T;
};

View File

@@ -14,21 +14,18 @@
*/
/**
* An ISO8601 date
* @pattern ^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])(T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])([\.,][0-9]{0,7})?(Z|[+-](?:2[0-3]|[01][0-9])(:?[0-5][0-9])?)?)?$
* @see https://gist.github.com/philipashlock/8830168
* @date
* @format date-time
*/
export type SCISO8601Date = string;
/**
* An ISO8601 duration
* @pattern ^(R\d*\/)?P(?:\d+(?:\.\d+)?Y)?(?:\d+(?:\.\d+)?M)?(?:\d+(?:\.\d+)?W)?(?:\d+(?:\.\d+)?D)?(?:T(?:\d+(?:\.\d+)?H)?(?:\d+(?:\.\d+)?M)?(?:\d+(?:\.\d+)?S)?)?$
* @see https://gist.github.com/philipashlock/8830168
* @format duration
*/
export type SCISO8601Duration = string;
/**
* An ISO8601 time
* @pattern ^(2[0-3]|[01][0-9]):?([0-5][0-9]):?([0-5][0-9])$
* @format time
*/
export type SCISO8601Time = string;

View File

@@ -14,8 +14,7 @@
*/
/**
* Universally unique identifier of the thing
* @filterable
* @pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
* @see http://stackoverflow.com/questions/7905929/how-to-test-valid-uuid-guid
* @elasticsearch filterable
* @format uuid
*/
export type SCUuid = string;

View File

@@ -11,7 +11,6 @@ export * from './config/monitoring.js';
export * from './config/user.js';
export * from './general/i18n.js';
export * from './general/map.js';
export * from './general/namespaces.js';
export * from './general/time.js';
export * from './general/uuid.js';

View File

@@ -12,7 +12,6 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {SCMap} from '../general/map.js';
import {SCErrorResponse} from './error.js';
import {SCIndexRequest, SCIndexResponse, SCIndexRoute} from './routes/index.js';
import {
@@ -61,7 +60,7 @@ export interface SCRoute {
/**
* Map of obligatory parameters and their type that have to be set via the requested path
*/
obligatoryParameters?: SCMap<string>;
obligatoryParameters?: Record<string, string>;
/**
* Name of the type of the request body
@@ -101,7 +100,7 @@ export abstract class SCAbstractRoute implements SCRoute {
/**
* @see SCRoute.obligatoryParameters
*/
obligatoryParameters?: SCMap<string>;
obligatoryParameters?: Record<string, string>;
/**
* @see SCRoute.requestBodyName
@@ -127,7 +126,7 @@ export abstract class SCAbstractRoute implements SCRoute {
* Get "compiled" URL path
* @param parameters Parameters to compile URL path with
*/
public getUrlPath(parameters: SCMap<string> = {}): string {
public getUrlPath(parameters: Record<string, string> = {}): string {
let obligatoryParameters: string[] = [];
if (typeof this.obligatoryParameters === 'object') {

View File

@@ -53,8 +53,8 @@ export interface SCBulkParameters {
source: string;
/**
* Type of things that are indexed in this bulk.
*
* Type discriminator
* @elasticsearch type
*/
type: SCThingType;
}

View File

@@ -13,7 +13,6 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {StatusCodes} from 'http-status-codes';
import {SCMap} from '../../general/map.js';
import {SCInternalServerErrorResponse} from '../errors/internal-server-error.js';
import {SCMethodNotAllowedErrorResponse} from '../errors/method-not-allowed.js';
import {SCRequestBodyTooLargeErrorResponse} from '../errors/request-body-too-large.js';
@@ -33,7 +32,7 @@ import {SCSearchResult} from '../search/result.js';
* **CAUTION: This is limited to an amount of queries. Currently this limit is 5.**
* @validatable
*/
export type SCMultiSearchRequest = SCMap<SCSearchQuery>;
export type SCMultiSearchRequest = Record<string, SCSearchQuery>;
/**
* A multi search response
@@ -41,7 +40,7 @@ export type SCMultiSearchRequest = SCMap<SCSearchQuery>;
* This is a map of [[SCSearchResponse]]s indexed by name
* @validatable
*/
export type SCMultiSearchResponse = SCMap<SCSearchResult>;
export type SCMultiSearchResponse = Record<string, SCSearchResult>;
/**
* Route for submission of multiple search requests at once

View File

@@ -12,10 +12,6 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {SCMap} from '../../general/map.js';
/**
* All available filter types
*/
import {SCSearchAvailabilityFilter} from './filters/availability.js';
import {SCSearchBooleanFilter} from './filters/boolean.js';
import {SCSearchDistanceFilter} from './filters/distance.js';
@@ -53,7 +49,7 @@ export interface SCSearchAbstractFilter<T extends SCSearchAbstractFilterArgument
/**
* Arguments for the filter instruction
*/
export type SCSearchAbstractFilterArguments = SCMap<unknown>;
export type SCSearchAbstractFilterArguments = Record<string, unknown>;
/**
* Available filter instructions

View File

@@ -12,7 +12,6 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {SCMap} from '../../general/map.js';
import {SCThingsField} from '../../meta.js';
import {SCDistanceSort} from './sorts/distance.js';
import {SCDucetSort} from './sorts/ducet.js';
@@ -42,7 +41,7 @@ export interface SCSearchAbstractSort<T extends SCSearchAbstractSortArguments> {
/**
* Map of arguments for the sort instruction
*/
export interface SCSearchAbstractSortArguments extends SCMap<unknown> {
export interface SCSearchAbstractSortArguments extends Record<string, unknown> {
/**
* Field to sort by
*/

View File

@@ -21,8 +21,7 @@ import {SCThing, SCThingMeta, SCThingWithoutReferences} from './thing.js';
export interface SCAcademicDegreeWithoutReferences extends SCThingWithoutReferences {
/**
* The achievable academic degree
* @filterable
* @sortable ducet
* @elasticsearch filterable sortable:ducet
*/
academicDegree: string;

View File

@@ -22,33 +22,31 @@ import {SCThing, SCThingMeta, SCThingWithoutReferences} from './thing.js';
export interface SCAcademicTermWithoutReferences extends SCThingWithoutReferences {
/**
* Short name of the academic term, using the given pattern
* @aggregatable
* @filterable
* @keyword
* @elasticsearch aggregatable filterable
*/
acronym: string;
/**
* End date of the academic term
* @filterable
* @elasticsearch filterable
*/
endDate: SCISO8601Date;
/**
* End date of lectures in the academic term
* @filterable
* @elasticsearch filterable
*/
eventsEndDate?: SCISO8601Date;
/**
* Start date of lectures in the academic term
* @filterable
* @elasticsearch filterable
*/
eventsStartDate?: SCISO8601Date;
/**
* Start date of the academic term
* @filterable
* @elasticsearch filterable
*/
startDate: SCISO8601Date;
}

View File

@@ -35,33 +35,28 @@ export interface SCCreativeWorkWithoutReferences extends SCThingWithoutReference
/**
* Edition of a creative work (e.g. the book edition or edition of an article)
* @keyword
*/
edition?: string;
/**
* Date (in text form) the creative work was published for the first time
* @keyword
*/
firstPublished?: string;
/**
* Languages this creative work is written/recorded/... in
* @filterable
* @elasticsearch filterable
*/
inLanguage?: SCLanguageCode;
/**
* Keywords of the creative work
* @aggregatable
* @filterable
* @keyword
* @elasticsearch aggregatable filterable
*/
keywords?: string[];
/**
* Date (in text form) the creative work was most recently
* @keyword
*/
lastPublished?: string;
@@ -112,7 +107,6 @@ export interface SCCreativeWork extends SCCreativeWorkWithoutReferences, SCThing
export interface SCCreativeWorkTranslatableProperties extends SCThingTranslatableProperties {
/**
* Translation of the keywords of the creative work
* @keyword
*/
keywords?: string[];
}

View File

@@ -39,37 +39,37 @@ export interface SCGeoInformation {
export interface SCPostalAddress {
/**
* Country of the address
* @filterable
* @elasticsearch filterable
*/
addressCountry: string;
/**
* City of the address
* @filterable
* @elasticsearch filterable
*/
addressLocality: string;
/**
* State of the address
* @filterable
* @elasticsearch filterable
*/
addressRegion?: string;
/**
* Zip code of the address
* @filterable
* @elasticsearch filterable
*/
postalCode: string;
/**
* Optional post box number
* @filterable
* @elasticsearch filterable
*/
postOfficeBoxNumber?: string;
/**
* Street of the address - with house number!
* @filterable
* @elasticsearch filterable
*/
streetAddress: string;
}
@@ -94,7 +94,6 @@ export interface SCPlaceWithoutReferences extends SCThingWithoutReferences {
/**
* Opening hours of the place
* @see http://wiki.openstreetmap.org/wiki/Key:opening_hours/specification
* @keyword
*/
openingHours?: string;

View File

@@ -16,8 +16,6 @@ import {SCISO8601Date} from '../../general/time.js';
/**
* Date Range
*
* CAUTION: Changing the name requires changes in the core-tools premaps
*/
export type SCISO8601DateRange = SCRange<SCISO8601Date>;

View File

@@ -29,7 +29,8 @@ export interface SCSaveableThing extends SCSaveableThingWithoutReferences, SCThi
*/
data: SCIndexableThings;
/**
* Type of the origin
* Type discriminator
* @elasticsearch type
*/
origin: SCThingUserOrigin;
}

View File

@@ -26,7 +26,7 @@ export type SCThingThatAcceptsPaymentsAcceptedPayments = 'cash' | 'credit' | 'ca
export interface SCThingThatAcceptsPaymentsWithoutReferences extends SCThingWithoutReferences {
/**
* Accepted payments of the place
* @filterable
* @elasticsearch filterable
*/
paymentsAccepted?: SCThingThatAcceptsPaymentsAcceptedPayments[];
}

View File

@@ -25,8 +25,7 @@ import {SCThing, SCThingMeta, SCThingTranslatableProperties, SCThingWithoutRefer
export interface SCPriceGroup {
/**
* Default price of the thing
* @sortable price
* @float
* @elasticsearch sortable:price
*/
default: number;
}
@@ -37,22 +36,19 @@ export interface SCPriceGroup {
export interface SCAcademicPriceGroup extends SCPriceGroup {
/**
* Price for employees
* @sortable price
* @float
* @elasticsearch sortable:price
*/
employee?: number;
/**
* Price for guests
* @sortable price
* @float
* @elasticsearch sortable:price
*/
guest?: number;
/**
* Price for students
* @sortable price
* @float
* @elasticsearch sortable:price
*/
student?: number;
}
@@ -115,7 +111,6 @@ export interface SCThingThatCanBeOfferedOffer<T extends SCPriceGroup> extends SC
export interface SCThingThatCanBeOfferedTranslatableProperties extends SCThingTranslatableProperties {
/**
* Availability of an offer
* @keyword
*/
'offers[].availability'?: string;
}

View File

@@ -13,7 +13,6 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {SCMetaTranslations, SCTranslations} from '../../general/i18n.js';
import {SCMap} from '../../general/map.js';
import {SCThing, SCThingMeta, SCThingTranslatableProperties, SCThingWithoutReferences} from './thing.js';
/**
@@ -26,9 +25,7 @@ export interface SCThingWithCategoriesWithoutReferences<T, U extends SCThingWith
extends SCThingWithoutReferences {
/**
* Categories of a thing with categories
* @sortable ducet
* @aggregatable
* @filterable
* @elasticsearch aggregatable sortable:ducet filterable
*/
categories: T[];
@@ -37,7 +34,7 @@ export interface SCThingWithCategoriesWithoutReferences<T, U extends SCThingWith
*
* A map from categories to their specific values.
*/
categorySpecificValues?: SCMap<U>;
categorySpecificValues?: Record<string, U>;
/**
* Translated fields of a thing with categories
@@ -62,46 +59,42 @@ export interface SCThingWithCategories<T, U extends SCThingWithCategoriesSpecifi
*/
export interface SCThingWithCategoriesTranslatableProperties extends SCThingTranslatableProperties {
/**
* translations of the categories of a thing with categories
* @sortable ducet
* translations of the categories for a thing with categories
* @elasticsearch filterable sortable:ducet
*/
categories?: string[];
}
/**
* Category specific values of a thing with categories
* Category-specific values of a thing with categories
*
* This interface contains properties that can be specific to a certain category.
*/
export interface SCThingWithCategoriesSpecificValues {
/**
* Category specific alternate names of a thing
* @keyword
* Category-specific alternate names of a thing
*/
alternateNames?: string[];
/**
* Category specific description of a thing
* @text
* Category-specific description of a thing
* @elasticsearch text
*/
description?: string;
/**
* URL of a category specific image of a thing
* @keyword
* URL of a category-specific image of a thing
*/
image?: string;
/**
* Category specific name of a thing
* @sortable ducet
* @text
* @elasticsearch text sortable:ducet
*/
name?: string;
/**
* Category specific URL of a thing
* @keyword
* Category-specific URL of a thing
*/
url?: string;
}
@@ -130,7 +123,7 @@ export class SCThingWithCategoriesWithoutReferencesMeta<T, U extends SCThingWith
};
/**
* Translations of values of fields
* Translations of field values
*/
fieldValueTranslations = {
de: {

View File

@@ -13,7 +13,6 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {SCMetaTranslations, SCTranslations} from '../../general/i18n.js';
import {SCMap} from '../../general/map.js';
import {SCISO8601Date} from '../../general/time.js';
import {SCUuid} from '../../general/uuid.js';
import {SCOrganizationWithoutReferences} from '../organization.js';
@@ -62,37 +61,33 @@ export enum SCThingType {
export interface SCThingWithoutReferences {
/**
* Alternate names of the thing
* @filterable
* @keyword
* @elasticsearch filterable
*/
alternateNames?: string[];
/**
* Description of the thing
* @minLength 1
* @text
* @elasticsearch text
*/
description?: string;
/**
* The identifier property represents any kind of additional identifier for any kind of SCThing
*
* E.g. GTIN codes, UUIDs, Database IDs etc.
* E.g., GTIN codes, UUIDs, Database IDs, etc.
*/
identifiers?: SCMap<string>;
identifiers?: Record<string, string>;
/**
* URL of an image of the thing
* @keyword
* URL to an image of the thing
*/
image?: string;
/**
* Name of the thing
* @filterable
* @minLength 1
* @sortable ducet
* @text
* @elasticsearch text filterable sortable:ducet
*/
name: string;
@@ -111,10 +106,8 @@ export interface SCThingWithoutReferences {
translations?: SCTranslations<SCThingTranslatableProperties>;
/**
* Type of the thing
* @sortable ducet
* @filterable
* @aggregatable global
* Type discriminator
* @elasticsearch type
*/
type: SCThingType;
@@ -159,7 +152,8 @@ export interface SCThingOrigin {
modified?: SCISO8601Date;
/**
* Type of the origin
* Type discriminator
* @elasticsearch type
*/
type: SCThingOriginType;
}
@@ -175,7 +169,7 @@ export interface SCThingRemoteOrigin extends SCThingOrigin {
/**
* Name of the origin
* @text
* @elasticsearch text
*/
name: string;
@@ -192,7 +186,8 @@ export interface SCThingRemoteOrigin extends SCThingOrigin {
responsibleEntity?: SCPersonWithoutReferences | SCOrganizationWithoutReferences;
/**
* Type of the origin
* Type discriminator
* @elasticsearch type
*/
type: SCThingOriginType.Remote;
@@ -217,7 +212,8 @@ export interface SCThingUserOrigin extends SCThingOrigin {
deleted?: boolean;
/**
* Type of the origin
* Type discriminator
* @elasticsearch type
*/
type: SCThingOriginType.User;
@@ -233,13 +229,12 @@ export interface SCThingUserOrigin extends SCThingOrigin {
export interface SCThingTranslatableProperties {
/**
* Translation of the description of the thing
* @text
* @elasticsearch text
*/
description?: string;
/**
* Translation of the name of the thing
* @sortable ducet
* @text
* @elasticsearch text sortable:ducet
*/
name?: string;
/**
@@ -254,7 +249,7 @@ export interface SCThingTranslatableProperties {
export interface SCThingTranslatablePropertyOrigin {
/**
* Translation of the name of the origin
* @text
* @elasticsearch text
*/
name: string;
}

View File

@@ -31,16 +31,13 @@ export interface SCAcademicEventWithoutReferences
SCThingWithCategoriesWithoutReferences<SCAcademicEventCategories, SCThingWithCategoriesSpecificValues> {
/**
* Majors of the academic event that this event belongs to
* @aggregatable
* @filterable
* @keyword
* @elasticsearch filterable aggregatable
*/
majors?: string[];
/**
* Original unmapped category from the source of the academic event
* @filterable
* @keyword
* @elasticsearch filterable
*/
originalCategory?: string;
@@ -50,7 +47,8 @@ export interface SCAcademicEventWithoutReferences
translations?: SCTranslations<SCAcademicEventTranslatableProperties>;
/**
* Type of an academic event
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.AcademicEvent;
}
@@ -58,7 +56,7 @@ export interface SCAcademicEventWithoutReferences
/**
* An academic event
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCAcademicEvent
extends SCEvent,
@@ -70,7 +68,8 @@ export interface SCAcademicEvent
translations?: SCTranslations<SCAcademicEventTranslatableProperties>;
/**
* Type of an academic event
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.AcademicEvent;
}
@@ -101,13 +100,11 @@ export type SCAcademicEventCategories =
export interface SCAcademicEventTranslatableProperties extends SCThingWithCategoriesTranslatableProperties {
/**
* Translations of the majors of the academic event that this event belongs to
* @keyword
*/
majors?: string[];
/**
* Translation of the original unmapped category from the source of the academic event
* @keyword
*/
originalCategory?: string;
}

View File

@@ -49,7 +49,7 @@ export interface SCArticleWithoutReferences
SCThingWithCategoriesWithoutReferences<SCArticleCategories, SCThingWithCategoriesSpecificValues> {
/**
* Article itself as markdown
* @text
* @elasticsearch filterable
*/
articleBody?: string;
@@ -64,7 +64,8 @@ export interface SCArticleWithoutReferences
translations?: SCTranslations<SCArticleTranslatableProperties>;
/**
* Type of an article
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Article;
}
@@ -72,7 +73,7 @@ export interface SCArticleWithoutReferences
/**
* An article
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCArticle
extends SCCreativeWork,
@@ -93,7 +94,8 @@ export interface SCArticle
translations?: SCTranslations<SCArticleTranslatableProperties>;
/**
* Type of an article
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Article;
}
@@ -107,7 +109,7 @@ export interface SCArticleTranslatableProperties
SCCreativeWorkTranslatableProperties {
/**
* Translation of the article itself as markdown
* @text
* @elasticsearch filterable
*/
articleBody?: string[];
}

View File

@@ -52,7 +52,6 @@ export interface SCAssessmentWithoutReferences
/**
* ECTS (credit-points)
* @float
*/
ects?: number;
@@ -72,7 +71,8 @@ export interface SCAssessmentWithoutReferences
translations?: SCTranslations<SCAssessmentTranslatableProperties>;
/**
* Type of an assessment
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Assessment;
}
@@ -101,7 +101,8 @@ export interface SCAssessment
translations?: SCTranslations<SCAssessmentTranslatableProperties>;
/**
* Type of an assessment
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Assessment;
}

View File

@@ -73,8 +73,7 @@ export interface SCBookWithoutReferences
/**
* ISBNs of a book
* @filterable
* @keyword
* @elasticsearch filterable
*/
ISBNs?: string[];
@@ -90,7 +89,8 @@ export interface SCBookWithoutReferences
translations?: SCTranslations<SCBookTranslatableFields>;
/**
* Type of a book
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Book;
}
@@ -98,7 +98,7 @@ export interface SCBookWithoutReferences
/**
* A book
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCBook
extends SCCreativeWork,
@@ -110,7 +110,8 @@ export interface SCBook
translations?: SCTranslations<SCBookTranslatableFields>;
/**
* Type of a book
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Book;
}

View File

@@ -43,8 +43,7 @@ export interface SCBuildingWithoutReferences
SCPlaceWithoutReferences {
/**
* List of floor names of the place
* @filterable
* @keyword
* @elasticsearch filterable
*/
floors?: string[];
@@ -54,7 +53,8 @@ export interface SCBuildingWithoutReferences
translations?: SCTranslations<SCBuildingTranslatableProperties>;
/**
* Type of the building
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Building;
}
@@ -62,7 +62,7 @@ export interface SCBuildingWithoutReferences
/**
* A building
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCBuilding
extends SCBuildingWithoutReferences,
@@ -74,7 +74,8 @@ export interface SCBuilding
translations?: SCTranslations<SCBuildingTranslatableProperties>;
/**
* Type of the building
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Building;
}

View File

@@ -32,13 +32,14 @@ export interface SCCatalogWithoutReferences
* Level of the catalog (0 for 'root catalog', 1 for its subcatalog, 2 for its subcatalog etc.)
*
* Needed for keeping order in catalog inheritance array.
* @filterable
* @elasticsearch filterable
* @integer
*/
level: number;
/**
* Type of a catalog
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Catalog;
}
@@ -46,7 +47,7 @@ export interface SCCatalogWithoutReferences
/**
* A catalog
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCCatalog
extends SCCatalogWithoutReferences,
@@ -73,7 +74,8 @@ export interface SCCatalog
translations?: SCTranslations<SCThingWithCategoriesTranslatableProperties>;
/**
* Type of a catalog
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Catalog;
}

View File

@@ -39,13 +39,14 @@ export interface SCCertificationWithoutReferences
translations?: SCTranslations<SCCertificationTranslatableProperties>;
/**
* Type of certification
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Certification;
}
/**
* @indexable
* @elasticsearch indexable
* @validatable
*/
export interface SCCertification
@@ -63,7 +64,8 @@ export interface SCCertification
translations?: SCTranslations<SCCertificationTranslatableProperties>;
/**
* Type of certification
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Certification;
}

View File

@@ -23,37 +23,33 @@ import {SCRoomWithoutReferences} from './room.js';
export interface SCContactPointWithoutReferences extends SCThingWithoutReferences {
/**
* E-mail at the work location
* @keyword
*/
email?: string;
/**
* Fax number at the work location
* @keyword
*/
faxNumber?: string;
/**
* Office hours for contacting someone at the work location
* @see http://wiki.openstreetmap.org/wiki/Key:opening_hours/specification
* @keyword
*/
officeHours?: string;
/**
* Contact number at the work location
* @keyword
*/
telephone?: string;
/**
* Type of a contact point
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.ContactPoint;
/**
* URL at the work location
* @keyword
*/
url?: string;
}
@@ -62,7 +58,7 @@ export interface SCContactPointWithoutReferences extends SCThingWithoutReference
* A contact point
* @see http://schema.org/ContactPoint
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCContactPoint extends SCContactPointWithoutReferences, SCThing {
/**
@@ -71,7 +67,8 @@ export interface SCContactPoint extends SCContactPointWithoutReferences, SCThing
areaServed?: SCRoomWithoutReferences;
/**
* Type of a contact point
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.ContactPoint;
}

View File

@@ -43,13 +43,13 @@ export interface SCCourseOfStudyWithoutReferences
/**
* The modes the course of study is offered in
* @filterable
* @elasticsearch filterable
*/
mode?: SCCourseOfStudyMode;
/**
* The time modes the course of study is offered in
* @filterable
* @elasticsearch filterable
*/
timeMode?: SCCourseOfStudyTimeMode;
@@ -59,7 +59,8 @@ export interface SCCourseOfStudyWithoutReferences
translations?: SCTranslations<SCCourseOfStudyTranslatableProperties>;
/**
* Type of the course of study
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.CourseOfStudy;
}
@@ -67,7 +68,7 @@ export interface SCCourseOfStudyWithoutReferences
/**
* A course of study
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCCourseOfStudy
extends SCCourseOfStudyWithoutReferences,
@@ -95,7 +96,8 @@ export interface SCCourseOfStudy
translations?: SCTranslations<SCCourseOfStudyTranslatableProperties>;
/**
* Type of the course of study
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.CourseOfStudy;
}

View File

@@ -33,7 +33,6 @@ import {SCSportCourseWithoutReferences} from './sport-course.js';
export interface SCSportCoursePriceGroup extends SCAcademicPriceGroup {
/**
* Price for alumnis
* @float
*/
alumni?: number;
}
@@ -44,7 +43,7 @@ export interface SCSportCoursePriceGroup extends SCAcademicPriceGroup {
export interface SCDateSeriesWithoutReferences extends SCThingThatCanBeOfferedWithoutReferences {
/**
* Dates of the date series that are initially planned to be held
* @filterable
* @elasticsearch filterable
*/
dates: SCISO8601Date[];
@@ -60,7 +59,7 @@ export interface SCDateSeriesWithoutReferences extends SCThingThatCanBeOfferedWi
/**
* Frequency of the date series
* @filterable
* @elasticsearch filterable
*/
repeatFrequency?: SCISO8601Duration;
@@ -70,7 +69,8 @@ export interface SCDateSeriesWithoutReferences extends SCThingThatCanBeOfferedWi
translations?: SCTranslations<SCDateSeriesTranslatableProperties>;
/**
* Type of a date series
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.DateSeries;
}
@@ -78,7 +78,7 @@ export interface SCDateSeriesWithoutReferences extends SCThingThatCanBeOfferedWi
/**
* A date series
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCDateSeries
extends SCDateSeriesWithoutReferences,
@@ -100,7 +100,8 @@ export interface SCDateSeries
translations?: SCTranslations<SCDateSeriesTranslatableProperties>;
/**
* Type of a date series
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.DateSeries;
}

View File

@@ -38,7 +38,8 @@ export interface SCDiffWithoutReferences extends SCThingWithoutReferences {
dateCreated: SCISO8601Date;
/**
* Type of a diff
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Diff;
}
@@ -54,7 +55,8 @@ export interface SCDiff extends SCDiffWithoutReferences, SCThing {
object: SCIndexableThings;
/**
* Type of a diff
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Diff;
}

View File

@@ -38,8 +38,7 @@ export interface SCDishWithoutReferences
SCThingWithCategoriesWithoutReferences<SCDishCategories, SCThingWithCategoriesSpecificValues> {
/**
* Additives of the dish
* @filterable
* @keyword
* @elasticsearch filterable
*/
additives?: string[];
@@ -64,7 +63,8 @@ export interface SCDishWithoutReferences
translations?: SCTranslations<SCDishTranslatableProperties>;
/**
* Type of a dish
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Dish;
}
@@ -72,7 +72,7 @@ export interface SCDishWithoutReferences
/**
* A dish
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCDish
extends SCDishWithoutReferences,
@@ -94,7 +94,8 @@ export interface SCDish
translations?: SCTranslations<SCDishTranslatableProperties>;
/**
* Type of a dish
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Dish;
}
@@ -104,8 +105,7 @@ export interface SCDishTranslatableProperties
SCThingThatCanBeOfferedTranslatableProperties {
/**
* Additives of the dish
* @filterable
* @keyword
* @elasticsearch filterable
*/
additives?: string[];
/**
@@ -120,14 +120,12 @@ export interface SCDishTranslatableProperties
export interface SCDishCharacteristic {
/**
* URL of an image of the characteristic
* @keyword
*/
image?: string;
/**
* Name of the characteristic
* @filterable
* @text
* @elasticsearch text filterable
*/
name: string;
}
@@ -144,43 +142,36 @@ export type SCDishCategories = 'appetizer' | 'salad' | 'main dish' | 'dessert' |
export interface SCNutritionInformation {
/**
* Number of calories contained (in kcal)
* @float
*/
calories?: number;
/**
* Content of carbohydrates (in grams)
* @float
*/
carbohydrateContent?: number;
/**
* Content of fat (in grams)
* @float
*/
fatContent?: number;
/**
* Content of proteins (in grams)
* @float
*/
proteinContent?: number;
/**
* Content of salt (in grams)
* @float
*/
saltContent?: number;
/**
* Content of saturated fat (in grams)
* @float
*/
saturatedFatContent?: number;
/**
* Content of sugar (in grams)
* @float
*/
sugarContent?: number;
}

View File

@@ -20,7 +20,8 @@ import {SCThingMeta, SCThingType} from './abstract/thing.js';
*/
export interface SCFavoriteWithoutReferences extends SCSaveableThingWithoutReferences {
/**
* Type of a favorite
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Favorite;
}
@@ -31,7 +32,8 @@ export interface SCFavoriteWithoutReferences extends SCSaveableThingWithoutRefer
*/
export interface SCFavorite extends SCSaveableThing {
/**
* Type of a favorite
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Favorite;
}

View File

@@ -30,7 +30,7 @@ import {SCRoomWithoutReferences} from './room.js';
export interface SCFloorWithoutReferences extends SCThingWithoutReferences {
/**
* Floor name in the place it is in e.g. "first floor", "ground floor". This doesn't reference the building name.
* @text
* @elasticsearch filterable
*/
floorName: string;
@@ -45,7 +45,8 @@ export interface SCFloorWithoutReferences extends SCThingWithoutReferences {
translations?: SCTranslations<SCFloorTranslatableProperties>;
/**
* Type of a floor
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Floor;
}
@@ -53,7 +54,7 @@ export interface SCFloorWithoutReferences extends SCThingWithoutReferences {
/**
* A floor
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCFloor extends SCFloorWithoutReferences, SCThingInPlace {
/**
@@ -62,7 +63,8 @@ export interface SCFloor extends SCFloorWithoutReferences, SCThingInPlace {
translations?: SCTranslations<SCFloorTranslatableProperties>;
/**
* Type of a floor
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Floor;
}
@@ -95,7 +97,7 @@ export interface SCFloorFeatureWithPlace<T extends GeometryObject>
export interface SCFloorTranslatableProperties extends SCThingTranslatableProperties {
/**
* Translation of the floor name
* @text
* @elasticsearch filterable
*/
floorName?: string;
}

View File

@@ -40,7 +40,7 @@ export interface SCIdCardWithoutReferences extends SCThingWithoutReferences {
/**
* A message
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCIdCard extends SCIdCardWithoutReferences, SCThing {
/**

View File

@@ -28,7 +28,8 @@ export interface SCJobPostingWithoutReferences
extends SCThingWithCategoriesWithoutReferences<SCJobCategories, SCThingWithCategoriesSpecificValues>,
SCInPlace {
/**
* Type of a job posting
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.JobPosting;
}
@@ -36,14 +37,14 @@ export interface SCJobPostingWithoutReferences
/**
* A JobPosting
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCJobPosting
extends SCThingWithCategories<SCJobCategories, SCThingWithCategoriesSpecificValues>,
SCJobPostingWithoutReferences {
/**
* A description of the employer
* @text
* @elasticsearch filterable
*/
employerOverview?: SCOrganizationWithoutReferences;

View File

@@ -48,7 +48,7 @@ export interface SCMessageWithoutReferences
/**
* Roles for which the message is intended
* @filterable
* @elasticsearch filterable
*/
audiences: SCUserGroup[];
@@ -59,13 +59,13 @@ export interface SCMessageWithoutReferences
/**
* When the message was created
* @filterable
* @elasticsearch filterable
*/
dateCreated?: SCISO8601Date;
/**
* Message itself
* @text
* @elasticsearch filterable
*/
messageBody: string;
@@ -80,7 +80,8 @@ export interface SCMessageWithoutReferences
translations?: SCTranslations<SCMessageTranslatableProperties>;
/**
* Type of a message
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Message;
}
@@ -88,7 +89,7 @@ export interface SCMessageWithoutReferences
/**
* A message
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCMessage extends SCCreativeWork, SCMessageWithoutReferences {
/**
@@ -97,7 +98,8 @@ export interface SCMessage extends SCCreativeWork, SCMessageWithoutReferences {
translations?: SCTranslations<SCMessageTranslatableProperties>;
/**
* Type of a message
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Message;
}
@@ -110,7 +112,7 @@ export interface SCMessageTranslatableProperties
SCThingThatCanBeOfferedTranslatableProperties {
/**
* Message itself
* @text
* @elasticsearch filterable
*/
messageBody?: string;
}

View File

@@ -22,7 +22,8 @@ import {SCContactPointWithoutReferences} from './contact-point.js';
*/
export interface SCOrganizationWithoutReferences extends SCThingWithoutReferences {
/**
* Type of an organization
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Organization;
}
@@ -30,7 +31,7 @@ export interface SCOrganizationWithoutReferences extends SCThingWithoutReference
/**
* An organization
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCOrganization extends SCOrganizationWithoutReferences, SCThingInPlace {
/**
@@ -39,7 +40,8 @@ export interface SCOrganization extends SCOrganizationWithoutReferences, SCThing
contactPoints?: SCContactPointWithoutReferences[];
/**
* Type of an organization
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Organization;
}

View File

@@ -52,8 +52,7 @@ export interface SCPeriodicalWithoutReferences
categories: SCPeriodicalCategories[];
/**
* A list of ISSNs of a periodical
* @filterable
* @keyword
* @elasticsearch filterable
*/
ISSNs?: string[];
@@ -63,7 +62,8 @@ export interface SCPeriodicalWithoutReferences
translations?: SCTranslations<SCPeriodicalTranslatableFields>;
/**
* Type of a periodical
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Periodical;
}
@@ -71,7 +71,7 @@ export interface SCPeriodicalWithoutReferences
/**
* A publication published at regular intervals (e.g. a magazine or newspaper)
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCPeriodical
extends SCCreativeWork,
@@ -83,7 +83,8 @@ export interface SCPeriodical
translations?: SCTranslations<SCPeriodicalTranslatableFields>;
/**
* Type of a periodical
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Periodical;
}

View File

@@ -27,77 +27,67 @@ import {SCRoomWithoutReferences} from './room.js';
export interface SCPersonWithoutReferences extends SCThingWithoutReferences {
/**
* Additional first names of the person.
* @filterable
* @keyword
* @elasticsearch filterable
*/
additionalName?: string;
/**
* The birth date of the person.
* @filterable
* @elasticsearch filterable
*/
birthDate?: SCISO8601Date;
/**
* The private email address of the person.
* @TJS-format email
* @filterable
* @keyword
* @elasticsearch filterable
*/
email?: string;
/**
* The family name of the person.
* @filterable
* @keyword
* @elasticsearch filterable
*/
familyName?: string;
/**
* The private fax number of the person.
* @filterable
* @keyword
* @elasticsearch filterable
*/
faxNumber?: string;
/**
* The gender of the person.
* @filterable
* @elasticsearch filterable
*/
gender?: SCPersonGender;
/**
* The first name of the person.
* @filterable
* @keyword
* @elasticsearch filterable
*/
givenName?: string;
/**
* Honorific prefix of the person.
* @filterable
* @keyword
* @elasticsearch filterable
*/
honorificPrefix?: string;
/**
* Honorific suffix of the person.
* @filterable
* @keyword
* @elasticsearch filterable
*/
honorificSuffix?: string;
/**
* Titles of jobs that the person has.
* @filterable
* @keyword
* @elasticsearch filterable
*/
jobTitles?: string[];
/**
* The complete name of the person combining all the parts of the name into one.
* @filterable
* @text
* @elasticsearch text filterable
*/
name: string;
@@ -108,12 +98,12 @@ export interface SCPersonWithoutReferences extends SCThingWithoutReferences {
/**
* The private telephone number of the person.
* @keyword
*/
telephone?: string;
/**
* Type of a person
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Person;
}
@@ -121,7 +111,7 @@ export interface SCPersonWithoutReferences extends SCThingWithoutReferences {
/**
* A person
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCPerson extends SCPersonWithoutReferences, SCThing {
/**
@@ -137,7 +127,8 @@ export interface SCPerson extends SCPersonWithoutReferences, SCThing {
>;
/**
* Type of a person
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Person;

View File

@@ -39,7 +39,8 @@ export interface SCPointOfInterestWithoutReferences
translations?: SCTranslations<SCThingWithCategoriesTranslatableProperties>;
/**
* Type of a point of interest
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.PointOfInterest;
}
@@ -47,7 +48,7 @@ export interface SCPointOfInterestWithoutReferences
/**
* A point of interest
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCPointOfInterest
extends SCPointOfInterestWithoutReferences,
@@ -60,7 +61,8 @@ export interface SCPointOfInterest
translations?: SCTranslations<SCThingWithCategoriesTranslatableProperties>;
/**
* Type of a point of interest
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.PointOfInterest;
}

View File

@@ -36,7 +36,8 @@ export interface SCPublicationEventWithoutReferences extends SCEventWithoutRefer
translations?: SCTranslations<SCPublicationEventTranslatableProperties>;
/**
* Type of an publication event
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.PublicationEvent;
}
@@ -44,7 +45,7 @@ export interface SCPublicationEventWithoutReferences extends SCEventWithoutRefer
/**
* An publication event
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCPublicationEvent extends SCEvent, SCPublicationEventWithoutReferences {
/**
@@ -53,7 +54,8 @@ export interface SCPublicationEvent extends SCEvent, SCPublicationEventWithoutRe
translations?: SCTranslations<SCPublicationEventTranslatableProperties>;
/**
* Type of an publication event
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.PublicationEvent;
}

View File

@@ -13,7 +13,6 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {SCMetaTranslations, SCTranslations} from '../general/i18n.js';
import {SCMap} from '../general/map.js';
import {SCPlace, SCPlaceWithoutReferences, SCPlaceWithoutReferencesMeta} from './abstract/place.js';
import {SCThingMeta, SCThingType} from './abstract/thing.js';
@@ -58,15 +57,14 @@ export interface SCRoomWithoutReferences
SCThingWithCategoriesWithoutReferences<SCRoomCategories, SCRoomSpecificValues> {
/**
* The name of the floor in which the room is in.
* @filterable
* @text
* @elasticsearch text filterable
*/
floorName?: string;
/**
* The inventory of the place/room as a list of items and their quantity.
*/
inventory?: SCMap<number>;
inventory?: Record<string, number>;
/**
* Translations of specific values of the object
@@ -76,7 +74,8 @@ export interface SCRoomWithoutReferences
translations?: SCTranslations<SCThingWithCategoriesTranslatableProperties>;
/**
* Type of the room
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Room;
}
@@ -84,7 +83,7 @@ export interface SCRoomWithoutReferences
/**
* A room
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCRoom
extends SCRoomWithoutReferences,
@@ -100,7 +99,8 @@ export interface SCRoom
translations?: SCTranslations<SCThingWithCategoriesTranslatableProperties>;
/**
* Type of the room
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Room;
}
@@ -111,14 +111,12 @@ export interface SCRoom
export interface SCRoomSpecificValues extends SCThingWithCategoriesSpecificValues {
/**
* Category specific opening hours of the room
* @keyword
*/
openingHours?: string;
/**
* Category specific service hours of the room (e.g. cooked food serving hours)
* @see http://wiki.openstreetmap.org/wiki/Key:opening_hours/specification
* @keyword
*/
serviceHours?: string;
}

View File

@@ -26,14 +26,13 @@ import {SCThingMeta, SCThingType} from './abstract/thing.js';
export interface SCSemesterWithoutReferences extends SCAcademicTermWithoutReferences {
/**
* The short name of the semester, using the given pattern.
* @filterable
* @pattern ^(WS|SS|WiSe|SoSe) [0-9]{4}(/[0-9]{2})?$
* @keyword
* @elasticsearch filterable
*/
acronym: string;
/**
* Type of the semester
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Semester;
}
@@ -41,11 +40,12 @@ export interface SCSemesterWithoutReferences extends SCAcademicTermWithoutRefere
/**
* A semester
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCSemester extends SCSemesterWithoutReferences, SCAcademicTerm {
/**
* Type of the semester
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Semester;
}

View File

@@ -109,7 +109,6 @@ export type SCSettingValues = SCSettingValue[];
export interface SCSettingValueTranslatableProperties extends SCThingWithCategoriesTranslatableProperties {
/**
* The translations of the possible values of a setting
* @keyword
*/
values?: string[];
}

View File

@@ -21,7 +21,8 @@ import {SCThingMeta, SCThingType} from './abstract/thing.js';
*/
export interface SCSportCourseWithoutReferences extends SCEventWithoutReferences {
/**
* Type of a sport course
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.SportCourse;
}
@@ -29,11 +30,12 @@ export interface SCSportCourseWithoutReferences extends SCEventWithoutReferences
/**
* A sport course
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCSportCourse extends SCEvent, SCSportCourseWithoutReferences {
/**
* Type of a sport course
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.SportCourse;
}

View File

@@ -13,7 +13,6 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {SCLanguage, SCMetaTranslations, SCTranslations} from '../general/i18n.js';
import {SCMap} from '../general/map.js';
import {SCThingMeta, SCThingType} from './abstract/thing.js';
import {
SCAcademicPriceGroup,
@@ -32,7 +31,6 @@ import {SCPersonWithoutReferences} from './person.js';
export interface SCStudyModuleWithoutReferences extends SCThingThatCanBeOfferedWithoutReferences {
/**
* ECTS points (European Credit Transfer System)
* @float
*/
ects: number;
@@ -43,15 +41,14 @@ export interface SCStudyModuleWithoutReferences extends SCThingThatCanBeOfferedW
/**
* Majors that this study module is meant for
* @filterable
* @keyword
* @elasticsearch filterable
*/
majors: string[];
/**
* Represents the modules necessity for each given major (of the major property)
*/
necessity: SCMap<SCStudyModuleNecessity>;
necessity: Record<string, SCStudyModuleNecessity>;
/**
* Translated fields of a study module
@@ -59,7 +56,9 @@ export interface SCStudyModuleWithoutReferences extends SCThingThatCanBeOfferedW
translations?: SCTranslations<SCStudyModuleTranslatableProperties>;
/**
* Type of the study module
* Type discriminator
* @elasticsearch type
* @elasticsearch type
*/
type: SCThingType.StudyModule;
}
@@ -67,7 +66,7 @@ export interface SCStudyModuleWithoutReferences extends SCThingThatCanBeOfferedW
/**
* A study module
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCStudyModule
extends SCStudyModuleWithoutReferences,
@@ -104,7 +103,8 @@ export interface SCStudyModule
translations?: SCTranslations<SCStudyModuleTranslatableProperties>;
/**
* Type of the study module
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.StudyModule;
}
@@ -112,14 +112,13 @@ export interface SCStudyModule
export interface SCStudyModuleTranslatableProperties extends SCThingThatCanBeOfferedTranslatableProperties {
/**
* Translations of the majors that this study module is meant for
* @keyword
*/
majors?: string[];
/**
* Translations of the modules necessity for each given major (of the major property)
*/
necessity: SCMap<SCStudyModuleNecessity>;
necessity: Record<string, SCStudyModuleNecessity>;
}
/**

View File

@@ -28,7 +28,6 @@ export interface SCTicketWithoutReferences extends SCThingWithoutReferences {
/**
* Waiting number of the ticket
* @keyword
*/
currentTicketNumber: string;
@@ -38,7 +37,8 @@ export interface SCTicketWithoutReferences extends SCThingWithoutReferences {
serviceType: string;
/**
* Type of a ticket
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Ticket;
}
@@ -46,11 +46,12 @@ export interface SCTicketWithoutReferences extends SCThingWithoutReferences {
/**
* A ticket
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCTicket extends SCTicketWithoutReferences, SCThingInPlace {
/**
* Type of a ticket
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Ticket;
}

View File

@@ -35,7 +35,7 @@ export interface SCToDoWithoutReferences
/**
* A date when the "to do" is due
* @filterable
* @elasticsearch filterable
*/
dueDate?: SCISO8601Date;
@@ -45,7 +45,8 @@ export interface SCToDoWithoutReferences
priority: SCToDoPriority;
/**
* Type of the "to do"
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.ToDo;
}
@@ -53,7 +54,7 @@ export interface SCToDoWithoutReferences
/**
* A "to do"
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCToDo
extends SCToDoWithoutReferences,
@@ -65,7 +66,8 @@ export interface SCToDo
translations?: SCTranslations<SCThingWithCategoriesTranslatableProperties>;
/**
* Type of the "to do"
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.ToDo;
}

View File

@@ -21,7 +21,7 @@ import {SCThing, SCThingMeta, SCThingType, SCThingWithoutReferences} from './abs
export interface SCTourWithoutReferences extends SCThingWithoutReferences {
/**
* Init script for the tour
* @text
* @elasticsearch filterable
*/
init?: string;
@@ -31,8 +31,8 @@ export interface SCTourWithoutReferences extends SCThingWithoutReferences {
steps: SCTourStep[];
/**
* Type of a tour
* @keyword
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Tour;
}
@@ -40,11 +40,12 @@ export interface SCTourWithoutReferences extends SCThingWithoutReferences {
/**
* A tour
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCTour extends SCTourWithoutReferences, SCThing {
/**
* Type of a tour
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Tour;
}
@@ -95,12 +96,12 @@ export type SCTourStep = SCTourStepMenu | SCTourStepLocation | SCTourStepTooltip
export interface SCTourStepLocation {
/**
* Location to go to
* @keyword
*/
location: string;
/**
* Type of the step
* Type discriminator
* @elasticsearch type
*/
type: 'location';
}
@@ -116,7 +117,6 @@ export interface SCTourStepTooltip {
/**
* Element that the tooltip shall be pointing at or a list of elements to try in the specified order
* @keyword
*/
element: string | string[];
@@ -132,7 +132,7 @@ export interface SCTourStepTooltip {
/**
* Text that the tooltip shall contain
* @text
* @elasticsearch filterable
*/
text: string;
@@ -143,7 +143,8 @@ export interface SCTourStepTooltip {
tries?: number;
/**
* Type of the step
* Type discriminator
* @elasticsearch type
*/
type: 'tooltip';
}
@@ -174,7 +175,6 @@ export interface SCTourStepMenu {
export interface SCTourResolvedElement {
/**
* Element name
* @keyword
*/
element: string;
}
@@ -185,7 +185,6 @@ export interface SCTourResolvedElement {
export interface SCTourResolvedEvent {
/**
* Event name
* @keyword
*/
event: string;
}
@@ -206,7 +205,6 @@ export interface SCTourResolvedLocation {
export interface SCTourResolvedLocationTypeIs {
/**
* Specific location name
* @keyword
*/
is: string;
}
@@ -217,7 +215,6 @@ export interface SCTourResolvedLocationTypeIs {
export interface SCTourResolvedLocationTypeMatch {
/**
* Regex location name
* @keyword
*/
match: string;
}

View File

@@ -48,7 +48,6 @@ export interface SCVideoWithoutReferences
/**
* URLs to a thumbnails for the Video
* @keyword
*/
thumbnails?: string[];
@@ -59,7 +58,7 @@ export interface SCVideoWithoutReferences
/**
* A Transcript of the Video
* @text
* @elasticsearch filterable
*/
transcript?: string;
@@ -69,7 +68,8 @@ export interface SCVideoWithoutReferences
translations?: SCTranslations<SCVideoTranslatableFields>;
/**
* Type of an Video
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Video;
}
@@ -83,7 +83,7 @@ export interface SCVideoSource {
/**
* MIME-Type of the source File
* @filterable
* @elasticsearch filterable
*/
mimeType: SCVideoMimeType;
@@ -95,7 +95,6 @@ export interface SCVideoSource {
/**
* URL to the Video File
* @keyword
*/
url: string;
@@ -114,13 +113,12 @@ export interface SCVideoTrack {
/**
* Content Type of the Track File
* @filterable
* @elasticsearch filterable
*/
type: SCVideoTrackTypes;
/**
* URL to the Track File
* @keyword
*/
url: string;
}
@@ -128,7 +126,7 @@ export interface SCVideoTrack {
/**
* A video
* @validatable
* @indexable
* @elasticsearch indexable
*/
export interface SCVideo
extends SCCreativeWork,
@@ -145,7 +143,8 @@ export interface SCVideo
translations?: SCTranslations<SCVideoTranslatableFields>;
/**
* Type of a video
* Type discriminator
* @elasticsearch type
*/
type: SCThingType.Video;
}

View File

@@ -1,13 +1,9 @@
import {validateFiles, writeReport} from '@openstapps/core-tools';
import {expect} from 'chai';
import {mkdir} from 'fs/promises';
import path from 'path';
describe('Schema', function () {
this.timeout(15_000);
this.slow(10_000);
it('should validate against test files', async function () {
// TODO
/*it('should validate against test files', async function () {
const errorsPerFile = await validateFiles(
path.resolve('lib', 'schema'),
path.resolve('test', 'resources'),
@@ -21,5 +17,5 @@ describe('Schema', function () {
expect(error.expected).to.be.true;
}
}
});
});*/
});

View File

@@ -0,0 +1,16 @@
import {defineConfig} from 'tsup';
import {jsonSchemaPlugin} from '@openstapps/json-schema-generator';
import {openapiPlugin} from '@openstapps/openapi-generator';
import {elasticsearchMappingGenerator} from '@openstapps/es-mapping-generator';
export default defineConfig({
entry: ['src/index.ts'],
sourcemap: true,
clean: true,
format: 'esm',
outDir: 'lib',
plugins: [
jsonSchemaPlugin('index.schema.json', elasticsearchMappingGenerator('elasticsearch.json')),
openapiPlugin('openapi.json', 'index.schema.json'),
],
});

View File

@@ -1,2 +0,0 @@
#!/usr/bin/env node
require('./lib/app.js');

View File

@@ -2,14 +2,11 @@
"name": "@openstapps/es-mapping-generator",
"description": "Tool to convert TypeScript Interfaces to Elasticsearch Mappings",
"version": "3.0.0",
"type": "commonjs",
"type": "module",
"license": "GPL-3.0-only",
"author": "Thea Schöbl <dev@theaninova.de>",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"bin": {
"openstapps-es-mapping-generator": "app.js"
},
"files": [
"app.js",
"lib",
@@ -18,7 +15,8 @@
"CHANGELOG.md"
],
"scripts": {
"build": "rimraf lib && tsc",
"build": "tsup-node --dts",
"dev": "tsup --watch --onSuccess \"node lib/index.js\"",
"format": "prettier . -c --ignore-path ../../.gitignore",
"format:fix": "prettier --write . --ignore-path ../../.gitignore",
"lint": "eslint --ext .ts src/ test/",
@@ -26,28 +24,33 @@
"test": "c8 mocha"
},
"dependencies": {
"@elastic/elasticsearch": "8.4.0",
"commander": "10.0.0",
"deepmerge": "4.3.1",
"flatted": "3.2.7",
"typedoc": "0.18.0",
"typescript": "3.8.3"
"@elastic/elasticsearch": "8.10.0",
"@openstapps/json-schema-generator": "workspace:*",
"@openstapps/tsup-plugin": "workspace:*",
"@types/json-schema": "7.0.14",
"ajv": "8.12.0",
"better-ajv-errors": "1.2.0"
},
"devDependencies": {
"@openstapps/eslint-config": "workspace:*",
"@openstapps/prettier-config": "workspace:*",
"@testdeck/mocha": "0.3.3",
"@openstapps/tsconfig": "workspace:*",
"@types/chai": "4.3.5",
"@types/fs-extra": "9.0.13",
"@types/glob": "8.0.1",
"@types/mocha": "10.0.1",
"@types/node": "14.18.38",
"@types/rimraf": "3.0.2",
"@types/mustache": "4.2.2",
"@types/node": "18.15.3",
"c8": "7.14.0",
"chai": "4.3.7",
"esbuild": "0.17.19",
"mocha": "10.2.0",
"mocha-junit-reporter": "2.2.0",
"nock": "13.3.1",
"rimraf": "5.0.0",
"ts-node": "10.9.1"
"ts-node": "10.9.1",
"tsup": "6.7.0",
"typedoc": "0.24.8",
"typescript": "5.1.6"
},
"prettier": "@openstapps/prettier-config",
"eslintConfig": {

View File

@@ -1,99 +0,0 @@
/*
* Copyright (C) 2018-2021 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Command} from 'commander';
import {copyFileSync, mkdirSync, readFileSync, writeFileSync} from 'fs';
import path from 'path';
import {generateTemplate} from './mapping';
import {getProjectReflection} from './project-reflection';
// handle unhandled promise rejections
process.on('unhandledRejection', async (reason: unknown) => {
if (reason instanceof Error) {
await console.error(reason.message);
console.info(reason.stack);
}
process.exit(1);
});
const commander = new Command('openstapps-core-tools');
// eslint-disable-next-line unicorn/prefer-module
commander.version(JSON.parse(readFileSync(path.resolve(__dirname, '..', 'package.json')).toString()).version);
commander
.command('mapping <relativeSrcPath>')
.option('-m, --mappingPath <relativeMappingPath>', 'Mapping Path')
.option('-i, --ignoredTags <ignoredTags>', 'Ignored Tags (comma-separated)')
.option('-a, --aggPath <relativeAggregationPath>', 'Aggregations Path')
.option('-e, --errorPath <relativeErrorPath>', 'Error Path')
.action(async (relativeSourcePath, options) => {
// get absolute paths
const sourcePath = path.resolve(relativeSourcePath);
let ignoredTagsList: string[] = [];
if (typeof options.ignoredTags === 'string') {
ignoredTagsList = options.ignoredTags.split(',');
}
// get project reflection
const projectReflection = getProjectReflection(sourcePath);
const result = generateTemplate(projectReflection, ignoredTagsList, true);
if (result.errors.length > 0) {
await console.error('Mapping generated with errors!');
} else {
console.log('Mapping generated without errors!');
}
// write documentation to file
if (options.aggPath !== undefined) {
const aggPath = path.resolve(options.aggPath);
mkdirSync(path.dirname(aggPath), {recursive: true});
// tslint:disable-next-line:no-magic-numbers
writeFileSync(aggPath, JSON.stringify(result.aggregations, null, 2));
copyFileSync(
// eslint-disable-next-line unicorn/prefer-module
require.resolve('../schema/aggregations.d.ts'),
path.join(path.dirname(aggPath), 'aggregations.json.d.ts'),
);
console.log(`Elasticsearch aggregations written to ${aggPath}.`);
}
if (options.mappingPath !== undefined) {
const mappingPath = path.resolve(options.mappingPath);
mkdirSync(path.dirname(mappingPath), {recursive: true});
writeFileSync(mappingPath, JSON.stringify(result.mappings, null, 2));
copyFileSync(
// eslint-disable-next-line unicorn/prefer-module
require.resolve('../schema/mappings.d.ts'),
path.join(path.dirname(mappingPath), 'mappings.json.d.ts'),
);
console.log(`Elasticsearch mappings written to ${mappingPath}.`);
}
if (options.errorPath !== undefined) {
const errorPath = path.resolve(options.errorPath);
mkdirSync(path.dirname(errorPath), {recursive: true});
// tslint:disable-next-line:no-magic-numbers
writeFileSync(errorPath, JSON.stringify(result.errors, null, 2));
console.log(`Mapping errors written to ${errorPath}.`);
} else if (result.errors.length > 0) {
for (const error of result.errors) {
await console.error(error);
}
throw new Error('Mapping generation failed');
}
});
commander.parse(process.argv);

View File

@@ -1,55 +0,0 @@
/*
* Copyright (C) 2019-2021 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {MappingProperty} from '@elastic/elasticsearch/lib/api/types';
import type {ElasticsearchFieldmap, SimpleType} from '../../schema/mappings.js';
const ducetSort = {
type: 'icu_collation_keyword',
language: 'de',
country: 'DE',
variant: '@collation=phonebook',
};
const keyword: MappingProperty['type'] = 'keyword';
export const fieldmap: ElasticsearchFieldmap = {
aggregatable: {
default: {
raw: {
ignore_above: 10_000,
type: keyword,
},
},
ignore: ['global'],
},
sortable: {
default: {
sort: ducetSort,
},
ducet: {
sort: ducetSort,
},
ignore: ['price'],
},
};
export const filterableTagName = 'filterable';
export const filterableMap: Record<string, SimpleType> = {
date: 'keyword',
keyword: 'keyword',
text: 'keyword',
integer: 'integer',
};

View File

@@ -1,64 +0,0 @@
/*
* Copyright (C) 2019-2021 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {MappingProperty} from '@elastic/elasticsearch/lib/api/types';
export const premaps: Record<string, MappingProperty> = {
'CoordinateReferenceSystem': {
dynamic: true,
properties: {
type: {
type: 'keyword',
},
},
},
'LineString': {
type: 'geo_shape',
},
'Point': {
properties: {
type: {
type: 'keyword',
},
coordinates: {
type: 'geo_point',
},
},
dynamic: 'strict',
},
'Polygon': {
type: 'geo_shape',
},
'SCISO8601DateRange': {
type: 'date_range',
},
'jsonpatch.OpPatch': {
dynamic: 'strict',
properties: {
from: {
type: 'keyword',
},
op: {
type: 'keyword',
},
path: {
type: 'keyword',
},
value: {
// this is actually an 'any' type; however, ES does not really support that.
type: 'keyword',
},
},
},
};

View File

@@ -1,62 +0,0 @@
/*
* Copyright (C) 2019-2021 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {MappingFloatNumberProperty} from '@elastic/elasticsearch/lib/api/types';
import type {ElasticsearchTypemap} from '../../schema/mappings';
export const PARSE_ERROR = 'PARSE_ERROR' as MappingFloatNumberProperty['type'];
export const MISSING_PREMAP = 'MISSING_PREMAP' as MappingFloatNumberProperty['type'];
export const TYPE_CONFLICT = 'TYPE_CONFLICT' as MappingFloatNumberProperty['type'];
export const typemap: ElasticsearchTypemap = {
boolean: {
default: 'boolean',
},
false: {
default: 'boolean',
},
number: {
default: 'integer',
float: 'float',
integer: 'integer',
date: 'date',
},
string: {
default: 'text',
keyword: 'keyword',
text: 'text',
date: 'date',
},
stringLiteral: {
default: 'keyword',
},
true: {
default: 'boolean',
},
};
/**
* If the string is a tag type
*/
export function isTagType(string_: string): boolean {
for (const key in typemap) {
if (typemap.hasOwnProperty(key) && typemap[key][string_] !== undefined) {
return true;
}
}
return false;
}
export const dynamicTypes = ['any', 'unknown'];

View File

@@ -0,0 +1,60 @@
import {MappingProperty, SearchRequest} from '@elastic/elasticsearch/lib/api/types.js';
import Ajv from 'ajv';
import {readFile} from 'fs/promises';
import {fileURLToPath} from 'url';
import path from 'path';
import {Context} from '../generator/context.js';
/**
* @validatable
*/
export interface ElasticsearchOptionsDSL {
/**
* Mark an interface as indexable
*/
indexable?: true;
/**
* Inherit customization options from another item
*/
extends?: string[];
/**
* Completely override the property
*/
override?: MappingProperty;
/**
* Merge property values
*/
merge?: MappingProperty;
/**
* Modify the search request
*
* Supports `{name}`, `{type}` and `{prop}` templates substitutions anywhere
*/
search?: Partial<SearchRequest>;
}
const schema = JSON.parse(
await readFile(path.join(path.dirname(fileURLToPath(import.meta.url)), 'index.schema.json'), 'utf8'),
);
const ajv = new Ajv.default({schemas: [schema], allowUnionTypes: true});
/**
* Validate that the options are valid
*/
export function validateElasticsearchOptionsDsl(
context: Context,
value: unknown,
): value is ElasticsearchOptionsDSL {
return ajv.validate('#/definitions/ElasticsearchOptionsDSL', value)
? true
: context.bail(JSON.stringify(ajv.errors));
}
/**
* Validate that the mapping result is correct
*/
export function validateMappingResult(context: Context, value: unknown): value is MappingProperty {
return ajv.validate('#/definitions/MappingProperty', value)
? true
: context.bail(JSON.stringify(ajv.errors));
}

View File

@@ -0,0 +1,47 @@
import {JSONSchema7} from 'json-schema';
import {MappingProperty} from '@elastic/elasticsearch/lib/api/types.js';
import {transformObject} from './transformers/object.js';
import {transformString} from './transformers/string.js';
import {Context} from './context.js';
import {transformDefinition} from './definition.js';
/**
* Transform JSONSchema without applying custom tag logic
*/
export function transformBase(context: Context, definition: JSONSchema7): MappingProperty {
if (definition.anyOf) {
return context.resolveUnion(definition.anyOf as JSONSchema7[]);
}
switch (definition.type) {
case 'array': {
if (Array.isArray(definition.items)) {
return context.resolveUnion(definition.items as JSONSchema7[]);
} else if (typeof definition.items === 'object') {
return transformDefinition(context, definition.items);
} else {
return context.bail(`Not implemented array type ${typeof definition.items}`);
}
}
case 'object': {
return transformObject(context, definition);
}
case 'string': {
return transformString(definition);
}
case 'number': {
return {type: 'float'};
}
case 'integer': {
return {type: 'integer'};
}
case 'boolean': {
return {type: 'boolean'};
}
default: {
return {
dynamic: false,
};
}
}
}

View File

@@ -0,0 +1,115 @@
import {JSONSchema7} from 'json-schema';
import {transformDefinition} from './definition.js';
import crypto from 'crypto';
import {
MappingProperty,
MappingDynamicProperty,
SearchRequest,
} from '@elastic/elasticsearch/lib/api/types.js';
import deepmerge from 'deepmerge';
import {MappingGenerator, sanitizeTypeName} from './mapping-generator.js';
import {renderTemplate} from '../template.js';
/**
* Get the name from a $ref such as `#/definitions/SCThing`
*/
function getNameFromRef(ref: string): string {
return decodeURI(ref).replace(/^#\/definitions\//, '');
}
export class Context {
constructor(
readonly generator: MappingGenerator,
readonly thingType: string,
readonly path: string[],
readonly propertyPath: string[],
readonly dependencies: Map<string, Set<string>>,
) {}
resolveReference(reference: string): MappingProperty {
return this.deriveContext(reference)[1];
}
resolveUnion(types: JSONSchema7[]): MappingDynamicProperty {
for (const type of types) {
const [name] = this.deriveContext(type.$ref ?? type);
this.addDependency(name, this.propertyPath.join('.'));
}
return {
type: '{dynamic_property}',
};
}
private deriveContext(reference: string | JSONSchema7): [dependencyName: string, mapping: MappingProperty] {
let referenceName = typeof reference === 'string' ? getNameFromRef(reference) : undefined;
let definition = typeof reference === 'string' ? undefined : reference;
if (!definition && !this.generator.cache.has(referenceName!)) {
definition = this.generator.project.definitions![referenceName!] as JSONSchema7;
if (typeof definition !== 'object') this.bail(`Invalid path ${referenceName!}`);
}
if (!referenceName || !this.generator.cache.has(referenceName)) {
const derivedContext = new Context(
this.generator,
this.thingType,
referenceName ? [referenceName] : [],
this.propertyPath,
new Map(),
);
const result = transformDefinition(derivedContext, definition);
referenceName ??= crypto.createHash('md5').update(JSON.stringify(result)).digest('hex');
this.generator.cache.set(referenceName, {mapping: result, dependencies: derivedContext.dependencies});
}
const {mapping, dependencies} = this.generator.cache.get(referenceName)!;
for (const [name, paths] of dependencies) {
for (const path of paths) {
this.addDependency(name, [...this.propertyPath.slice(0, -1), path].join('.'));
}
}
return [referenceName, mapping];
}
registerSearchMod(modification: Partial<SearchRequest>) {
this.generator.searchMods.mods = deepmerge<Partial<SearchRequest>>(
this.generator.searchMods.mods,
renderTemplate(modification, [
['{name}', this.path[0]],
['{prop}', this.propertyPath.join('.')],
['{type}', this.thingType],
['{_type}', sanitizeTypeName(this.thingType)],
]),
);
}
private addDependency(name: string, path: string) {
if (!this.dependencies.has(name)) {
this.dependencies.set(name, new Set([path]));
} else {
this.dependencies.get(name)!.add(path);
}
}
/**
* Step down into a property
*/
step(property: string): Context {
return new Context(
this.generator,
this.thingType,
[...this.path, property],
[...this.propertyPath, property],
this.dependencies,
);
}
/**
* Bail and throw
*/
bail(reason: string): never {
throw new Error(`${this.path.join('.')} ${reason}`);
}
}

View File

@@ -0,0 +1,32 @@
import {JSONSchema7} from 'json-schema';
import {MappingProperty} from '@elastic/elasticsearch/lib/api/types.js';
import {transformBase} from './base.js';
import {Context} from './context.js';
import deepmerge from 'deepmerge';
import {resolveDsl} from './dsl.js';
import {getTags, INDEXABLE_TAG_NAME} from './tags.js';
/**
* Transform JSONSchema
*/
export function transformDefinition(context: Context, definition: JSONSchema7): MappingProperty {
if (definition.$ref) return context.resolveReference(definition.$ref);
const tags = getTags(definition);
tags.delete(INDEXABLE_TAG_NAME);
let base = transformBase(context, definition);
if (tags.size > 0) {
const options = resolveDsl(context, {extends: [...tags]});
if (options.override) {
base = options.override;
}
if (options.merge) {
base = deepmerge<MappingProperty>(base, options.merge);
}
if (options.search) {
context.registerSearchMod(options.search);
}
}
return base;
}

View File

@@ -0,0 +1,48 @@
import {Context} from './context.js';
import {ElasticsearchOptionsDSL} from '../dsl/schema.js';
import deepmerge from 'deepmerge';
type ResolvedOptions = Omit<ElasticsearchOptionsDSL, 'extends'>;
/**
* Resolve DSL inheritance
*/
export function resolveDsl(
context: Context,
{extends: parents, ...result}: ElasticsearchOptionsDSL,
): ResolvedOptions {
for (const reference of parents ?? []) {
result = deepmerge<ResolvedOptions>(
result,
reference.startsWith('@')
? resolveReferencePath(context, reference)
: resolvePresetReference(context, reference),
);
}
return result;
}
/**
* Resolve preset references
*/
function resolvePresetReference(context: Context, reference: string): ResolvedOptions {
if (!context.generator.presets.has(reference)) return context.bail(`Missing preset ${reference}`);
return resolveDsl(context, context.generator.presets.get(reference)!);
}
/**
* Resolve @references
*/
function resolveReferencePath(context: Context, reference: string): ResolvedOptions {
const [type, ...path] = reference.replace(/^@/, '').split('.');
let declaration = context.resolveReference(type);
while (path.length > 0) {
const property = path.shift()!;
if (!('properties' in declaration && declaration.properties && property in declaration.properties))
context.bail(`Invalid reference ${reference}`);
declaration = declaration.properties[property];
}
return {override: declaration};
}

View File

@@ -0,0 +1,171 @@
import type {JSONSchema7} from 'json-schema';
import {ElasticsearchOptionsDSL} from '../dsl/schema.js';
import {IndicesPutTemplateRequest, MappingProperty} from '@elastic/elasticsearch/lib/api/types.js';
import {MappingGenerator} from './mapping-generator.js';
import {getTags, INDEXABLE_TAG_NAME} from './tags.js';
export interface GeneratorOptions {
/**
* Presets you can extend
*/
presets: Record<string, ElasticsearchOptionsDSL>;
/**
* Override specific types
*/
overrides: Record<string, MappingProperty>;
/**
* Template for the generated index request
*
* Supports `{type}` and `{sanitized_type}` (same as `{type}`, but no spaces) template substitutions
*/
template: Partial<IndicesPutTemplateRequest>;
}
/**
* Fully transform a project
*/
export function transformProject(project: JSONSchema7) {
const context = new MappingGenerator(project, OPTIONS);
const results = [];
for (const name in project.definitions) {
const definition = project.definitions[name];
if (typeof definition !== 'object' || !getTags(definition).has(INDEXABLE_TAG_NAME)) continue;
results.push(context.buildTemplate(name));
}
return {
mappings: results,
search: context.searchMods.mods,
};
}
const OPTIONS: GeneratorOptions = {
template: {
name: 'template_{_type}',
index_patterns: 'stapps_{_type}*',
settings: {
'mapping.total_fields.limit': 10_000,
'max_result_window': 30_000,
'number_of_replicas': 0,
'number_of_shards': 1,
},
mappings: {
date_detection: false,
_source: {
excludes: ['creation_date'],
},
properties: {
creation_date: {
type: 'date',
},
},
},
},
overrides: {
'CoordinateReferenceSystem': {
dynamic: true,
properties: {
type: {
type: 'keyword',
},
},
},
'LineString': {
type: 'geo_shape',
},
'Point': {
properties: {
type: {
type: 'keyword',
},
coordinates: {
type: 'geo_point',
},
},
dynamic: 'strict',
},
'Polygon': {
type: 'geo_shape',
},
'SCISO8601DateRange': {
type: 'date_range',
},
'jsonpatch.OpPatch': {
dynamic: 'strict',
properties: {
from: {
type: 'keyword',
},
op: {
type: 'keyword',
},
path: {
type: 'keyword',
},
value: {
// this is actually an 'any' type; however, ES does not really support that.
type: 'keyword',
},
},
},
},
presets: {
'type': {extends: ['sortable:ducet', 'filterable', 'aggregatable:global']},
'text': {
merge: {type: 'text'},
},
'filterable': {
merge: {
fields: {
raw: {type: 'keyword'},
},
},
},
'sortable:ducet': {
merge: {
fields: {
sort: {
type: 'icu_collation_keyword',
language: 'de',
country: 'DE',
variant: '@collation=phonebook',
} as never,
},
},
},
'sortable': {
extends: ['sortable:ducet'],
},
'sortable:price': {},
'aggregatable': {
search: {
aggs: {
'{type}': {
aggs: {
'{prop}': {
terms: {
field: '{prop}.raw',
size: 1000,
},
},
},
filter: {term: {type: '{type}'}},
},
},
},
},
'aggregatable:global': {
search: {
aggs: {
'@all.{prop}': {
terms: {
field: '{prop}.raw',
size: 1000,
},
},
},
},
},
},
};

View File

@@ -0,0 +1,76 @@
import {GeneratorOptions} from './index.js';
import {JSONSchema7} from 'json-schema';
import {ElasticsearchOptionsDSL} from '../dsl/schema.js';
import {
IndicesPutTemplateRequest,
MappingProperty,
MappingTypeMapping,
SearchRequest,
} from '@elastic/elasticsearch/lib/api/types.js';
import {Context} from './context.js';
import deepmerge from 'deepmerge';
import {renderTemplate} from '../template.js';
/**
* Sanitize a type name
*/
export function sanitizeTypeName(typeName: string): string {
return typeName.replaceAll(' ', '_');
}
export class MappingGenerator {
readonly presets: Map<string, ElasticsearchOptionsDSL>;
readonly cache: Map<string, {mapping: MappingProperty; dependencies: Map<string, Set<string>>}>;
readonly searchMods: {mods: Partial<SearchRequest>} = {mods: {}};
readonly template: Partial<IndicesPutTemplateRequest>;
constructor(readonly project: JSONSchema7, options: GeneratorOptions) {
this.template = options.template ?? {};
this.presets = new Map(Object.entries(options.presets));
this.cache = new Map(
Object.entries(options.overrides).map(([name, mapping]) => [
name,
{
mapping,
dependencies: new Map(),
},
]),
);
}
buildTemplate(name: string): IndicesPutTemplateRequest {
const thingType = ((this.project.definitions![name] as JSONSchema7).properties!.type as JSONSchema7)
.const;
if (typeof thingType !== 'string') {
throw new TypeError(`${name} needs a valid thing type`);
}
const mappingContext = new Context(this, thingType, [name], [], new Map());
const mappings = mappingContext.resolveReference(name);
const request: IndicesPutTemplateRequest = deepmerge(
{mappings: mappings as MappingTypeMapping},
renderTemplate(this.template, [
['{name}', name],
['{type}', thingType],
['{_type}', sanitizeTypeName(thingType)],
]),
);
if (mappingContext.dependencies.size > 0) {
request.mappings!.dynamic_templates = [];
for (const [name, paths] of mappingContext.dependencies) {
request.mappings!.dynamic_templates.push({
[name]: {
path_match: paths.size > 1 ? [...paths] : paths.values().next().value,
mapping: mappingContext.resolveReference(name),
},
});
}
}
return request;
}
}

View File

@@ -0,0 +1,13 @@
import {JSONSchema7} from 'json-schema';
export const INDEXABLE_TAG_NAME = 'indexable';
/**
* Get elasticsearch tags
*/
export function getTags(definition: JSONSchema7): Set<string> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return new Set(
(definition as {elasticsearch: string}).elasticsearch?.split(/\s/).map(it => it.trim()) ?? [],
);
}

View File

@@ -0,0 +1,20 @@
import {JSONSchema7} from 'json-schema';
import {MappingProperty} from '@elastic/elasticsearch/lib/api/types.js';
import {transformDefinition} from '../definition.js';
import {Context} from '../context.js';
/**
* Transform a JSON Schema with `object` type
*/
export function transformObject(context: Context, definition: JSONSchema7): MappingProperty {
const value = {
dynamic: definition.additionalProperties === true ? undefined : 'strict',
properties: {} as Record<string, MappingProperty>,
} satisfies MappingProperty;
for (const key in definition.properties!) {
value.properties[key] = transformDefinition(context.step(key), definition.properties[key] as JSONSchema7);
}
return value;
}

View File

@@ -0,0 +1,19 @@
import {JSONSchema7} from 'json-schema';
import {MappingProperty} from '@elastic/elasticsearch/lib/api/types.js';
const stringFormats = new Map<string, MappingProperty>([
['date', {type: 'date', format: 'date'}],
['time', {type: 'date', format: 'time'}],
['date-time', {type: 'date', format: 'date_optional_time'}],
['ipv4', {type: 'ip'}],
['ipv6', {type: 'ip'}],
]);
/**
* Transform a JSON Schema with `string` type
*/
export function transformString(definition: JSONSchema7): MappingProperty {
return definition.format && stringFormats.has(definition.format)
? stringFormats.get(definition.format)!
: {type: 'keyword'};
}

View File

@@ -1,7 +1,16 @@
export * from './mapping';
export * from './project-reflection';
import {transformProject} from './generator/index.js';
import {SchemaConsumer} from '@openstapps/json-schema-generator';
export * from './config/premap';
export * from './config/fieldmap';
export * from './config/settings';
export * from './config/typemap';
/**
* JSON Schema Generator plugin for Elasticsearch Mappings
*/
export function elasticsearchMappingGenerator(fileName: string): [string, SchemaConsumer] {
return [
'Elasticsearch-Mappings',
function (schema) {
return {
[fileName]: JSON.stringify(transformProject(schema)),
};
},
];
}

Some files were not shown because too many files have changed in this diff Show More