Last active
March 12, 2025 01:06
-
-
Save ezzabuzaid/94a8b865b806dc89214e5b01253d2f78 to your computer and use it in GitHub Desktop.
January archive
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import Ajv from 'ajv'; | |
| import addErrors from 'ajv-errors'; | |
| import addFormats from 'ajv-formats'; | |
| import validator from 'validator'; | |
| import { ErrorObject, JSONSchemaType } from 'ajv'; | |
| import { PartialSchema } from 'ajv/dist/types/json-schema'; | |
| import { ProblemDetailsException } from 'rfc-7807-problem-details'; | |
| const ajv = new Ajv({ | |
| allErrors: true, | |
| useDefaults: true, | |
| removeAdditional: 'failing', | |
| coerceTypes: true, | |
| }); | |
| addErrors(ajv); | |
| addFormats(ajv); | |
| function isBetween(date: string, startDate: string, endDate: string) { | |
| if (!date) { | |
| return false; | |
| } | |
| if (!startDate) { | |
| return false; | |
| } | |
| if (!endDate) { | |
| return false; | |
| } | |
| return ( | |
| validator.isAfter(date, startDate) && validator.isBefore(date, endDate) | |
| ); | |
| } | |
| type Input<T> = | |
| T extends Record<infer K, any> | |
| ? { | |
| [P in K]: unknown; | |
| } | |
| : never; | |
| const validations = [ | |
| ['isBefore', validator.isBefore], | |
| ['isAfter', validator.isAfter], | |
| ['isBoolean', validator.isBoolean], | |
| ['isDate', validator.isDate], | |
| ['isNumeric', validator.isNumeric], | |
| ['isLatLong', validator.isLatLong], | |
| ['isMobilePhone', validator.isMobilePhone], | |
| ['isEmpty', validator.isEmpty], | |
| ['isDecimal', validator.isDecimal], | |
| ['isURL', validator.isURL], | |
| ['isEmail', validator.isEmail], | |
| ['isBetween', isBetween], | |
| ]; | |
| validations.forEach(([key, value]) => { | |
| const keyword = key as string; | |
| ajv.addKeyword({ | |
| keyword: keyword, | |
| validate: (schema: any, data: any) => { | |
| if (schema === undefined || schema === null) { | |
| return false; | |
| } | |
| const func = value as any; | |
| return func.apply(validator, [ | |
| data, | |
| ...(Array.isArray(schema) ? schema : [schema]), | |
| ]); | |
| }, | |
| }); | |
| }); | |
| export function createSchema<T>( | |
| properties: Record< | |
| keyof T, | |
| PartialSchema<any> & { | |
| required?: boolean; | |
| } | |
| >, | |
| ): PartialSchema<T> { | |
| const required: string[] = []; | |
| const requiredErrorMessages: Record<string, string> = {}; | |
| for (const [key, value] of Object.entries(properties) as any[]) { | |
| if (value.required) { | |
| required.push(key); | |
| } | |
| if ('errorMessage' in value && value.errorMessage?.required) { | |
| // move the required error message from the property schema to the root schema | |
| // as the required keyword is not part of the property schema | |
| requiredErrorMessages[key] = value.errorMessage.required; | |
| delete value.errorMessage.required; | |
| } | |
| } | |
| const extendSchema: Record<string, unknown> = {}; | |
| if (Object.keys(requiredErrorMessages).length) { | |
| extendSchema['errorMessage'] = { | |
| required: requiredErrorMessages, | |
| }; | |
| } | |
| const clearProperties = Object.fromEntries( | |
| (Object.entries(properties) as any[]).map(([key, value]) => { | |
| const { required, ...rest } = value; | |
| return [key, rest]; | |
| }), | |
| ); | |
| return { | |
| type: 'object', | |
| properties: clearProperties, | |
| required: required, | |
| additionalProperties: false, | |
| ...extendSchema, | |
| } as JSONSchemaType<T>; | |
| } | |
| /** | |
| * Validate input against schema | |
| * | |
| * @param schema ajv augmented json-schema | |
| * @param input input to validate | |
| * @returns | |
| */ | |
| export function validateInput<T>( | |
| schema: PartialSchema<T>, | |
| input: Record<keyof T, unknown>, | |
| ): asserts input is T { | |
| const validate = ajv.compile(schema); | |
| const valid = validate(input); | |
| if (!valid && validate.errors) { | |
| throw formatErrors(validate.errors); | |
| } | |
| } | |
| function formatErrors( | |
| errors: ErrorObject<string, Record<string, any>, unknown>[], | |
| parent?: ErrorObject<string, Record<string, any>, unknown>, | |
| ): ErrorObject<string, Record<string, any>, unknown> { | |
| return errors.reduce( | |
| (acc, it) => { | |
| if (it.keyword === 'errorMessage') { | |
| return { | |
| ...acc, | |
| ...formatErrors(it.params['errors'], it), | |
| }; | |
| } | |
| const property = (it.instancePath || it.params['missingProperty']) | |
| .replace('.', '') | |
| .replace('/', ''); | |
| return { ...acc, [property]: parent?.message || it.message || '' }; | |
| }, | |
| {} as ErrorObject<string, Record<string, any>, unknown>, | |
| ); | |
| } | |
| export function validateOrThrow<T>( | |
| schema: PartialSchema<T>, | |
| input: Record<keyof T, unknown>, | |
| ): asserts input is T { | |
| try { | |
| validateInput(schema, input); | |
| } catch (errors: any) { | |
| const exception = new ProblemDetailsException({ | |
| type: 'validation-failed', | |
| status: 400, | |
| title: 'Bad Request.', | |
| detail: 'Validation failed.', | |
| }); | |
| exception.Details.errors = errors; | |
| throw exception; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { classes } from '@automapper/classes'; | |
| import { | |
| CamelCaseNamingConvention, | |
| createMap, | |
| createMapper, | |
| } from '@automapper/core'; | |
| export const mapper = createMapper({ | |
| strategyInitializer: classes(), | |
| namingConventions: new CamelCaseNamingConvention(), | |
| }); | |
| export function AutoMapHost(): ClassDecorator { | |
| return function (target) { | |
| createMap(mapper, target as never, target as never); | |
| }; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import type { Plugin } from 'prettier'; | |
| export async function formatCode( | |
| code: string, | |
| extension?: string, | |
| ignoreError = true | |
| ): Promise<string> { | |
| if (!code || code.trim().length === 0) return ''; | |
| function whatIsParserImport(): { | |
| parserImport: Promise<Plugin>[]; | |
| parserName: string; | |
| } { | |
| switch (extension) { | |
| case 'ts': | |
| return { | |
| parserImport: [import('prettier/plugins/typescript')], | |
| parserName: 'typescript', | |
| }; | |
| case 'js': | |
| return { | |
| parserImport: [import('prettier/plugins/babel')], | |
| parserName: 'babel', | |
| }; | |
| case 'html': | |
| return { | |
| parserImport: [import('prettier/plugins/html')], | |
| parserName: 'html', | |
| }; | |
| case 'css': | |
| return { | |
| parserImport: [import('prettier/plugins/postcss')], | |
| parserName: 'css', | |
| }; | |
| case 'scss': | |
| return { | |
| parserImport: [import('prettier/plugins/postcss')], | |
| parserName: 'scss', | |
| }; | |
| case 'code-snippets': | |
| case 'json': | |
| case 'prettierrc': | |
| return { | |
| parserImport: [import('prettier/plugins/babel')], | |
| parserName: 'json', | |
| }; | |
| case 'md': | |
| return { | |
| parserImport: [import('prettier/plugins/markdown')], | |
| parserName: 'markdown', | |
| }; | |
| case 'yaml': | |
| case 'yml': | |
| return { | |
| parserImport: [import('prettier/plugins/yaml')], | |
| parserName: 'yaml', | |
| }; | |
| case '': | |
| case 'gitignore': | |
| case 'dockerignore': | |
| case 'prettierignore': | |
| case 'Dockerfile': | |
| case 'toml': | |
| case 'env': | |
| case 'txt': | |
| return { | |
| parserImport: [], | |
| parserName: '', | |
| }; | |
| default: | |
| return { | |
| parserImport: [], | |
| parserName: '', | |
| }; | |
| } | |
| } | |
| const { parserImport, parserName } = whatIsParserImport(); | |
| if (!parserName) return code; | |
| const [prettier, ...plugins] = await Promise.all([ | |
| import('prettier/standalone'), | |
| import('prettier/plugins/estree').then((e) => e as any), | |
| ...parserImport, | |
| ] as const); | |
| try { | |
| return prettier | |
| .format(code, { | |
| parser: parserName, | |
| plugins: plugins, | |
| singleQuote: true, | |
| }) | |
| .then((formattedCode) => formattedCode.trim()); | |
| } catch (error) { | |
| if (error instanceof Error) | |
| if (error.name === 'SyntaxError') { | |
| return ignoreError === true ? code : formatCode(code, 'ts', true); | |
| } | |
| if (!ignoreError) { | |
| throw error; | |
| } | |
| return code; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| interface Language { | |
| code: string; | |
| name: string; | |
| native: string; | |
| } | |
| interface Location { | |
| geoname_id: number; | |
| capital: string; | |
| languages: Language[]; | |
| country_flag: string; | |
| country_flag_emoji: string; | |
| country_flag_emoji_unicode: string; | |
| calling_code: string; | |
| is_eu: boolean; | |
| } | |
| interface TimeZone { | |
| id: string; | |
| current_time: Date; | |
| gmt_offset: number; | |
| code: string; | |
| is_daylight_saving: boolean; | |
| } | |
| interface Currency { | |
| code: string; | |
| name: string; | |
| plural: string; | |
| symbol: string; | |
| symbol_native: string; | |
| } | |
| interface Connection { | |
| asn: number; | |
| isp: string; | |
| } | |
| export interface IpData { | |
| ip: string; | |
| type: string; | |
| continent_code: string; | |
| continent_name: string; | |
| country_code: string; | |
| country_name: string; | |
| region_code: string; | |
| region_name: string; | |
| city: string; | |
| zip: string; | |
| latitude: number; | |
| longitude: number; | |
| location: Location; | |
| time_zone: TimeZone; | |
| currency: Currency; | |
| connection: Connection; | |
| } | |
| export async function lookupIp(ip?: string | null) { | |
| const access_key = ''; | |
| return ip | |
| ? await ( | |
| await fetch(`https://api.ipapi.com/api/${ip}?access_key=${access_key}`) | |
| ).json() | |
| : undefined; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| export type WithChildren<T extends { id: string }> = T & { | |
| children: WithChildren<T>[]; | |
| data: Record<string, any>; | |
| }; | |
| export interface WithSequence { | |
| sequence: string; | |
| parentSequence?: string; | |
| siblingSequence?: string; | |
| } | |
| export interface SourceTarget { | |
| source: string; | |
| target: string; | |
| data: Record<string, any>; | |
| } | |
| export interface ITree<T extends WithSequence = WithSequence> { | |
| source?: T; | |
| target?: T; | |
| paths: ITree<T>[]; | |
| type: 'root' | 'leaf' | 'branch'; | |
| orphan?: boolean; | |
| self: T; | |
| } | |
| export interface IBetterTree<T> { | |
| id: string; | |
| children: IBetterTree<T>[]; | |
| data: Record<string, any>; | |
| } | |
| export interface Edge<T extends WithSequence> { | |
| source: T; | |
| target: T; | |
| path: boolean; | |
| } | |
| export class LinearTree<T extends WithSequence> { | |
| line: ITree<T>[] = []; | |
| orphans: T[] = []; | |
| constructor(root: T) { | |
| this.line.push({ | |
| self: root, | |
| source: undefined, | |
| target: undefined, | |
| paths: [], | |
| type: 'root', | |
| }); | |
| } | |
| #findParent(node: T) { | |
| if (!node.parentSequence) { | |
| return this.line; | |
| } | |
| const stack = [...this.line]; | |
| while (stack.length) { | |
| const subtree = stack.pop()!; | |
| if (node.parentSequence === subtree.self.sequence) { | |
| return subtree.paths; | |
| } | |
| stack.push(...subtree.paths); | |
| } | |
| return null; | |
| } | |
| addNode(node: T) { | |
| const parent = this.#findParent(node); | |
| switch (true) { | |
| case !parent: | |
| this.orphans.push(node); | |
| return; | |
| case parent?.length === 0: | |
| parent.push({ | |
| self: node, | |
| source: undefined, | |
| target: undefined, | |
| paths: [], | |
| type: 'leaf', | |
| }); | |
| this.#tryAddOrphans(); | |
| return; | |
| case !node.siblingSequence: | |
| // at this point we know that the parent is not empty | |
| // and the node has no sibling | |
| parent.push({ | |
| self: node, | |
| source: undefined, | |
| target: undefined, | |
| paths: [], | |
| type: 'leaf', | |
| }); | |
| this.#tryAddOrphans(); | |
| return; | |
| } | |
| for (const branch of parent) { | |
| if (node.siblingSequence === branch.self.sequence) { | |
| // sibiling found, good. now need to add it after the sibiling | |
| const index = parent.findIndex( | |
| (it) => it.source?.sequence === branch.source?.sequence | |
| ); | |
| const sourceSibiling = parent[index]; | |
| const targetSibiling = parent[index + 1]; | |
| sourceSibiling.target = node; | |
| // NOTE: if the node have same sequance sibling, it'll conflict | |
| parent.splice(index + 1, 0, { | |
| self: node, | |
| source: sourceSibiling.self, | |
| target: targetSibiling?.self, | |
| paths: [], | |
| type: 'leaf', | |
| }); | |
| this.#tryAddOrphans(); | |
| return; | |
| } | |
| if (node.sequence === branch.self.siblingSequence) { | |
| // sibiling found, good. now need to add it before the sibiling | |
| const index = parent.findIndex( | |
| (it) => it.source?.sequence === branch.source?.sequence | |
| ); | |
| const sibiling = parent[index]; | |
| sibiling.target = node; | |
| parent.splice(index - 1, 0, { | |
| self: node, | |
| source: sibiling.self, | |
| target: undefined, | |
| paths: [], | |
| type: 'leaf', | |
| }); | |
| this.#tryAddOrphans(); | |
| return; | |
| } | |
| } | |
| this.orphans.push(node); | |
| } | |
| #tryAddOrphans() { | |
| // check if there are orphans that can be added | |
| const clone = [...this.orphans]; | |
| this.orphans = []; | |
| clone.forEach((orphan) => this.addNode(orphan)); | |
| } | |
| connect(source: WithSequence, target: WithSequence) { | |
| // | |
| } | |
| toJson() { | |
| // | |
| } | |
| toEdges(stack = [...this.line], path = false) { | |
| const edges: Edge<T>[] = []; | |
| while (stack.length) { | |
| const subtreeTarget = stack.pop(); | |
| if (!subtreeTarget || !subtreeTarget.source) { | |
| continue; | |
| } | |
| const subtreeSource = stack.at(-1)!; | |
| edges.push({ | |
| source: subtreeSource.self, | |
| target: subtreeTarget.self, | |
| path: path, | |
| }); | |
| if (subtreeTarget.paths) { | |
| const vnodes = subtreeTarget.paths | |
| .map((it) => [ | |
| { | |
| ...subtreeTarget, | |
| source: undefined, // remove the source as this acts as root to its paths | |
| }, | |
| { ...it, source: subtreeTarget.self }, | |
| ]) | |
| .flat(); | |
| edges.push(...this.toEdges(vnodes, true)); | |
| } | |
| } | |
| return edges; | |
| } | |
| pretty() { | |
| for (const orphan of this.orphans) { | |
| const parent = this.#findParent(orphan) ?? [...this.line]; | |
| parent.push({ | |
| self: orphan, | |
| source: undefined, | |
| target: undefined, | |
| paths: [], | |
| type: 'leaf', | |
| orphan: true, | |
| }); | |
| } | |
| return this.line; | |
| } | |
| } | |
| export class BetterTree<T extends SourceTarget> { | |
| line: IBetterTree<T>[] = []; | |
| orphans: T[] = []; | |
| constructor( | |
| private start: { | |
| source: string; | |
| nodes: T[]; | |
| } | |
| ) { | |
| this.start.nodes.forEach((node) => this.addNode(node)); | |
| } | |
| #findParent(node: T) { | |
| const stack = [...this.line]; | |
| while (stack.length) { | |
| const subtree = stack.pop()!; | |
| if (node.source === subtree.id) { | |
| return subtree; | |
| } | |
| stack.push(...subtree.children); | |
| } | |
| return null; | |
| } | |
| #tryAddOrphans() { | |
| // check if there are orphans that can be added | |
| const clone = [...this.orphans]; | |
| this.orphans = []; | |
| clone.forEach((orphan) => this.addNode(orphan)); | |
| } | |
| addNode(node: T) { | |
| const [root] = this.line; | |
| if (!root) { | |
| const maybeRoot = this.start.source === node.source ? node : null; | |
| if (maybeRoot) { | |
| this.line.push({ | |
| id: node.source, | |
| children: [], | |
| data: node.data, | |
| }); | |
| this.line.push({ | |
| id: node.target, | |
| children: [], | |
| data: node.data, | |
| }); | |
| this.#tryAddOrphans(); | |
| return; | |
| } else { | |
| this.orphans.push(node); | |
| return; | |
| } | |
| } | |
| if (node.data?.['child']) { | |
| const parent = this.#findParent(node); | |
| if (parent) { | |
| parent.children.push({ | |
| id: node.target, | |
| children: [], | |
| data: node.data, | |
| }); | |
| this.#tryAddOrphans(); | |
| } else { | |
| this.orphans.push(node); | |
| } | |
| return; | |
| } else { | |
| const last = this.line.at(-1); | |
| if (!last) { | |
| throw new Error(`No parent found for ${node.source}`); | |
| } | |
| if (last.id === node.source) { | |
| this.line.push({ | |
| id: node.target, | |
| children: [], | |
| data: node.data, | |
| }); | |
| this.#tryAddOrphans(); | |
| } else { | |
| this.orphans.push(node); | |
| } | |
| } | |
| } | |
| map<R extends { id: string }>( | |
| mapFn: (id: string) => R, | |
| array = [...this.line] | |
| ) { | |
| const list: WithChildren<R>[] = []; | |
| while (array.length) { | |
| const node = array.shift()!; | |
| list.push({ | |
| ...node, | |
| ...mapFn(node.id), | |
| children: this.map(mapFn, [...node.children]), | |
| data: node.data, | |
| }); | |
| } | |
| return list; | |
| } | |
| } | |
| // const tree = new BetterTree<SourceTarget>({ | |
| // source: '7986d237-e6e1-4ae4-8b34-fd677e63028a', | |
| // nodes, | |
| // }); | |
| // console.dir(tree.line, { | |
| // showHidden: false, | |
| // depth: Infinity, | |
| // maxArrayLength: Infinity, | |
| // colors: true, | |
| // }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { | |
| GenericQueryCondition, | |
| QueryBuilder, | |
| QueryColumn, | |
| Visitor, | |
| toAst, | |
| } from '@january/compiler/transpilers'; | |
| import { camelcase } from 'stringcase'; | |
| import { pascalcase } from '@faslh/utils'; | |
| export class TypeOrmVisitor extends Visitor<string | null> { | |
| constructor( | |
| private _rootExpr: QueryBuilder.QuerySelectExpr, | |
| private tableName: string, | |
| private qbVar = 'qb' | |
| ) { | |
| super(); | |
| } | |
| private _wrapInIfStatement(condition: any, statement: string) { | |
| return `if(${condition}) { | |
| ${statement} | |
| }`; | |
| } | |
| private _wrapBrackets(statement: string) { | |
| // https://github.com/typeorm/typeorm/issues/6170#issuecomment-832790446 | |
| return `new Brackets(qb => {${statement}})`; | |
| // return statement | |
| } | |
| private _addSelect(columns: QueryColumn[]) { | |
| // use it to group by non-aggregated columns | |
| const aggregateFound = columns.some((column) => column.aggregator); | |
| const groupbyCols = columns | |
| .filter((column) => !column.aggregator) | |
| .map((column) => `'${this.tableName}.${column.name}'`); | |
| const selectColumns = columns | |
| .map((column) => { | |
| if (column.aggregator) { | |
| /** | |
| * we are using scape character because of having sql aggregators inside string | |
| * the generated Code will be like this: | |
| * "qb.addSelect('avg(\'Transactions.sales\') as avgSales');" | |
| */ | |
| return `'${column.aggregator}(\"${this.tableName}.${ | |
| column.name | |
| }\") as ${column.alias ?? column.name}'`; | |
| } else if (column.name.includes('.')) { | |
| return `'${column.name} as ${ | |
| column.alias ?? column.name.split('.')[1] | |
| }'`; | |
| } | |
| return `'${this.tableName}.${column.name}'`; | |
| }) | |
| .join(', '); | |
| return `${this.qbVar}.addSelect(${selectColumns});${ | |
| aggregateFound && groupbyCols.length | |
| ? `${this.qbVar}.addGroupBy(${groupbyCols.join(', ')});` | |
| : '' | |
| }`; | |
| } | |
| private _addJoins(joinsFields: string[]) { | |
| return joinsFields | |
| .map((join) => { | |
| return `${this.qbVar}.innerJoin('${ | |
| this.tableName | |
| }.${join.toLowerCase()}', '${join}');`; | |
| }) | |
| .join(';'); | |
| } | |
| public _visitBetween( | |
| identifier: QueryBuilder.Identifier, | |
| expr: QueryBuilder.BinaryExpression< | |
| QueryBuilder.Identifier, | |
| QueryBuilder.Identifier | |
| >, | |
| context: any | |
| ) { | |
| const left = expr.left.accept(this, { | |
| format: (value: string) => { | |
| return `:${value}`; | |
| }, | |
| }); | |
| const right = expr.right.accept(this, { | |
| format: (value: string) => { | |
| return `:${value}`; | |
| }, | |
| }); | |
| const statement = `${identifier.accept(this, { | |
| format: (value: string) => { | |
| const [tableNameOrFieldName, maybeFieldName] = value.split('.'); | |
| if (!maybeFieldName) { | |
| return `${this.tableName}.${value}`; | |
| } | |
| return `${tableNameOrFieldName}.${maybeFieldName}`; | |
| }, | |
| })} BETWEEN :min AND :max`; | |
| const parameters = `{ min: ${left}, max: ${right} }`; | |
| const query = `${this.qbVar}.${context.combinator}Where('${statement}', ${parameters})`; | |
| return query; | |
| } | |
| public visitDateLiteralExpr( | |
| expr: QueryBuilder.Literal<'date'>, | |
| context: any | |
| ): any { | |
| // Why not use StringLiteral instead? | |
| return `'${expr.value}'`; | |
| } | |
| public visitBinaryExpr( | |
| expr: QueryBuilder.BinaryExpression<any, any>, | |
| context: any | |
| ): any { | |
| switch (expr.operator) { | |
| case 'between': | |
| return this._visitBetween(expr.left, expr.right, context); | |
| case 'like': | |
| return this._visitLike(expr, { | |
| ...context, | |
| required: context.required ?? false, | |
| operator: context.inverse ? 'NOT LIKE' : 'LIKE', | |
| prefix: context.prefix ?? '', | |
| postfix: context.postfix ?? '', | |
| }); | |
| case 'is': | |
| return this._visitIs(expr, { | |
| ...context, | |
| operator: context.inverse ? 'IS NOT' : 'IS', | |
| }); | |
| case '===': | |
| return this._equal(expr, { | |
| ...context, | |
| operator: context.inverse ? '!=' : '=', | |
| }); | |
| case '<': | |
| return this._equal(expr, { | |
| ...context, | |
| operator: context.inverse ? '>' : '<', | |
| }); | |
| case '>': | |
| return this._equal(expr, { | |
| ...context, | |
| operator: context.inverse ? '<' : '>', | |
| }); | |
| case '<=': | |
| return this._equal(expr, { | |
| ...context, | |
| operator: context.inverse ? '>=' : '<=', | |
| }); | |
| case '>=': | |
| return this._equal(expr, { | |
| ...context, | |
| operator: context.inverse ? '<=' : '>=', | |
| }); | |
| case 'in': | |
| return this._visitIn(expr, { | |
| ...context, | |
| operator: context.inverse ? 'NOT IN' : 'IN', | |
| }); | |
| default: | |
| throw new Error( | |
| `Expression is not supported. operator: ${expr.operator}` | |
| ); | |
| } | |
| } | |
| private _visitIs(expr: QueryBuilder.BinaryExpression, context: any) { | |
| const left = this._acceptLeftExpression(expr, context); | |
| const binding = this._acceptBindings(expr, context); | |
| const statement = `${left} ${context.operator} ${binding}`; | |
| const parameters = this._acceptParameters(expr, context); | |
| const isRequired = context.required === true; | |
| const query = `${this.qbVar}.${context.combinator}Where('${statement}', ${parameters})`; | |
| const finalQuery = !isRequired | |
| ? this._wrapInIfStatement(expr.right.accept(this, {}), query) | |
| : query; | |
| return finalQuery; | |
| } | |
| private _visitLike(expr: QueryBuilder.BinaryExpression, context: any) { | |
| const left = this._acceptLeftExpression(expr, context); | |
| const binding = this._acceptBindings(expr, context); | |
| const statement = `${left} ${context.operator} ${binding}`; | |
| const right = expr.right.accept(this, { | |
| ...context, | |
| }); | |
| const keyName = expr.left.accept(this, { | |
| ...context, | |
| format: (value: string) => { | |
| const [tableNameOrFieldName, fieldName] = value.split('.'); | |
| if (!fieldName) { | |
| return `${tableNameOrFieldName}`; | |
| } | |
| return `${camelcase(value)}`; | |
| }, | |
| }); | |
| const rightWithBinding = `${ | |
| context.prefix ? `'${context.prefix}' + ` : '' | |
| }${right}${context.postfix ? ` + '${context.postfix}'` : ''}`; | |
| const valueName = expr.right.accept(this, { | |
| ...context, | |
| }); | |
| const parameters = `{${keyName}: ${rightWithBinding}}`; | |
| const isRequired = context.required === true; | |
| const query = `${this.qbVar}.${context.combinator}Where('${statement}', ${parameters})`; | |
| const finalQuery = !isRequired | |
| ? this._wrapInIfStatement(valueName, query) | |
| : query; | |
| return finalQuery; | |
| } | |
| private _visitIn(expr: QueryBuilder.BinaryExpression, context: any) { | |
| const left = this._acceptLeftExpression(expr, context); | |
| const right = expr.right.accept(this, { | |
| ...context, | |
| }); | |
| const binding = this._acceptBindings(expr, context); | |
| const statement = `${left} ${context.operator} ${binding}`; | |
| const keyName = expr.left.accept(this, { | |
| ...context, | |
| format: (value: string) => { | |
| const [tableName, prop] = value.split('.'); | |
| if (!prop) { | |
| return `${value}`; | |
| } | |
| return `${camelcase(value)}`; | |
| }, | |
| }); | |
| const parameters = `{ ${keyName}: ${right} }`; | |
| const valueName = expr.right.accept(this, { | |
| ...context, | |
| }); | |
| const isRequired = context.required === true; | |
| const query = `${this.qbVar}.${context.combinator}Where('${statement}', ${parameters})`; | |
| const finalQuery = !isRequired | |
| ? this._wrapInIfStatement(valueName, query) | |
| : query; | |
| return finalQuery; | |
| } | |
| _acceptLeftExpression( | |
| expr: QueryBuilder.BinaryExpression<any, any>, | |
| context: any | |
| ) { | |
| return expr.left.accept(this, { | |
| ...context, | |
| format: (value: string) => { | |
| const [tableNameOrFieldName, maybeFieldName] = value.split('.'); | |
| if (!maybeFieldName) { | |
| return `${this.tableName}.${value}`; | |
| } | |
| return `${tableNameOrFieldName}.${maybeFieldName}`; | |
| }, | |
| }); | |
| } | |
| _acceptBindings( | |
| expr: QueryBuilder.BinaryExpression<any, any>, | |
| context: { | |
| operator: string; | |
| } & any | |
| ) { | |
| return expr.left.accept(this, { | |
| ...context, | |
| format: (value: string) => { | |
| const [tableName, prop] = value.split('.'); | |
| if (!prop) { | |
| return `:${value}`; | |
| } | |
| return `:${camelcase(value)}`; | |
| }, | |
| }); | |
| } | |
| _acceptParameters( | |
| expr: QueryBuilder.BinaryExpression<any, any>, | |
| context: { | |
| operator: string; | |
| } & any | |
| ) { | |
| const valueName = expr.right.accept(this, { | |
| ...context, | |
| }); | |
| const keyName = expr.left.accept(this, { | |
| ...context, | |
| format: (value: string) => { | |
| const [tableName, prop] = value.split('.'); | |
| if (!prop) { | |
| return `${value}`; | |
| } | |
| return `${camelcase(value)}`; | |
| }, | |
| }); | |
| if (valueName === 'WILL_BE_USED_FROM_DESTRUCTURED_INPUT') { | |
| return `{${keyName}}`; | |
| } | |
| return `{ ${keyName}: ${valueName} }`; | |
| } | |
| private _equal( | |
| expr: QueryBuilder.BinaryExpression<any, any>, | |
| context: { | |
| operator: string; | |
| } & any | |
| ) { | |
| // TODO: use template design pattern to override specific steps | |
| // for instance, _visitLike method uses same logic here with only | |
| // difference being that right expressions is formatted differently. | |
| const left = this._acceptLeftExpression(expr, context); | |
| const binding = this._acceptBindings(expr, context); | |
| const statement = `${left} ${context.operator} ${binding}`; | |
| const valueName = expr.right.accept(this, { | |
| ...context, | |
| }); | |
| const parameters = this._acceptParameters(expr, context); | |
| const isRequired = context.required === true; | |
| const query = `${this.qbVar}.${context.combinator}Where('${statement}', ${parameters})`; | |
| const finalQuery = !isRequired | |
| ? this._wrapInIfStatement( | |
| valueName === 'WILL_BE_USED_FROM_DESTRUCTURED_INPUT' | |
| ? left | |
| : valueName, | |
| query | |
| ) | |
| : query; | |
| return finalQuery; | |
| } | |
| public visitNumericLiteralExpr( | |
| expr: QueryBuilder.Literal<'numeric'> | |
| ): string { | |
| return `${expr.value}`; | |
| } | |
| public visitNullLiteralExpr(expr: QueryBuilder.Literal<'null'>): null { | |
| return null; | |
| } | |
| public visitBooleanLiteralExpr( | |
| expr: QueryBuilder.Literal<'boolean'> | |
| ): string { | |
| return `${expr.value}`; | |
| } | |
| public visitStringLiteralExpr(expr: QueryBuilder.Literal<'string'>): string { | |
| return `'${expr.value}'`; | |
| } | |
| public visitIdentifier( | |
| expr: QueryBuilder.Identifier, | |
| context: { | |
| format?: (value: string) => string; | |
| } | |
| ): string { | |
| if (context.format) { | |
| return context.format(expr.value); | |
| } | |
| return expr.value; | |
| } | |
| public visitCombinator(expr: QueryBuilder.Combinator, context: any): string { | |
| return ''; | |
| } | |
| visitListExpr(expr: QueryBuilder.ListExpr, context: any) { | |
| const children: string[] = expr.value.map((childExpr) => | |
| childExpr.accept(this, context) | |
| ); | |
| return `[${children.join(',')}]`; | |
| } | |
| public visitQuerySelectExpr( | |
| expr: QueryBuilder.QuerySelectExpr, | |
| context: any | |
| ): any { | |
| const { columns, joinsFields } = context; | |
| const addSelectAndJoins = | |
| columns && columns.length | |
| ? `${this._addSelect(columns)}${this._addJoins(joinsFields)}` | |
| : ''; | |
| const query = expr.value.map((group) => group.accept(this)).join(';'); | |
| return `${addSelectAndJoins}${query}`; | |
| } | |
| public visitGroupExpr(expr: QueryBuilder.GroupExpr, context: any): any { | |
| const qb = expr.value.map((childExpr) => { | |
| return childExpr.accept(this, { | |
| ...context, | |
| combinator: expr.combinator.operator, | |
| }); | |
| }); | |
| if (qb.length > 1) { | |
| return `${this.qbVar}.${ | |
| expr.combinator.operator | |
| }Where(${this._wrapBrackets(qb.join(';'))})`; | |
| } | |
| return qb.join(';'); | |
| } | |
| public execute(): string { | |
| const result = this._rootExpr.accept(this); | |
| return result; | |
| } | |
| public executeWithQueryBuiler() { | |
| const queryBuilder = `const ${this.qbVar} = createQueryBuilder(${pascalcase( | |
| this.tableName | |
| )},'${this.tableName}')`; | |
| const result = this._rootExpr.accept(this); | |
| return `${queryBuilder};${result}`; | |
| } | |
| } | |
| export function runTypeormVisitor(props: { | |
| query: GenericQueryCondition<unknown>; | |
| tableName: string; | |
| qbVarName?: string; | |
| }) { | |
| return new TypeOrmVisitor( | |
| toAst(props.query) as QueryBuilder.QuerySelectExpr, | |
| pascalcase(props.tableName), | |
| props.qbVarName | |
| ).execute(); | |
| } | |
| import { Contracts, IncomingActionProperty } from '@faslh/compiler/contracts'; | |
| import { camelcase } from '@faslh/utils'; | |
| const naminize = (input: string[]) => camelcase(input.join(' ')); | |
| function format( | |
| it: Omit<Contracts.DefaultQueryConditionContract, 'operator'> | |
| ): Record<string, IncomingActionProperty> { | |
| return { | |
| [naminize(it.input)]: { | |
| input: it.data.input as string, | |
| defaultValue: it.data.defaultValue, | |
| validations: it.data.validation, | |
| }, | |
| }; | |
| } | |
| function processSelect( | |
| select: Contracts.QuerySelectConditionContract | |
| ): Record<string, IncomingActionProperty> { | |
| return select.data | |
| .map(processGroup) | |
| .flat() | |
| .reduce<Record<string, IncomingActionProperty>>((acc, item) => { | |
| return { | |
| ...acc, | |
| ...item, | |
| }; | |
| }, {}); | |
| } | |
| function processGroup( | |
| group: Contracts.GroupQueryConditionContract | |
| ): Record<string, IncomingActionProperty> { | |
| return group.data | |
| .map((item) => flatQueryConditions(item)) | |
| .flat() | |
| .reduce<Record<string, IncomingActionProperty>>((acc, item) => { | |
| return { | |
| ...acc, | |
| ...item, | |
| }; | |
| }, {}); | |
| } | |
| function processBetween( | |
| group: Contracts.BetweenQueryConditionContract | |
| ): Record<string, IncomingActionProperty> { | |
| return { | |
| ...format({ | |
| input: group.input, | |
| data: group.data.min, | |
| }), | |
| ...format({ | |
| input: group.input, | |
| data: group.data.max, | |
| }), | |
| }; | |
| } | |
| function processDefault( | |
| group: Contracts.DefaultQueryConditionContract | |
| ): Record<string, IncomingActionProperty> { | |
| return format({ | |
| input: group.input, | |
| data: group.data, | |
| }); | |
| } | |
| export function flatQueryConditions( | |
| condition: Contracts.QueryConditionContract | |
| ): Record<string, IncomingActionProperty> { | |
| switch (condition.operator) { | |
| case 'group': | |
| return processGroup(condition); | |
| case 'between': | |
| return processBetween(condition); | |
| case 'querySelect': | |
| return processSelect(condition); | |
| default: | |
| return processDefault(condition); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment