Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/core/src/models/artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ error:
message: 'You are not allowed to access this resource'
*/

export enum PaginationMode {
CURSOR = 'CURSOR',
OFFSET = 'OFFSET',
KEYSET = 'KEYSET',
}

export enum FieldInType {
QUERY = 'QUERY',
HEADER = 'HEADER',
Expand All @@ -44,6 +50,12 @@ export interface RequestSchema {
validators: Array<ValidatorDefinition>;
}

export interface PaginationSchema {
mode: PaginationMode;
// The key name used for do filtering by key for keyset pagination.
keyName?: string;
}

export interface ErrorInfo {
code: string;
message: string;
Expand All @@ -59,6 +71,9 @@ export interface APISchema {
request: Array<RequestSchema>;
errors: Array<ErrorInfo>;
response: any;
// The pagination strategy that do paginate when querying
// If not set pagination, then API request not provide the field to do it
pagination?: PaginationSchema;
}

export interface BuiltArtifact {
Expand Down
49 changes: 42 additions & 7 deletions packages/serve/src/lib/data-query/builder/dataQueryBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { IDataSource } from '@data-source/.';
import { Pagination } from '@route/.';

import { find, isEmpty } from 'lodash';
import {
ComparisonPredicate,
Expand Down Expand Up @@ -177,6 +180,8 @@ export interface SQLClauseOperation {
export interface IDataQueryBuilder {
readonly statement: string;
readonly operations: SQLClauseOperation;
readonly dataSource: IDataSource;

// Select clause methods
select(...columns: Array<SelectedColumn | string>): DataQueryBuilder;
distinct(...columns: Array<SelectedColumn | string>): DataQueryBuilder;
Expand Down Expand Up @@ -386,21 +391,29 @@ export interface IDataQueryBuilder {
limit(size: number): DataQueryBuilder;
offset(move: number): DataQueryBuilder;
take(size: number, move: number): DataQueryBuilder;
// paginate
paginate(pagination: Pagination): void;
value(): Promise<object>;
clone(): IDataQueryBuilder;
}

export class DataQueryBuilder implements IDataQueryBuilder {
public readonly statement: string;
// record all operations for different SQL clauses
public readonly operations: SQLClauseOperation;

public readonly dataSource: IDataSource;
public pagination?: Pagination;
constructor({
statement,
operations,
dataSource,
}: {
statement: string;
operations?: SQLClauseOperation;
dataSource: IDataSource;
}) {
this.statement = statement;
this.dataSource = dataSource;
this.operations = operations || {
select: null,
where: [],
Expand Down Expand Up @@ -601,6 +614,7 @@ export class DataQueryBuilder implements IDataQueryBuilder {
public whereWrapped(builderCallback: BuilderClauseCallback) {
const wrappedBuilder = new DataQueryBuilder({
statement: '',
dataSource: this.dataSource,
});
builderCallback(wrappedBuilder);
this.recordWhere({
Expand Down Expand Up @@ -1043,6 +1057,33 @@ export class DataQueryBuilder implements IDataQueryBuilder {
return this;
}

public clone() {
return new DataQueryBuilder({
statement: this.statement,
dataSource: this.dataSource,
operations: this.operations,
});
}

// setup pagination if would like to do paginate
public paginate(pagination: Pagination) {
this.pagination = pagination;
}

public async value() {
// call data source
const result = await this.dataSource.execute({
statement: this.statement,
operations: this.operations,
pagination: this.pagination,
});

// Reset operations
await this.resetOperations();

return result;
}

// record Select-On related operations
private recordSelect({
command,
Expand Down Expand Up @@ -1128,10 +1169,4 @@ export class DataQueryBuilder implements IDataQueryBuilder {
this.operations.limit = null;
this.operations.offset = null;
}
public value() {
// TODO: call Driver

// Reset operations
this.resetOperations();
}
}
7 changes: 6 additions & 1 deletion packages/serve/src/lib/data-query/factory.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { IDataSource } from '@data-source/.';
import {
DataQueryBuilder,
IDataQueryBuilder,
} from './builder/dataQueryBuilder';

export const dataQuery = (sqlStatement: string): IDataQueryBuilder => {
export const dataQuery = (
sqlStatement: string,
dataSource: IDataSource
): IDataQueryBuilder => {
return new DataQueryBuilder({
statement: sqlStatement,
dataSource: dataSource,
});
};
14 changes: 14 additions & 0 deletions packages/serve/src/lib/data-source/dataSource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { SQLClauseOperation } from '@data-query/.';
import { Pagination } from '@route/.';

export interface IDataSource {
execute({
statement,
operations,
pagination,
}: {
statement: string;
operations: SQLClauseOperation;
pagination?: Pagination;
}): Promise<object>;
}
1 change: 1 addition & 0 deletions packages/serve/src/lib/data-source/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './dataSource';
1 change: 1 addition & 0 deletions packages/serve/src/lib/pagination/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './strategy';
26 changes: 26 additions & 0 deletions packages/serve/src/lib/pagination/strategy/cursorBasedStrategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { KoaRouterContext } from '@route/route-component';
import { normalizeStringValue, PaginationMode } from '@vulcan/core';
import { PaginationStrategy } from './strategy';

export interface CursorPagination {
limit: number;
cursor: string;
}

export class CursorBasedStrategy extends PaginationStrategy<CursorPagination> {
public async transform(ctx: KoaRouterContext) {
const checkFelidInHeader = ['limit', 'cursor'].every((field) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're still checking the "header", it should be query too :D

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thanks founding the missing part, it has been fix~

Object.keys(ctx.request.query).includes(field)
);
if (!checkFelidInHeader)
throw new Error(
`The ${PaginationMode.CURSOR} must provide limit and cursor in query string.`
);
const limitVal = ctx.request.query['limit'] as string;
const cursorVal = ctx.request.query['cursor'] as string;
return {
limit: normalizeStringValue(limitVal, 'limit', Number.name),
cursor: normalizeStringValue(cursorVal, 'cursor', Number.name),
} as CursorPagination;
}
}
4 changes: 4 additions & 0 deletions packages/serve/src/lib/pagination/strategy/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './strategy';
export * from './offsetBasedStrategy';
export * from './cursorBasedStrategy';
export * from './keysetBasedStrategy';
41 changes: 41 additions & 0 deletions packages/serve/src/lib/pagination/strategy/keysetBasedStrategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
APISchema,
normalizeStringValue,
PaginationMode,
PaginationSchema,
} from '@vulcan/core';
import { KoaRouterContext } from '@route/route-component';
import { PaginationStrategy } from './strategy';

export interface KeysetPagination {
limit: number;
[keyName: string]: string | number;
}

export class KeysetBasedStrategy extends PaginationStrategy<KeysetPagination> {
private pagination: PaginationSchema;
constructor(pagination: PaginationSchema) {
super();
this.pagination = pagination;
}
public async transform(ctx: KoaRouterContext) {
if (!this.pagination.keyName)
throw new Error(
`The keyset pagination need to set "keyName" in schema for indicate what key need to do filter.`
);
const { keyName } = this.pagination;
const checkFelidInHeader = ['limit', keyName].every((field) =>
Object.keys(ctx.request.query).includes(field)
);
if (!checkFelidInHeader)
throw new Error(
`The ${PaginationMode.KEYSET} must provide limit and offset in query string.`
);
const limitVal = ctx.request.query['limit'] as string;
const keyNameVal = ctx.request.query[keyName] as string;
return {
limit: normalizeStringValue(limitVal, 'limit', Number.name),
[keyName]: normalizeStringValue(keyNameVal, keyName, Number.name),
} as KeysetPagination;
}
}
26 changes: 26 additions & 0 deletions packages/serve/src/lib/pagination/strategy/offsetBasedStrategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { normalizeStringValue, PaginationMode } from '@vulcan/core';
import { KoaRouterContext } from '@route/route-component';
import { PaginationStrategy } from './strategy';

export interface OffsetPagination {
limit: number;
offset: number;
}

export class OffsetBasedStrategy extends PaginationStrategy<OffsetPagination> {
public async transform(ctx: KoaRouterContext) {
const checkFelidInHeader = ['limit', 'offset'].every((field) =>
Object.keys(ctx.request.query).includes(field)
);
if (!checkFelidInHeader)
throw new Error(
`The ${PaginationMode.OFFSET} must provide limit and offset in query string.`
);
const limitVal = ctx.request.query['limit'] as string;
const offsetVal = ctx.request.query['offset'] as string;
return {
limit: normalizeStringValue(limitVal, 'limit', Number.name),
offset: normalizeStringValue(offsetVal, 'offset', Number.name),
} as OffsetPagination;
}
}
5 changes: 5 additions & 0 deletions packages/serve/src/lib/pagination/strategy/strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { KoaRouterContext } from '@route/route-component';

export abstract class PaginationStrategy<T> {
public abstract transform(ctx: KoaRouterContext): Promise<T>;
}
33 changes: 19 additions & 14 deletions packages/serve/src/lib/route/route-component/baseRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,52 @@ import { RouterContext as KoaRouterContext } from 'koa-router';
import { IRequestTransformer, RequestParameters } from './requestTransformer';
import { IRequestValidator } from './requestValidator';
import { APISchema, TemplateEngine } from '@vulcan/core';
import { IPaginationTransformer, Pagination } from './paginationTransformer';
export { KoaRouterContext, KoaNext };

interface TransformedRequest {
reqParams: RequestParameters;
pagination?: Pagination;
}

interface IRoute {
respond(ctx: KoaRouterContext): Promise<any>;
}

export abstract class BaseRoute implements IRoute {
public readonly apiSchema: APISchema;
private readonly reqTransformer: IRequestTransformer;
private readonly reqValidator: IRequestValidator;
private readonly templateEngine: TemplateEngine;

protected readonly reqTransformer: IRequestTransformer;
protected readonly reqValidator: IRequestValidator;
protected readonly templateEngine: TemplateEngine;
protected readonly paginationTransformer: IPaginationTransformer;
constructor({
apiSchema,
reqTransformer,
reqValidator,
paginationTransformer,
templateEngine,
}: {
apiSchema: APISchema;
reqTransformer: IRequestTransformer;
reqValidator: IRequestValidator;
paginationTransformer: IPaginationTransformer;
templateEngine: TemplateEngine;
}) {
this.apiSchema = apiSchema;
this.reqTransformer = reqTransformer;
this.reqValidator = reqValidator;
this.paginationTransformer = paginationTransformer;
this.templateEngine = templateEngine;
}

public async respond(ctx: KoaRouterContext) {
const params = await this.reqTransformer.transform(ctx, this.apiSchema);
await this.reqValidator.validate(params, this.apiSchema);
return await this.handleRequest(ctx, params);
}
public abstract respond(ctx: KoaRouterContext): Promise<any>;

protected abstract handleRequest(
ctx: KoaRouterContext,
reqParams: RequestParameters
): Promise<any>;
protected abstract prepare(
ctx: KoaRouterContext
): Promise<TransformedRequest>;

protected async runQuery(reqParams: RequestParameters) {
protected async handle(transformed: TransformedRequest) {
const { reqParams } = transformed;
// could template name or template path, use for template engine
const { templateSource } = this.apiSchema;
const statement = await this.templateEngine.render(
Expand Down
Loading