From 0de613969e45192bbc32e68e140ad4d4de6ebb4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thea=20Sch=C3=B6bl?= Date: Wed, 1 Nov 2023 14:31:12 +0100 Subject: [PATCH] feat: json-schema updates\nfeat: new route proposal --- .../src/plugin/minimal-plugin.ts | 9 ++- .../src/plugin/protocol/request.ts | 1 - .../src/plugin/protocol/response.ts | 3 +- examples/minimal-plugin/src/schemas.d.ts | 5 ++ examples/minimal-plugin/tsup.config.ts | 6 +- packages/core/src/elasticsearch.ts | 61 +++++++++++++++++ packages/core/src/generators.d.ts | 10 +++ packages/core/src/protocol/route.ts | 17 ++--- packages/core/src/protocol/routes/bulk-add.ts | 8 ++- packages/core/src/protocol/routes/rating.ts | 41 ++++++------ .../core/src/protocol/routes/search-multi.ts | 43 ++++++------ packages/core/src/protocol/routes/search.ts | 41 ++++++------ .../core/src/protocol/routes/thing-update.ts | 51 +++++++------- packages/core/src/schemas.ts | 67 +++++++++++++++++++ packages/core/src/things/academic-event.ts | 4 +- packages/core/src/things/ticket.ts | 1 - packages/core/tsup.config.ts | 11 ++- .../src/generator/index.ts | 2 +- packages/es-mapping-generator/src/index.ts | 45 ++++++++++++- packages/json-schema-generator/src/index.ts | 32 +++++---- 20 files changed, 327 insertions(+), 131 deletions(-) create mode 100644 examples/minimal-plugin/src/schemas.d.ts create mode 100644 packages/core/src/elasticsearch.ts create mode 100644 packages/core/src/generators.d.ts create mode 100644 packages/core/src/schemas.ts diff --git a/examples/minimal-plugin/src/plugin/minimal-plugin.ts b/examples/minimal-plugin/src/plugin/minimal-plugin.ts index b78ad5c6..9b96f14f 100644 --- a/examples/minimal-plugin/src/plugin/minimal-plugin.ts +++ b/examples/minimal-plugin/src/plugin/minimal-plugin.ts @@ -14,9 +14,8 @@ */ import {Plugin} from '@openstapps/api-plugin'; import * as express from 'express'; -import schema from '../../lib/schema.json'; -import {SCMinimalRequest} from './protocol/request.js'; -import {SCMinimalResponse} from './protocol/response.js'; +import {requestSchema, SCMinimalRequest} from './protocol/request.js'; +import {responseSchema, SCMinimalResponse} from './protocol/response.js'; /** * The Plugin Class @@ -25,9 +24,9 @@ import {SCMinimalResponse} from './protocol/response.js'; * TODO: rename the class */ export class MinimalPlugin extends Plugin { - requestSchema = schema.SCMinimalRequest; + requestSchema = requestSchema; - responseSchema = schema.SCMinimalResponse; + responseSchema = responseSchema; /** * Calculates the sum of a list of numbers diff --git a/examples/minimal-plugin/src/plugin/protocol/request.ts b/examples/minimal-plugin/src/plugin/protocol/request.ts index a31bfca7..eeb4029d 100644 --- a/examples/minimal-plugin/src/plugin/protocol/request.ts +++ b/examples/minimal-plugin/src/plugin/protocol/request.ts @@ -19,7 +19,6 @@ * All incoming requests will look like this, this is being checked by the backend. You need to add the @validatable tag * like shown below for the plugin to work. The request can have any layout you like. * TODO: remove body of the interface and replace with your own layout - * @validatable */ export interface SCMinimalRequest { /** diff --git a/examples/minimal-plugin/src/plugin/protocol/response.ts b/examples/minimal-plugin/src/plugin/protocol/response.ts index 6bd91e60..68d4ad07 100644 --- a/examples/minimal-plugin/src/plugin/protocol/response.ts +++ b/examples/minimal-plugin/src/plugin/protocol/response.ts @@ -19,7 +19,6 @@ * All your responses to the backend are required to look like this. You need to add the @validatable tag like shown * below for the plugin to work. The response can have any layout you like. * TODO: remove body of the interface and replace with your own layout - * @validatable */ export interface SCMinimalResponse { /** @@ -28,4 +27,4 @@ export interface SCMinimalResponse { sum: number; } -export {default as requestSchema} from 'schema:#SCMinimalResponse'; +export {default as responseSchema} from 'schema:#SCMinimalResponse'; diff --git a/examples/minimal-plugin/src/schemas.d.ts b/examples/minimal-plugin/src/schemas.d.ts new file mode 100644 index 00000000..2a2f1d15 --- /dev/null +++ b/examples/minimal-plugin/src/schemas.d.ts @@ -0,0 +1,5 @@ +declare module 'schema:*' { + import {JSONSchema7} from 'json-schema'; + const schema: JSONSchema7; + export default schema; +} diff --git a/examples/minimal-plugin/tsup.config.ts b/examples/minimal-plugin/tsup.config.ts index 1f2c6b40..18b53f08 100644 --- a/examples/minimal-plugin/tsup.config.ts +++ b/examples/minimal-plugin/tsup.config.ts @@ -1,5 +1,5 @@ import {defineConfig} from 'tsup'; -import {jsonSchemaPlugin} from '@openstapps/json-schema-generator'; +import {esbuildJsonSchemaPlugin} from '@openstapps/json-schema-generator'; export default defineConfig({ entry: ['src/app.ts'], @@ -7,6 +7,6 @@ export default defineConfig({ clean: true, format: 'esm', outDir: 'lib', - noExternal: [/.*:schema#.*/], - plugins: [jsonSchemaPlugin('schema.json')], + noExternal: [/schema:.*/], + esbuildPlugins: [esbuildJsonSchemaPlugin], }); diff --git a/packages/core/src/elasticsearch.ts b/packages/core/src/elasticsearch.ts new file mode 100644 index 00000000..dcd2bff9 --- /dev/null +++ b/packages/core/src/elasticsearch.ts @@ -0,0 +1,61 @@ +import academicEventMapping from 'elasticsearch:./things/academic-event.js#SCAcademicEvent'; +import articleMapping from 'elasticsearch:./things/article.js#SCArticle'; +import assessmentMapping from 'elasticsearch:./things/assessment.js#SCAssessment'; +import bookMapping from 'elasticsearch:./things/book.js#SCBook'; +import buildingMapping from 'elasticsearch:./things/building.js#SCBuilding'; +import catalogMapping from 'elasticsearch:./things/catalog.js#SCCatalog'; +import certificationMapping from 'elasticsearch:./things/certification.js#SCCertification'; +import contactPointMapping from 'elasticsearch:./things/contact-point.js#SCContactPoint'; +import courseOfStudyMapping from 'elasticsearch:./things/course-of-study.js#SCCourseOfStudy'; +import dateSeriesMapping from 'elasticsearch:./things/date-series.js#SCDateSeries'; +import dishMapping from 'elasticsearch:./things/dish.js#SCDish'; +import floorMapping from 'elasticsearch:./things/floor.js#SCFloor'; +import idCardMapping from 'elasticsearch:./things/id-card.js#SCIdCard'; +import jopPostingMapping from 'elasticsearch:./things/job-posting.js#SCJobPosting'; +import messageMapping from 'elasticsearch:./things/message.js#SCMessage'; +import organizationMapping from 'elasticsearch:./things/organization.js#SCOrganization'; +import periodicalMapping from 'elasticsearch:./things/periodical.js#SCPeriodical'; +import personMapping from 'elasticsearch:./things/person.js#SCPerson'; +import pointOfInterestMapping from 'elasticsearch:./things/point-of-interest.js#SCPointOfInterest'; +import publicationEventMapping from 'elasticsearch:./things/publication-event.js#SCPublicationEvent'; +import roomMapping from 'elasticsearch:./things/room.js#SCRoom'; +import semesterMapping from 'elasticsearch:./things/semester.js#SCSemester'; +import sportCourseMapping from 'elasticsearch:./things/sport-course.js#SCSportCourse'; +import studyModuleMapping from 'elasticsearch:./things/study-module.js#SCStudyModule'; +import ticketMapping from 'elasticsearch:./things/ticket.js#SCTicket'; +import todoMapping from 'elasticsearch:./things/todo.js#SCToDo'; +import tourMapping from 'elasticsearch:./things/tour.js#SCTour'; +import videoMapping from 'elasticsearch:./things/video.js#SCVideo'; +import {SCIndexableThings} from './meta.js'; + +export type IndexableThingTypes = SCIndexableThings['type']; +export const elasticsearchMappings: Record = { + 'academic event': academicEventMapping, + 'article': articleMapping, + 'assessment': assessmentMapping, + 'book': bookMapping, + 'building': buildingMapping, + 'catalog': catalogMapping, + 'certification': certificationMapping, + 'contact point': contactPointMapping, + 'course of study': courseOfStudyMapping, + 'date series': dateSeriesMapping, + 'dish': dishMapping, + 'floor': floorMapping, + 'id card': idCardMapping, + 'job posting': jopPostingMapping, + 'message': messageMapping, + 'organization': organizationMapping, + 'periodical': periodicalMapping, + 'person': personMapping, + 'point of interest': pointOfInterestMapping, + 'publication event': publicationEventMapping, + 'room': roomMapping, + 'semester': semesterMapping, + 'sport course': sportCourseMapping, + 'study module': studyModuleMapping, + 'ticket': ticketMapping, + 'todo': todoMapping, + 'tour': tourMapping, + 'video': videoMapping, +}; diff --git a/packages/core/src/generators.d.ts b/packages/core/src/generators.d.ts new file mode 100644 index 00000000..d5df378c --- /dev/null +++ b/packages/core/src/generators.d.ts @@ -0,0 +1,10 @@ +declare module 'schema:*' { + import {JSONSchema7} from 'json-schema'; + const schema: JSONSchema7; + export default schema; +} + +declare module 'elasticsearch:*' { + const indexRequest: unknown; + export default indexRequest; +} diff --git a/packages/core/src/protocol/route.ts b/packages/core/src/protocol/route.ts index 6848c616..9ce42e48 100644 --- a/packages/core/src/protocol/route.ts +++ b/packages/core/src/protocol/route.ts @@ -27,6 +27,7 @@ import {SCSearchRequest, SCSearchResponse, SCSearchRoute} from './routes/search. import {SCMultiSearchRequest, SCMultiSearchResponse, SCMultiSearchRoute} from './routes/search-multi.js'; import {SCThingUpdateRequest, SCThingUpdateResponse, SCThingUpdateRoute} from './routes/thing-update.js'; import {SCRatingRequest, SCRatingResponse, SCRatingRoute} from './routes/rating.js'; +import {JSONSchema7} from 'json-schema'; /** * Possible Verbs for HTTP requests @@ -50,7 +51,7 @@ export interface SCRoute { /** * A map of names of possible errors that can be returned by the route with their appropriate status codes */ - errorNames: SCErrorResponseConstructor[]; + errors: ReadonlyArray; /** * HTTP verb to use to request the route @@ -65,12 +66,12 @@ export interface SCRoute { /** * Name of the type of the request body */ - requestBodyName: string; + requestBodySchema: JSONSchema7; /** * Name of the type of the response body */ - responseBodyName: string; + responseBodySchema: JSONSchema7; /** * Status code for success @@ -90,7 +91,7 @@ export abstract class SCAbstractRoute implements SCRoute { /** * @see SCRoute.errorNames */ - errorNames: SCErrorResponseConstructor[] = []; + errorSchemas: SCErrorResponseConstructor[] = []; /** * @see SCRoute.method @@ -103,14 +104,14 @@ export abstract class SCAbstractRoute implements SCRoute { obligatoryParameters?: Record; /** - * @see SCRoute.requestBodyName + * @see SCRoute.requestBodySchema */ - requestBodyName = 'any'; + abstract requestBodySchema: JSONSchema7; /** - * @see SCRoute.responseBodyName + * @see SCRoute.responseBodySchema */ - responseBodyName = 'any'; + abstract responseBodySchema: JSONSchema7; /** * @see SCRoute.statusCodeSuccess diff --git a/packages/core/src/protocol/routes/bulk-add.ts b/packages/core/src/protocol/routes/bulk-add.ts index 079a75f1..9dfedf8c 100644 --- a/packages/core/src/protocol/routes/bulk-add.ts +++ b/packages/core/src/protocol/routes/bulk-add.ts @@ -28,17 +28,23 @@ import {SCAbstractRoute, SCRouteHttpVerbs} from '../route.js'; * @validatable */ export type SCBulkAddRequest = SCThings; +import {default as bulkAddRequestSchema} from 'schema:#SCBulkAddRequest'; /** * Response to a request to add a thing to a bulk * @validatable */ export interface SCBulkAddResponse {} +import {default as bulkAddResponseSchema} from 'schema:#SCBulkAddResponse'; /** * Route for indexing SC things in a bulk */ export class SCBulkAddRoute extends SCAbstractRoute { + responseBodySchema = bulkAddRequestSchema; + + requestBodySchema = bulkAddResponseSchema; + constructor() { super(); this.errorNames = [ @@ -54,8 +60,6 @@ export class SCBulkAddRoute extends SCAbstractRoute { this.obligatoryParameters = { UID: 'SCUuid', }; - this.requestBodyName = 'SCBulkAddRequest'; - this.responseBodyName = 'SCBulkAddResponse'; this.statusCodeSuccess = StatusCodes.CREATED; this.urlPath = '/bulk/:UID'; } diff --git a/packages/core/src/protocol/routes/rating.ts b/packages/core/src/protocol/routes/rating.ts index ce399089..2d8b1752 100644 --- a/packages/core/src/protocol/routes/rating.ts +++ b/packages/core/src/protocol/routes/rating.ts @@ -18,16 +18,17 @@ import {SCMethodNotAllowedErrorResponse} from '../errors/method-not-allowed.js'; import {SCRequestBodyTooLargeErrorResponse} from '../errors/request-body-too-large.js'; import {SCSyntaxErrorResponse} from '../errors/syntax-error.js'; import {SCUnsupportedMediaTypeErrorResponse} from '../errors/unsupported-media-type.js'; -import {SCAbstractRoute, SCRouteHttpVerbs} from '../route.js'; +import {SCRoute, SCRouteHttpVerbs} from '../route.js'; import {SCThing} from '../../things/abstract/thing.js'; import {SCUserGroupSetting} from '../../things/setting.js'; import {SCValidationErrorResponse} from '../errors/validation.js'; +import {default as ratingRequestSchema} from 'schema:#SCRatingRequest'; +import {default as ratingResponseSchema} from 'schema:#SCRatingResponse'; /** * User rating from the app * Plugin needs to define its own rating request to hit the target rating system. * That request should extend this one and contain timestamp and other needed data. - * @validatable */ export interface SCRatingRequest { /** @@ -48,28 +49,26 @@ export interface SCRatingRequest { /** * A response to a rating request - * @validatable */ export interface SCRatingResponse {} /** * Route for rating submission */ -export class SCRatingRoute extends SCAbstractRoute { - constructor() { - super(); - this.errorNames = [ - SCInternalServerErrorResponse, - SCMethodNotAllowedErrorResponse, - SCRequestBodyTooLargeErrorResponse, - SCSyntaxErrorResponse, - SCUnsupportedMediaTypeErrorResponse, - SCValidationErrorResponse, - ]; - this.method = SCRouteHttpVerbs.POST; - this.requestBodyName = 'SCRatingRequest'; - this.responseBodyName = 'SCRatingResponse'; - this.statusCodeSuccess = StatusCodes.OK; - this.urlPath = '/rating'; - } -} +export type SCRatingRoute = typeof ratingRoute; + +export const ratingRoute = Object.freeze({ + errors: [ + SCInternalServerErrorResponse, + SCMethodNotAllowedErrorResponse, + SCRequestBodyTooLargeErrorResponse, + SCSyntaxErrorResponse, + SCUnsupportedMediaTypeErrorResponse, + SCValidationErrorResponse, + ] as const, + method: SCRouteHttpVerbs.POST, + requestBodySchema: ratingRequestSchema, + responseBodySchema: ratingResponseSchema, + statusCodeSuccess: StatusCodes.OK, + urlPath: '/rating', +}) satisfies Readonly; diff --git a/packages/core/src/protocol/routes/search-multi.ts b/packages/core/src/protocol/routes/search-multi.ts index 4a7e7906..b4c4005a 100644 --- a/packages/core/src/protocol/routes/search-multi.ts +++ b/packages/core/src/protocol/routes/search-multi.ts @@ -20,9 +20,11 @@ import {SCSyntaxErrorResponse} from '../errors/syntax-error.js'; import {SCTooManyRequestsErrorResponse} from '../errors/too-many-requests.js'; import {SCUnsupportedMediaTypeErrorResponse} from '../errors/unsupported-media-type.js'; import {SCValidationErrorResponse} from '../errors/validation.js'; -import {SCAbstractRoute, SCRouteHttpVerbs} from '../route.js'; +import {SCRoute, SCRouteHttpVerbs} from '../route.js'; import {SCSearchQuery} from '../search/query.js'; import {SCSearchResult} from '../search/result.js'; +import {default as multiSearchRequestSchema} from 'schema:#SCMultiSearchRequest'; +import {default as multiSearchResponseSchema} from 'schema:#SCMultiSearchResponse'; /** * A multi search request @@ -30,7 +32,6 @@ import {SCSearchResult} from '../search/result.js'; * This is a map of [[SCSearchRequest]]s indexed by name. * * **CAUTION: This is limited to an amount of queries. Currently this limit is 5.** - * @validatable */ export type SCMultiSearchRequest = Record; @@ -38,29 +39,27 @@ export type SCMultiSearchRequest = Record; * A multi search response * * This is a map of [[SCSearchResponse]]s indexed by name - * @validatable */ export type SCMultiSearchResponse = Record; /** * Route for submission of multiple search requests at once */ -export class SCMultiSearchRoute extends SCAbstractRoute { - constructor() { - super(); - this.errorNames = [ - SCInternalServerErrorResponse, - SCMethodNotAllowedErrorResponse, - SCRequestBodyTooLargeErrorResponse, - SCSyntaxErrorResponse, - SCTooManyRequestsErrorResponse, - SCUnsupportedMediaTypeErrorResponse, - SCValidationErrorResponse, - ]; - this.method = SCRouteHttpVerbs.POST; - this.requestBodyName = 'SCMultiSearchRequest'; - this.responseBodyName = 'SCMultiSearchResponse'; - this.statusCodeSuccess = StatusCodes.OK; - this.urlPath = '/search/multi'; - } -} +export type SCMultiSearchRoute = typeof multiSearchRoute; + +export const multiSearchRoute = Object.freeze({ + errors: [ + SCInternalServerErrorResponse, + SCMethodNotAllowedErrorResponse, + SCRequestBodyTooLargeErrorResponse, + SCSyntaxErrorResponse, + SCTooManyRequestsErrorResponse, + SCUnsupportedMediaTypeErrorResponse, + SCValidationErrorResponse, + ] as const, + method: SCRouteHttpVerbs.POST, + requestBodySchema: multiSearchRequestSchema, + responseBodySchema: multiSearchResponseSchema, + statusCodeSuccess: StatusCodes.OK, + urlPath: '/search/multi', +}) satisfies Readonly; diff --git a/packages/core/src/protocol/routes/search.ts b/packages/core/src/protocol/routes/search.ts index 2c19db23..661724d8 100644 --- a/packages/core/src/protocol/routes/search.ts +++ b/packages/core/src/protocol/routes/search.ts @@ -19,40 +19,39 @@ import {SCRequestBodyTooLargeErrorResponse} from '../errors/request-body-too-lar import {SCSyntaxErrorResponse} from '../errors/syntax-error.js'; import {SCUnsupportedMediaTypeErrorResponse} from '../errors/unsupported-media-type.js'; import {SCValidationErrorResponse} from '../errors/validation.js'; -import {SCAbstractRoute, SCRouteHttpVerbs} from '../route.js'; +import {SCRoute, SCRouteHttpVerbs} from '../route.js'; import {SCSearchQuery} from '../search/query.js'; import {SCSearchResult} from '../search/result.js'; +import {default as searchRequestSchema} from 'schema:#SCSearchRequest'; +import {default as searchResponseSchema} from 'schema:#SCSearchResponse'; /** * A search request - * @validatable */ export type SCSearchRequest = SCSearchQuery; /** * A search response - * @validatable */ export type SCSearchResponse = SCSearchResult; /** * Route for searching things */ -export class SCSearchRoute extends SCAbstractRoute { - constructor() { - super(); - this.errorNames = [ - SCInternalServerErrorResponse, - SCMethodNotAllowedErrorResponse, - SCRequestBodyTooLargeErrorResponse, - SCSyntaxErrorResponse, - SCUnsupportedMediaTypeErrorResponse, - SCValidationErrorResponse, - ]; - this.method = SCRouteHttpVerbs.POST; - this.requestBodyName = 'SCSearchRequest'; - this.responseBodyName = 'SCSearchResponse'; - this.statusCodeSuccess = StatusCodes.OK; - this.urlPath = '/search'; - } -} +export type SCSearchRoute = typeof searchRoute; + +export const searchRoute = Object.freeze({ + errors: [ + SCInternalServerErrorResponse, + SCMethodNotAllowedErrorResponse, + SCRequestBodyTooLargeErrorResponse, + SCSyntaxErrorResponse, + SCUnsupportedMediaTypeErrorResponse, + SCValidationErrorResponse, + ] as const, + method: SCRouteHttpVerbs.POST, + requestBodySchema: searchRequestSchema, + responseBodySchema: searchResponseSchema, + statusCodeSuccess: StatusCodes.OK, + urlPath: '/search', +}) satisfies Readonly; diff --git a/packages/core/src/protocol/routes/thing-update.ts b/packages/core/src/protocol/routes/thing-update.ts index aca5577a..fac9d378 100644 --- a/packages/core/src/protocol/routes/thing-update.ts +++ b/packages/core/src/protocol/routes/thing-update.ts @@ -21,43 +21,42 @@ import {SCRequestBodyTooLargeErrorResponse} from '../errors/request-body-too-lar import {SCSyntaxErrorResponse} from '../errors/syntax-error.js'; import {SCUnsupportedMediaTypeErrorResponse} from '../errors/unsupported-media-type.js'; import {SCValidationErrorResponse} from '../errors/validation.js'; -import {SCAbstractRoute, SCRouteHttpVerbs} from '../route.js'; +import {SCRoute, SCRouteHttpVerbs} from '../route.js'; +import {default as thingUpdateRequestSchema} from 'schema:#SCThingUpdateRequest'; +import {default as thingUpdateResponseSchema} from 'schema:#SCThingUpdateResponse'; /** * Request to update an existing thing - * @validatable */ export type SCThingUpdateRequest = SCThings; /** * Response for an entity update request - * @validatable */ export interface SCThingUpdateResponse {} /** * Route for updating existing things */ -export class SCThingUpdateRoute extends SCAbstractRoute { - constructor() { - super(); - this.errorNames = [ - SCInternalServerErrorResponse, - SCMethodNotAllowedErrorResponse, - SCNotFoundErrorResponse, - SCRequestBodyTooLargeErrorResponse, - SCSyntaxErrorResponse, - SCUnsupportedMediaTypeErrorResponse, - SCValidationErrorResponse, - ]; - this.method = SCRouteHttpVerbs.PUT; - this.obligatoryParameters = { - TYPE: 'SCThingType', - UID: 'SCUuid', - }; - this.requestBodyName = 'SCThingUpdateRequest'; - this.responseBodyName = 'SCThingUpdateResponse'; - this.statusCodeSuccess = StatusCodes.OK; - this.urlPath = '/:TYPE/:UID'; - } -} +export type SCThingUpdateRoute = typeof thingUpdateRoute; + +export const thingUpdateRoute = Object.freeze({ + errors: [ + SCInternalServerErrorResponse, + SCMethodNotAllowedErrorResponse, + SCNotFoundErrorResponse, + SCRequestBodyTooLargeErrorResponse, + SCSyntaxErrorResponse, + SCUnsupportedMediaTypeErrorResponse, + SCValidationErrorResponse, + ] as const, + method: SCRouteHttpVerbs.PUT, + obligatoryParameters: { + TYPE: 'SCThingType', + UID: 'SCUuid', + }, + requestBodySchema: thingUpdateRequestSchema, + responseBodySchema: thingUpdateResponseSchema, + statusCodeSuccess: StatusCodes.OK, + urlPath: '/:TYPE/:UID', +}) satisfies Readonly; diff --git a/packages/core/src/schemas.ts b/packages/core/src/schemas.ts new file mode 100644 index 00000000..81a9b631 --- /dev/null +++ b/packages/core/src/schemas.ts @@ -0,0 +1,67 @@ +import academicEventSchema from 'schema:./things/academic-event.js#SCAcademicEvent'; +import articleSchema from 'schema:./things/article.js#SCArticle'; +import assessmentSchema from 'schema:./things/assessment.js#SCAssessment'; +import bookSchema from 'schema:./things/book.js#SCBook'; +import buildingSchema from 'schema:./things/building.js#SCBuilding'; +import catalogSchema from 'schema:./things/catalog.js#SCCatalog'; +import certificationSchema from 'schema:./things/certification.js#SCCertification'; +import contactPointSchema from 'schema:./things/contact-point.js#SCContactPoint'; +import courseOfStudySchema from 'schema:./things/course-of-study.js#SCCourseOfStudy'; +import dateSeriesSchema from 'schema:./things/date-series.js#SCDateSeries'; +import diffSchema from 'schema:./things/diff.js#SCDiff'; +import dishSchema from 'schema:./things/dish.js#SCDish'; +import favoriteSchema from 'schema:./things/favorite.js#SCFavorite'; +import floorSchema from 'schema:./things/floor.js#SCFloor'; +import idCardSchema from 'schema:./things/id-card.js#SCIdCard'; +import jopPostingSchema from 'schema:./things/job-posting.js#SCJobPosting'; +import messageSchema from 'schema:./things/message.js#SCMessage'; +import organizationSchema from 'schema:./things/organization.js#SCOrganization'; +import periodicalSchema from 'schema:./things/periodical.js#SCPeriodical'; +import personSchema from 'schema:./things/person.js#SCPerson'; +import pointOfInterestSchema from 'schema:./things/point-of-interest.js#SCPointOfInterest'; +import publicationEventSchema from 'schema:./things/publication-event.js#SCPublicationEvent'; +import roomSchema from 'schema:./things/room.js#SCRoom'; +import semesterSchema from 'schema:./things/semester.js#SCSemester'; +import settingSchema from 'schema:./things/setting.js#SCSetting'; +import sportCourseSchema from 'schema:./things/sport-course.js#SCSportCourse'; +import studyModuleSchema from 'schema:./things/study-module.js#SCStudyModule'; +import ticketSchema from 'schema:./things/ticket.js#SCTicket'; +import todoSchema from 'schema:./things/todo.js#SCToDo'; +import tourSchema from 'schema:./things/tour.js#SCTour'; +import videoSchema from 'schema:./things/video.js#SCVideo'; +import {SCThingType} from './things/abstract/thing.js'; +import {JSONSchema7} from 'json-schema'; + +export const thingSchemas: Record = { + 'academic event': academicEventSchema, + 'article': articleSchema, + 'assessment': assessmentSchema, + 'book': bookSchema, + 'building': buildingSchema, + 'catalog': catalogSchema, + 'certification': certificationSchema, + 'contact point': contactPointSchema, + 'course of study': courseOfStudySchema, + 'date series': dateSeriesSchema, + 'diff': diffSchema, + 'dish': dishSchema, + 'favorite': favoriteSchema, + 'floor': floorSchema, + 'id card': idCardSchema, + 'job posting': jopPostingSchema, + 'message': messageSchema, + 'organization': organizationSchema, + 'periodical': periodicalSchema, + 'person': personSchema, + 'point of interest': pointOfInterestSchema, + 'publication event': publicationEventSchema, + 'room': roomSchema, + 'semester': semesterSchema, + 'setting': settingSchema, + 'sport course': sportCourseSchema, + 'study module': studyModuleSchema, + 'ticket': ticketSchema, + 'todo': todoSchema, + 'tour': tourSchema, + 'video': videoSchema, +}; diff --git a/packages/core/src/things/academic-event.ts b/packages/core/src/things/academic-event.ts index c9a806b0..5bc09496 100644 --- a/packages/core/src/things/academic-event.ts +++ b/packages/core/src/things/academic-event.ts @@ -55,7 +55,6 @@ export interface SCAcademicEventWithoutReferences /** * An academic event - * @validatable * @elasticsearch indexable */ export interface SCAcademicEvent @@ -74,6 +73,9 @@ export interface SCAcademicEvent type: SCThingType.AcademicEvent; } +// export {default as academicEventSchema} from 'schema:#SCAcademicEvent'; +// export {default as academicEventElasticsearchMapping} from 'elasticsearch:#SCAcademicEvent'; + /** * Categories of academic events */ diff --git a/packages/core/src/things/ticket.ts b/packages/core/src/things/ticket.ts index abe8969b..400c9e4a 100644 --- a/packages/core/src/things/ticket.ts +++ b/packages/core/src/things/ticket.ts @@ -45,7 +45,6 @@ export interface SCTicketWithoutReferences extends SCThingWithoutReferences { /** * A ticket - * @validatable * @elasticsearch indexable */ export interface SCTicket extends SCTicketWithoutReferences, SCThingInPlace { diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts index 299c4daf..b9aad40b 100644 --- a/packages/core/tsup.config.ts +++ b/packages/core/tsup.config.ts @@ -1,14 +1,19 @@ import {defineConfig} from 'tsup'; -import {jsonSchemaPlugin} from '@openstapps/json-schema-generator'; +import {esbuildJsonSchemaPlugin, jsonSchemaPlugin} from '@openstapps/json-schema-generator'; import {openapiPlugin} from '@openstapps/openapi-generator'; -import {elasticsearchMappingGenerator} from '@openstapps/es-mapping-generator'; +import { + elasticsearchMappingGenerator, + esbuildElasticsearchMappingPlugin, +} from '@openstapps/es-mapping-generator'; export default defineConfig({ - entry: ['src/index.ts'], + entry: ['src/index.ts', 'src/schemas.ts', 'src/elasticsearch.ts'], sourcemap: true, clean: true, format: 'esm', outDir: 'lib', + esbuildPlugins: [esbuildJsonSchemaPlugin, esbuildElasticsearchMappingPlugin], + noExternal: [/schema:*/, /elasticsearch:*/], plugins: [ jsonSchemaPlugin('schema.json', elasticsearchMappingGenerator('elasticsearch.json')), openapiPlugin('openapi.json', 'schema.json'), diff --git a/packages/es-mapping-generator/src/generator/index.ts b/packages/es-mapping-generator/src/generator/index.ts index ecfd5cb6..c84e8028 100644 --- a/packages/es-mapping-generator/src/generator/index.ts +++ b/packages/es-mapping-generator/src/generator/index.ts @@ -40,7 +40,7 @@ export function transformProject(project: JSONSchema7) { }; } -const OPTIONS: GeneratorOptions = { +export const OPTIONS: GeneratorOptions = { template: { name: 'template_{_type}', index_patterns: 'stapps_{_type}*', diff --git a/packages/es-mapping-generator/src/index.ts b/packages/es-mapping-generator/src/index.ts index 337238fa..2dac6138 100644 --- a/packages/es-mapping-generator/src/index.ts +++ b/packages/es-mapping-generator/src/index.ts @@ -1,5 +1,48 @@ -import {transformProject} from './generator/index.js'; +import {OPTIONS, transformProject} from './generator/index.js'; import {SchemaConsumer} from '@openstapps/json-schema-generator'; +import {Plugin} from 'esbuild'; +// eslint-disable-next-line unicorn/import-style +import {dirname} from 'path'; +import {MappingGenerator} from './generator/mapping-generator.js'; + +export const esbuildElasticsearchMappingPlugin: Plugin = { + name: 'elasticsearch-mappings', + setup(build) { + const fileRegex = /^elasticsearch:/; + const namespace = 'elasticsearch-mappings-ns'; + const mappings = new Map(); + + build.onResolve({filter: fileRegex}, async ({path, importer}) => { + const [from, name] = path.replace(fileRegex, '').split('#', 2); + + return { + path: `${ + from + ? await build + .resolve(from, {resolveDir: dirname(importer), kind: 'import-statement'}) + .then(it => it.path) + : importer + }#${name}`, + namespace, + }; + }); + + build.onLoad({filter: /.*/, namespace}, async ({path}) => { + if (!mappings.has(path)) { + const result = await build.resolve(`schema:${path}`, {kind: 'import-statement'}); + console.log(result); + const context = new MappingGenerator(result.pluginData.schema, OPTIONS); + const name = path.split('#', 2)[1]; + + mappings.set(path, JSON.stringify(context.buildTemplate(name))); + } + return { + contents: mappings.get(path), + loader: 'json', + }; + }); + }, +}; /** * JSON Schema Generator plugin for Elasticsearch Mappings diff --git a/packages/json-schema-generator/src/index.ts b/packages/json-schema-generator/src/index.ts index 22711aa0..6a4fe488 100644 --- a/packages/json-schema-generator/src/index.ts +++ b/packages/json-schema-generator/src/index.ts @@ -3,35 +3,41 @@ import {generateFiles, Plugin, PluginContext} from '@openstapps/tsup-plugin'; import {JSONSchema7} from 'json-schema'; import {Plugin as EsbuildPlugin} from 'esbuild'; import {createGenerator} from 'ts-json-schema-generator'; +import {dirname} from 'path'; export type SchemaConsumer = (this: PluginContext, schema: JSONSchema7) => Record; -export const jsonSchema: EsbuildPlugin = { +export const esbuildJsonSchemaPlugin: EsbuildPlugin = { name: 'json-schema', setup(build) { const fileRegex = /^schema:/; const namespace = 'json-schema-ns'; const schemas = new Map(); - build.onResolve({filter: fileRegex}, ({path, importer}) => { - const [from, name] = path.replace(fileRegex, '').split('#', 1); + build.onResolve({filter: fileRegex}, async ({path, importer}) => { + const [from, name] = path.replace(fileRegex, '').split('#', 2); + const outputName = `${name}.schema.json`; + if (!schemas.has(outputName)) { + const generator = createGenerator({ + path: from + ? await build + .resolve(from, {resolveDir: dirname(importer), kind: 'import-statement'}) + .then(it => it.path) + : importer, + extraTags: ['elasticsearch'], + skipTypeCheck: true, + }); + schemas.set(outputName, JSON.stringify(generator.createSchema(name))); + } return { - path: `${from === 'file' ? importer : from}#${name}`, + path: outputName, + pluginData: {schema: JSON.parse(schemas.get(outputName)!)}, namespace, }; }); build.onLoad({filter: /.*/, namespace}, ({path}) => { - if (!schemas.has(path)) { - const [sourcePath, schemaName] = path.split('#', 1); - const generator = createGenerator({ - path: sourcePath, - extraTags: ['elasticsearch'], - skipTypeCheck: true, - }); - schemas.set(path, JSON.stringify(generator.createSchema(schemaName))); - } return { contents: schemas.get(path), loader: 'json',