UNPKG

api-core

Version:

Model-based dynamic multi-level APIs for any provider, plus multiple consumption channels

951 lines (808 loc) 38.8 kB
import {ApiQuery, ApiQueryScope, QueryStep} from "./ApiQuery"; import {ApiEdgeQuery} from "../edge/ApiEdgeQuery"; import {ApiEdgeQueryContext} from "../edge/ApiEdgeQueryContext"; import {ApiEdgeRelation} from "../relations/ApiEdgeRelation"; import {ApiEdgeError} from "./ApiEdgeError"; import {ApiEdgeQueryFilter, ApiEdgeQueryFilterType} from "../edge/ApiEdgeQueryFilter"; import { ApiRequest, ApiRequestType, EdgePathSegment, EntryPathSegment, MethodPathSegment, PathSegment, RelatedFieldPathSegment } from "../request/ApiRequest"; import {ApiEdgeQueryResponse} from "../edge/ApiEdgeQueryResponse"; import {ApiEdgeQueryType} from "../edge/ApiEdgeQueryType"; import {OneToOneRelation} from "../relations/OneToOneRelation"; import {Api} from "../Api"; import {ApiEdgeMethod, ApiEdgeMethodOutput, ApiEdgeMethodScope} from "../edge/ApiEdgeMethod"; import {ApiEdgeAction, ApiEdgeActionTrigger, ApiEdgeActionTriggerKind} from "../edge/ApiEdgeAction"; import {ApiAction, ApiActionTriggerKind} from "./ApiAction"; import {ApiEdgeDefinition} from "../edge/ApiEdgeDefinition"; import {OneToManyRelation} from "../relations/OneToManyRelation"; const parse = require('obj-parse'); const debug = require('debug')('api-core'); export class EmbedQueryQueryStep implements QueryStep { query: ApiQuery; request: ApiRequest; segment: PathSegment; sourceField: string; targetField: string; idField: string; forceArray: boolean; isMultiMulti: boolean; constructor(query: ApiQuery, segment: PathSegment, request: ApiRequest) { this.query = query; this.query.request = this.request = request; this.segment = segment; if(!this.segment.relation) throw new Error('Invalid relation provided.'); this.sourceField = this.segment.relation.relationId; this.targetField = this.segment.relation.name; this.idField = this.segment.relation.relatedId; this.forceArray = this.segment.relation instanceof OneToManyRelation; this.isMultiMulti = this.segment.relation.hasPair; } execute = (scope: ApiQueryScope): Promise<ApiQueryScope> => { return new Promise((resolve, reject) => { debug(`[${scope.query.id}]`, this.inspect()); if(scope.response) { const target = scope.response.data; if(Array.isArray(target)) { const targetIndex: { [key: string]: any[] } = {}, targetArrayIndex: { [key: string]: { entry: any, index: number }[] } = {}, ids: string[] = []; for(let entry of target) { const id = entry[this.sourceField]; if(id) { if(Array.isArray(id)) { let index = 0; for(let _id of id) { if (targetArrayIndex[_id]) targetArrayIndex[_id].push({ entry, index }); else targetArrayIndex[_id] = [{entry, index}]; ids.push(_id); index++ } entry[this.sourceField] = []; } else { if (targetIndex[id]) targetIndex[id].push(entry); else targetIndex[id] = [entry]; ids.push(id); if (this.forceArray) entry[this.targetField] = []; } } } this.request.context.filters = [ new ApiEdgeQueryFilter(this.idField, ApiEdgeQueryFilterType.In, ids) ]; this.query.execute(scope.identity).then((response) => { if(response.data && response.data.length) { for (let entry of response.data) { let ids = entry[this.idField]; if(!Array.isArray(ids)) { ids = [ids]; } for(let id of ids) { if (targetIndex[id]) { for (let subEntry of targetIndex[id]) { if (this.forceArray) subEntry[this.targetField].push(entry); else subEntry[this.targetField] = entry; } } if(targetArrayIndex[id]) { for (let { entry: subEntry, index } of targetArrayIndex[id]) { subEntry[this.targetField][index] = entry; } } } } } resolve(scope) }).catch(reject); } else { const sourceId = target[this.sourceField]; if(!sourceId) { resolve(scope); return } if(Array.isArray(sourceId)) { this.request.context.filters = [ new ApiEdgeQueryFilter(this.idField, ApiEdgeQueryFilterType.In, sourceId) ]; } else if(this.forceArray) { this.request.context.filters = [ new ApiEdgeQueryFilter(this.idField, ApiEdgeQueryFilterType.Equals, sourceId) ]; } else { //Now we can replace TBD and provide a real id for the query. (this.segment as EntryPathSegment).id = sourceId; } this.query.execute(scope.identity).then((response) => { if (Array.isArray(sourceId)) { // restore original order of array items const unordered_data = response.data; response.data = []; for (const id of sourceId) { const item = unordered_data.find((item:any) => { return item.id.toString() == id.toString(); }); if (item) response.data.push(item); else console.warn("WARNING: can\'t find in embed results this id: " + id); } } target[this.targetField] = response.data; resolve(scope) }).catch(e => { console.warn(e); resolve(scope) }) } } else resolve(scope) }) }; inspect = () => `EMBED QUERY /${this.sourceField} -> ${this.targetField}`; } export class QueryEdgeQueryStep implements QueryStep { query: ApiEdgeQuery; constructor(query: ApiEdgeQuery) { this.query = query; } execute = (scope: ApiQueryScope): Promise<ApiQueryScope> => { return new Promise((resolve, reject) => { debug(`[${scope.query.id}]`, this.inspect()); if(this.query.type !== ApiEdgeQueryType.Get && this.query.type !== ApiEdgeQueryType.List) { this.query.body = scope.body; } this.query.context = scope.context; this.query.context.populatedRelations = []; // prevent embed step to run on external query --- it shall only run at the original executor this.query.context.identity = scope.identity; this.query.execute().then((response) => { scope.context = new ApiEdgeQueryContext(); scope.response = response; resolve(scope) }).catch(reject); }) }; inspect = () => `QUERY /${this.query.edge.pluralName}`; } export class CallMethodQueryStep implements QueryStep { method: ApiEdgeMethod; edge: ApiEdgeDefinition; constructor(method: ApiEdgeMethod, edge: ApiEdgeDefinition) { this.method = method; this.edge = edge; } execute = (scope: ApiQueryScope): Promise<ApiQueryScope> => { return new Promise((resolve, reject) => { debug(`[${scope.query.id}]`, this.inspect()); scope.context.method = this.method.name; this.method.execute(scope) .then((response) => { scope.response = response; resolve(scope) }).catch((e) => { debug(`failed to execute ${this.method.name} method`, e); reject(e) }); }) }; inspect = () => `call{${this.method.name}}`; } export class RelateQueryStep implements QueryStep { relation: ApiEdgeRelation; constructor(relation: ApiEdgeRelation) { this.relation = relation; } execute = (scope: ApiQueryScope): Promise<ApiQueryScope> => { return new Promise((resolve, reject) => { debug(`[${scope.query.id}]`, this.inspect()); if(!scope.response) return reject(new ApiEdgeError(404, "Missing Related Entry")); scope.context.filter(this.relation.relationId, ApiEdgeQueryFilterType.Equals, scope.response.data[this.relation.relatedId]); resolve(scope); }) }; inspect = () => `RELATE ${this.relation.relationId} = ${this.relation.relatedId}`; } export class RelateBackwardsQueryStep implements QueryStep { relation: ApiEdgeRelation; constructor(relation: ApiEdgeRelation) { this.relation = relation; } execute = (scope: ApiQueryScope): Promise<ApiQueryScope> => { return new Promise((resolve, reject) => { debug(`[${scope.query.id}]`, this.inspect()); if(!scope.response) return reject(new ApiEdgeError(404, "Missing Related Entry")); scope.context.filter(this.relation.relatedId, ApiEdgeQueryFilterType.Equals, scope.response.data[this.relation.relationId]); resolve(scope); }) }; inspect = () => `RELATE ${this.relation.relatedId} = ${this.relation.relationId}`; } export class RelateChangeQueryStep implements QueryStep { relation: ApiEdgeRelation; constructor(relation: ApiEdgeRelation) { this.relation = relation; } execute = (scope: ApiQueryScope): Promise<ApiQueryScope> => { return new Promise((resolve, reject) => { debug(`[${scope.query.id}]`, this.inspect()); if(!scope.body) return reject(new ApiEdgeError(404, "Missing Body")); if(!scope.response) return reject(new ApiEdgeError(404, "Missing Related Entry")); parse(this.relation.relationId).assign( scope.body, scope.response.data[this.relation.relatedId].toString()); resolve(scope); }) }; inspect = () => `RELATE CHANGE ${this.relation.relationId}`; } export class RelateBackwardsChangeQueryStep implements QueryStep { relation: ApiEdgeRelation; constructor(relation: ApiEdgeRelation) { this.relation = relation; } execute = (scope: ApiQueryScope): Promise<ApiQueryScope> => { return new Promise((resolve, reject) => { debug(`[${scope.query.id}]`, this.inspect()); if(!scope.body) return reject(new ApiEdgeError(404, "Missing Body")); if(!scope.response) return reject(new ApiEdgeError(404, "Missing Related Entry")); parse(this.relation.relatedId).assign( scope.body, scope.response.data[this.relation.relationId].toString()); resolve(scope); }) }; inspect = () => `RELATE CHANGE BACKWARD ${this.relation.relatedId}`; } /*export class CheckResponseQueryStep implements QueryStep { execute = (scope: QueryScope) => { return new Promise((resolve, reject) => { if(!scope.response) return reject(new ApiEdgeError(404, "Missing Related Entry")); resolve(scope); }) }; inspect = () => `CHECK`; } export class NotImplementedQueryStep implements QueryStep { description: string; constructor(description: string) { this.description = description; } execute = (scope: QueryScope) => { return new Promise(resolve => { resolve(scope); }) }; inspect = () => `NOT IMPLEMENTED: ${this.description}`; }*/ export class SetResponseQueryStep implements QueryStep { response: ApiEdgeQueryResponse; constructor(response: ApiEdgeQueryResponse) { this.response = response; } execute = (scope: ApiQueryScope): Promise<ApiQueryScope> => { return new Promise(resolve => { debug(`[${scope.query.id}]`, this.inspect()); scope.response = this.response; scope.context = new ApiEdgeQueryContext(); resolve(scope); }) }; inspect = () => `SET RESPONSE`; } export class SetBodyQueryStep implements QueryStep { body: any; stream: NodeJS.ReadableStream|null; constructor(body: any, stream: NodeJS.ReadableStream|null = null) { this.body = body; this.stream = stream } execute = (scope: ApiQueryScope): Promise<ApiQueryScope> => { return new Promise(resolve => { debug(`[${scope.query.id}]`, this.inspect()); scope.body = this.body; scope.stream = this.stream; resolve(scope); }) }; inspect = () => `SET BODY`; } export class ProvideIdQueryStep implements QueryStep { fieldName: string; constructor(fieldName: string = Api.defaultIdField) { this.fieldName = fieldName; } execute = (scope: ApiQueryScope): Promise<ApiQueryScope> => { return new Promise((resolve, reject) => { debug(`[${scope.query.id}]`, this.inspect()); if(!scope.response) return reject(new ApiEdgeError(404, "Missing Entry")); scope.context.id = scope.response.data[this.fieldName]; resolve(scope); }) }; inspect = () => `PROVIDE ID: ${this.fieldName}`; } export class ExtendContextQueryStep implements QueryStep { context: ApiEdgeQueryContext; constructor(context: ApiEdgeQueryContext) { this.context = context } execute = (scope: ApiQueryScope): Promise<ApiQueryScope> => { return new Promise(resolve => { debug(`[${scope.query.id}]`, this.inspect()); scope.context.id = this.context.id || scope.context.id; if(this.context.pagination) { scope.context.pagination = this.context.pagination; } this.context.fields.forEach(f => scope.context.fields.push(f)); this.context.populatedRelations.forEach(f => scope.context.populatedRelations.push(f)); this.context.filters.forEach(f => scope.context.filters.push(f)); this.context.sortBy.forEach(f => scope.context.sortBy.push(f)); resolve(scope) }) }; inspect = () => { if(this.context.id) { return `EXTEND CONTEXT (id=${this.context.id})` } else { return `APPLY PARAMETERS` } }; } export class ExtendContextLiveQueryStep implements QueryStep { apply: (context: ApiEdgeQueryContext) => void|any; constructor(func: (context: ApiEdgeQueryContext) => void|any) { this.apply = func } execute = (scope: ApiQueryScope): Promise<ApiQueryScope> => { return new Promise(resolve => { debug(`[${scope.query.id}]`, this.inspect()); this.apply(scope.context); resolve(scope) }) }; inspect = () => { return `EXTEND CONTEXT LIVE` }; } /*export class GenericQueryStep implements QueryStep { description: string; step: () => Promise<QueryScope>; context: any; constructor(description: string, step: () => Promise<QueryScope>, context: any) { this.description = description; this.step = step; this.context = context; } execute = (scope: QueryScope) => { return this.step.apply(this.context, [ scope ]); }; inspect = () => this.description }*/ export class ApiQueryBuilder { api: Api; constructor(api: Api) { this.api = api; } private addQueryActions(triggerKind: ApiEdgeActionTriggerKind, query: ApiQuery, edgeQuery: ApiEdgeQuery, relation: ApiEdgeRelation|null, output: boolean = false) { const edge = edgeQuery.edge, queryType = edgeQuery.type, trigger = relation ? ApiEdgeActionTrigger.Relation : (output ? ApiEdgeActionTrigger.OutputQuery : ApiEdgeActionTrigger.SubQuery); let actions: ApiEdgeAction[]; if(relation) { actions = edge.actions.filter((action: ApiEdgeAction) => action.triggerKind == triggerKind && (action.targetTypes & queryType) && (action.triggers & trigger) && (!action.triggerNames.length || action.triggerNames.indexOf(relation.name) == -1)) } else { actions = edge.actions.filter((action: ApiEdgeAction) => action.triggerKind == triggerKind && (action.targetTypes & queryType) && (action.triggers & trigger)) } actions.forEach((action: ApiEdgeAction) => query.unshift(action)); if(output) { const apiTrigger = triggerKind == ApiEdgeActionTriggerKind.BeforeEvent ? ApiActionTriggerKind.BeforeOutput : ApiActionTriggerKind.AfterOutput; this.api.actions .filter((action: ApiAction) => action.triggerKind == apiTrigger) .forEach((action: ApiAction) => query.unshift(action)) } } private addMethodActions(triggerKind: ApiEdgeActionTriggerKind, query: ApiQuery, method: ApiEdgeMethod, queryType: ApiEdgeQueryType, edge: ApiEdgeDefinition, output: boolean = false) { const trigger = ApiEdgeActionTrigger.Method; let actions = edge.actions.filter((action: ApiEdgeAction) => action.triggerKind == triggerKind && (action.targetTypes & queryType) && (action.triggers & trigger) && (!action.triggerNames.length || action.triggerNames.indexOf(method.name) == -1)); actions.forEach((action: ApiEdgeAction) => query.unshift(action)); if(output) { const apiTrigger = triggerKind == ApiEdgeActionTriggerKind.BeforeEvent ? ApiActionTriggerKind.BeforeOutput : ApiActionTriggerKind.AfterOutput; this.api.actions .filter((action: ApiAction) => action.triggerKind == apiTrigger) .forEach((action: ApiAction) => query.unshift(action)) } } private addMethodCallStep(request: ApiRequest, query: ApiQuery, method: ApiEdgeMethod, edge: ApiEdgeDefinition, output: boolean) { if(method.acceptedTypes & request.type) { let queryType = ApiEdgeQueryType.Any; if (request.type === ApiRequestType.Create) { queryType = ApiEdgeQueryType.Create; } else if (request.type === ApiRequestType.Read) { queryType = ApiEdgeQueryType.Read; } else if (request.type === ApiRequestType.Update) { queryType = ApiEdgeQueryType.Update; } else if (request.type === ApiRequestType.Patch) { queryType = ApiEdgeQueryType.Patch; } else if (request.type === ApiRequestType.Delete) { queryType = ApiEdgeQueryType.Delete; } else if (request.type === ApiRequestType.Exists) { queryType = ApiEdgeQueryType.Exists; } else if (request.type === ApiRequestType.Change) { queryType = ApiEdgeQueryType.Change; } this.addMethodActions(ApiEdgeActionTriggerKind.AfterEvent, query, method, queryType, edge, output); query.unshift(new CallMethodQueryStep(method, edge)); this.addMethodActions(ApiEdgeActionTriggerKind.BeforeEvent, query, method, queryType, edge, output) } else { throw new ApiEdgeError(405, "Method Not Allowed"); } } private addQueryStep(query: ApiQuery, step: QueryEdgeQueryStep, relation: ApiEdgeRelation|null = null, output: boolean = false) { this.addQueryActions(ApiEdgeActionTriggerKind.AfterEvent, query, step.query, relation, output); query.unshift(step); this.addQueryActions(ApiEdgeActionTriggerKind.BeforeEvent, query, step.query, relation, output); } private static buildProvideIdStep(query: ApiQuery, currentSegment: PathSegment): boolean { if(currentSegment instanceof EntryPathSegment) { query.unshift(new ExtendContextLiveQueryStep(context => context.id = currentSegment.id)); return false } else if(currentSegment instanceof RelatedFieldPathSegment) { query.unshift(new ProvideIdQueryStep(currentSegment.relation.relationId)); return true } else { //TODO: Add support for method calls with parameters return false } } private buildCheckStep(query: ApiQuery, currentSegment: PathSegment): boolean { //STEP 1: Create the check query. //TODO: Check this code... if(currentSegment instanceof EntryPathSegment) { query.unshift(new SetResponseQueryStep(new ApiEdgeQueryResponse({ [currentSegment.edge.idField||Api.defaultIdField]: currentSegment.id }))); return false } else if(currentSegment instanceof RelatedFieldPathSegment) { this.addQueryStep(query, new QueryEdgeQueryStep(new ApiEdgeQuery(currentSegment.relation.to, ApiEdgeQueryType.Get)), currentSegment.relation); } else { //TODO: Add support for method calls (non-base query case) throw new ApiEdgeError(500, "Not Implemented") } //STEP 2: Provide ID for the check query. return ApiQueryBuilder.buildProvideIdStep(query, currentSegment) } private buildReadStep(query: ApiQuery, currentSegment: PathSegment): boolean { //STEP 1: Create the read query. if(currentSegment instanceof RelatedFieldPathSegment) { this.addQueryStep(query, new QueryEdgeQueryStep(new ApiEdgeQuery(currentSegment.relation.to, ApiEdgeQueryType.Get)), currentSegment.relation); } else { this.addQueryStep(query, new QueryEdgeQueryStep(new ApiEdgeQuery(currentSegment.edge, ApiEdgeQueryType.Get))); } //STEP 2: Provide ID for the read query. return ApiQueryBuilder.buildProvideIdStep(query, currentSegment) } private buildEmbedSteps(query: ApiQuery, request: ApiRequest, lastSegment: PathSegment) { if(request.type === ApiRequestType.Read && (lastSegment instanceof EdgePathSegment || (lastSegment instanceof MethodPathSegment && lastSegment.method.output === ApiEdgeMethodOutput.List))) { for (let relation of request.context.populatedRelations) { const segment = new EdgePathSegment(relation.to, relation); const embedRequest = new ApiRequest(request.api); embedRequest.path.add(segment); // We add the step directly directly, as pre- and post-actions are not // supported on embed query steps. These actions will be executed as // part of the sub-query. query.unshift(new EmbedQueryQueryStep(this.build(embedRequest), segment, embedRequest)); } } else { for (let relation of request.context.populatedRelations) { let segment: EdgePathSegment|EntryPathSegment; if(relation instanceof OneToManyRelation) { // TODO: Should we specify exactly array relations? segment = new EdgePathSegment(relation.to, relation); } else { // The id is literally TBD, it is going to be set once we have the data, // what we build now is only an execution plan. segment = new EntryPathSegment(relation.to, 'TBD', relation); } const embedRequest = new ApiRequest(request.api); embedRequest.path.add(segment); // We add the step directly, as pre- and post-actions are not // supported on embed query steps. These actions will be executed as // part of the sub-query. query.unshift(new EmbedQueryQueryStep(this.build(embedRequest), segment, embedRequest)); } } } private buildReadQuery = (request: ApiRequest): ApiQuery => { let query = new ApiQuery(); let segments = request.path.segments, lastSegment = segments[segments.length-1]; //STEP 0: Create embed queries this.buildEmbedSteps(query, request, lastSegment); //STEP 1: Create the base query which will provide the final data. let readMode = true; let baseQuery: ApiEdgeQuery; if(lastSegment instanceof EdgePathSegment) { baseQuery = new ApiEdgeQuery(lastSegment.edge, ApiEdgeQueryType.List); this.addQueryStep(query, new QueryEdgeQueryStep(baseQuery), null, true); } else if(lastSegment instanceof RelatedFieldPathSegment) { baseQuery = new ApiEdgeQuery(lastSegment.relation.to, ApiEdgeQueryType.Get); this.addQueryStep(query, new QueryEdgeQueryStep(baseQuery), lastSegment.relation, true); } else if(lastSegment instanceof MethodPathSegment) { this.addMethodCallStep(request, query, lastSegment.method, lastSegment.edge, true); if(lastSegment.method.scope === ApiEdgeMethodScope.Entry) { //TODO: Add support for providing id for Edge methods. query.unshift(new ProvideIdQueryStep(lastSegment.edge.idField)); } readMode = lastSegment.method.requiresData; } else { baseQuery = new ApiEdgeQuery(lastSegment.edge, ApiEdgeQueryType.Get); this.addQueryStep(query, new QueryEdgeQueryStep(baseQuery), null, true); } //STEP 2: Provide context for the base query. query.unshift(new ExtendContextQueryStep(request.context)); //STEP 3: Provide ID for the base query. if(lastSegment instanceof EntryPathSegment) { const _segment = lastSegment; //Add closure to make sure it won't be overridden later. query.unshift(new ExtendContextLiveQueryStep(context => context.id = _segment.id)) } else if(lastSegment instanceof RelatedFieldPathSegment) { if(lastSegment.relation.relatedId !== lastSegment.relation.to.idField) { query.unshift(new RelateBackwardsQueryStep(lastSegment.relation)); } else { query.unshift(new ProvideIdQueryStep(lastSegment.relation.relationId)) } } else { //TODO: Add support for method calls with parameters } //STEP 4: Provide filters and validation for the base query. for(let i = segments.length-2; i >= 0; i--) { let currentSegment = segments[i]; //STEP 1: Relate to the current query. let relation = segments[i+1].relation; let edge = segments[i+1].edge; if(relation && !(relation instanceof OneToOneRelation)) { if(edge === relation.to) { query.unshift(new RelateBackwardsQueryStep(relation)); } else { query.unshift(new RelateQueryStep(relation)); } } //STEP 2: Read or Check if(readMode) { readMode = this.buildReadStep(query, currentSegment) } else { readMode = this.buildCheckStep(query, currentSegment) } } //STEP 5: Add OnInput actions this.api.actions .filter((action: ApiAction) => action.triggerKind == ApiActionTriggerKind.OnInput) .forEach((action: ApiAction) => query.unshift(action)); //STEP 6: Return the completed query. return query }; private buildChangeQuery = (request: ApiRequest): ApiQuery => { let query = new ApiQuery(); let segments = request.path.segments, lastSegment = segments[segments.length-1], readMode = true; //STEP 0: Create embed queries this.buildEmbedSteps(query, request, lastSegment); //STEP 1: Create the base query which will provide the final data. let baseQuery: ApiEdgeQuery; if(lastSegment instanceof RelatedFieldPathSegment) { if(request.type === ApiRequestType.Update) { baseQuery = new ApiEdgeQuery(lastSegment.edge, ApiEdgeQueryType.Patch); request.body = { [lastSegment.relation.relationId]: request.body.id||request.body._id }; this.addQueryStep(query, new QueryEdgeQueryStep(baseQuery), null, true); } else if(request.type === ApiRequestType.Patch) { baseQuery = new ApiEdgeQuery(lastSegment.relation.to, ApiEdgeQueryType.Patch); this.addQueryStep(query, new QueryEdgeQueryStep(baseQuery), null, true); } else { throw new ApiEdgeError(400, "Invalid Delete Query"); } } else if(lastSegment instanceof MethodPathSegment) { this.addMethodCallStep(request, query, lastSegment.method, lastSegment.edge, true); if(lastSegment.method.scope === ApiEdgeMethodScope.Entry) { //TODO: Add support for providing id for Edge methods. query.unshift(new ProvideIdQueryStep(lastSegment.edge.idField)); } readMode = lastSegment.method.requiresData; } else { if(request.type === ApiRequestType.Update) { baseQuery = new ApiEdgeQuery(lastSegment.edge, ApiEdgeQueryType.Update); this.addQueryStep(query, new QueryEdgeQueryStep(baseQuery), null, true); } else if(request.type === ApiRequestType.Patch) { baseQuery = new ApiEdgeQuery(lastSegment.edge, ApiEdgeQueryType.Patch); this.addQueryStep(query, new QueryEdgeQueryStep(baseQuery), null, true); } else { baseQuery = new ApiEdgeQuery(lastSegment.edge, ApiEdgeQueryType.Delete); this.addQueryStep(query, new QueryEdgeQueryStep(baseQuery), null, true); } } //STEP 2: Provide context for the base query. query.unshift(new ExtendContextQueryStep(request.context)); //STEP 3: Provide ID for the base query. if(lastSegment instanceof EntryPathSegment) { const _segment = lastSegment; //Add closure to make sure it won't be overridden later. query.unshift(new ExtendContextLiveQueryStep(context => context.id = _segment.id)) } else if(lastSegment instanceof RelatedFieldPathSegment) { if(request.type === ApiRequestType.Update) { let previousSegment = segments[segments.length-2]; query.unshift(new ProvideIdQueryStep(previousSegment.edge.idField||Api.defaultIdField)); readMode = false; //Provide ID from the previous segment without querying the database. } else { query.unshift(new ProvideIdQueryStep(lastSegment.relation.relationId)) } } else { //TODO: Add support for method calls with parameters } //STEP 4: Provide filters and validation for the base query. for(let i = segments.length-2; i >= 0; i--) { let currentSegment = segments[i]; //STEP 1: Relate to the current query. let relation = segments[i+1].relation; let edge = segments[i+1].edge; if(relation && !(relation instanceof OneToOneRelation)) { if(edge === relation.to) { query.unshift(new RelateBackwardsQueryStep(relation)); } else { query.unshift(new RelateQueryStep(relation)); } if(request.type !== ApiRequestType.Delete) { if(edge === relation.to) { query.unshift(new RelateBackwardsChangeQueryStep(relation)) } else { query.unshift(new RelateChangeQueryStep(relation)) } } } //STEP 2: Read or Check if(readMode) { readMode = this.buildReadStep(query, currentSegment) } else { readMode = this.buildCheckStep(query, currentSegment) } } //STEP 5: Provide body for the query if(request.body || request.stream) query.unshift(new SetBodyQueryStep(request.body, request.stream)); //STEP 6: Add OnInput actions this.api.actions .filter((action: ApiAction) => action.triggerKind == ApiActionTriggerKind.OnInput) .forEach((action: ApiAction) => query.unshift(action)); //STEP 7: Return the completed query. return query }; private buildCreateQuery = (request: ApiRequest): ApiQuery => { let query = new ApiQuery(); let segments = request.path.segments, lastSegment = segments[segments.length-1], readMode = true; //STEP 0: Create embed queries this.buildEmbedSteps(query, request, lastSegment); //STEP 1: Create the base query which will provide the final data. if(lastSegment instanceof MethodPathSegment) { this.addMethodCallStep(request, query, lastSegment.method, lastSegment.edge, true); if(lastSegment.method.scope === ApiEdgeMethodScope.Entry) { //TODO: Add support for providing id for Edge methods. query.unshift(new ProvideIdQueryStep(lastSegment.edge.idField)); } readMode = lastSegment.method.requiresData; } else { this.addQueryStep(query, new QueryEdgeQueryStep(new ApiEdgeQuery(lastSegment.edge, ApiEdgeQueryType.Create))); } //STEP 2: Provide filters and validation for the base query. for(let i = segments.length-2; i >= 0; i--) { let currentSegment = segments[i]; //STEP 1: Relate to the current query. let relation = segments[i+1].relation; let edge = segments[i+1].edge; if(relation && !(relation instanceof OneToOneRelation)) { if(edge === relation.to) { query.unshift(new RelateBackwardsChangeQueryStep(relation)) } else { query.unshift(new RelateChangeQueryStep(relation)) } } //STEP 2: Read or Check if(readMode) { readMode = this.buildReadStep(query, currentSegment) } else { readMode = this.buildCheckStep(query, currentSegment) } } //STEP 3: Provide context for the base query. query.unshift(new SetBodyQueryStep(request.body, request.stream)); //STEP 4: Add OnInput actions this.api.actions .filter((action: ApiAction) => action.triggerKind == ApiActionTriggerKind.OnInput) .forEach((action: ApiAction) => query.unshift(action)); //STEP 5: Return the completed query. return query }; build = (request: ApiRequest): ApiQuery => { switch(request.type) { case ApiRequestType.Read: return this.buildReadQuery(request); case ApiRequestType.Update: case ApiRequestType.Patch: case ApiRequestType.Delete: return this.buildChangeQuery(request); case ApiRequestType.Create: return this.buildCreateQuery(request); default: throw new ApiEdgeError(400, "Unsupported Query Type") } } }