feat(nestjs): migrate to NestJS

This commit is contained in:
Ahmed Bouhuolia
2025-04-07 11:51:24 +02:00
parent f068218a16
commit 55fcc908ef
3779 changed files with 631 additions and 195332 deletions

View File

@@ -0,0 +1,101 @@
import { get } from 'lodash';
import { ACCOUNT_TYPES } from '@/constants/accounts';
export class AccountTypesUtils {
/**
* Retrieve account types list.
*/
static getList() {
return ACCOUNT_TYPES;
}
/**
* Retrieve accounts types by the given root type.
* @param {string} rootType -
* @return {string}
*/
static getTypesByRootType(rootType: string) {
return ACCOUNT_TYPES.filter((type) => type.rootType === rootType);
}
/**
* Retrieve account type by the given account type key.
* @param {string} key
* @param {string} accessor
*/
static getType(key: string, accessor?: string) {
const type = ACCOUNT_TYPES.find((type) => type.key === key);
if (accessor) {
return get(type, accessor);
}
return type;
}
/**
* Retrieve accounts types by the parent account type.
* @param {string} parentType
*/
static getTypesByParentType(parentType: string) {
return ACCOUNT_TYPES.filter((type) => type.parentType === parentType);
}
/**
* Retrieve accounts types by the given account normal.
* @param {string} normal
*/
static getTypesByNormal(normal: string) {
return ACCOUNT_TYPES.filter((type) => type.normal === normal);
}
/**
* Detarmines whether the root type equals the account type.
* @param {string} key
* @param {string} rootType
*/
static isRootTypeEqualsKey(key: string, rootType: string): boolean {
return ACCOUNT_TYPES.some((type) => {
const isType = type.key === key;
const isRootType = type.rootType === rootType;
return isType && isRootType;
});
}
/**
* Detarmines whether the parent account type equals the account type key.
* @param {string} key - Account type key.
* @param {string} parentType - Account parent type.
*/
static isParentTypeEqualsKey(key: string, parentType: string): boolean {
return ACCOUNT_TYPES.some((type) => {
const isType = type.key === key;
const isParentType = type.parentType === parentType;
return isType && isParentType;
});
}
/**
* Detarmines whether account type has balance sheet.
* @param {string} key - Account type key.
*
*/
static isTypeBalanceSheet(key: string): boolean {
return ACCOUNT_TYPES.some((type) => {
const isType = type.key === key;
return isType && type.balanceSheet;
});
}
/**
* Detarmines whether account type has profit/loss sheet.
* @param {string} key - Account type key.
*/
static isTypePLSheet(key: string): boolean {
return ACCOUNT_TYPES.some((type) => {
const isType = type.key === key;
return isType && type.incomeSheet;
});
}
}

View File

@@ -0,0 +1,26 @@
import { ChromiumRoute, LibreOfficeRoute, PdfEngineRoute } from './_types';
export class Chromiumly {
public static readonly GOTENBERG_ENDPOINT = process.env.GOTENBERG_URL || '';
public static readonly CHROMIUM_PATH = 'forms/chromium/convert';
public static readonly PDF_ENGINES_PATH = 'forms/pdfengines';
public static readonly LIBRE_OFFICE_PATH = 'forms/libreoffice';
public static readonly GOTENBERG_DOCS_ENDPOINT =
process.env.GOTENBERG_DOCS_URL || '';
public static readonly CHROMIUM_ROUTES = {
url: ChromiumRoute.URL,
html: ChromiumRoute.HTML,
markdown: ChromiumRoute.MARKDOWN,
};
public static readonly PDF_ENGINE_ROUTES = {
merge: PdfEngineRoute.MERGE,
};
public static readonly LIBRE_OFFICE_ROUTES = {
convert: LibreOfficeRoute.CONVERT,
};
}

View File

@@ -0,0 +1,66 @@
import FormData from 'form-data';
import { GotenbergUtils } from './GotenbergUtils';
import { PageProperties } from './_types';
export class ConverterUtils {
public static injectPageProperties(
data: FormData,
pageProperties: PageProperties
): void {
if (pageProperties.size) {
GotenbergUtils.assert(
pageProperties.size.width >= 1.0 && pageProperties.size.height >= 1.5,
'size is smaller than the minimum printing requirements (i.e. 1.0 x 1.5 in)'
);
data.append('paperWidth', pageProperties.size.width);
data.append('paperHeight', pageProperties.size.height);
}
if (pageProperties.margins) {
GotenbergUtils.assert(
pageProperties.margins.top >= 0 &&
pageProperties.margins.bottom >= 0 &&
pageProperties.margins.left >= 0 &&
pageProperties.margins.left >= 0,
'negative margins are not allowed'
);
data.append('marginTop', pageProperties.margins.top);
data.append('marginBottom', pageProperties.margins.bottom);
data.append('marginLeft', pageProperties.margins.left);
data.append('marginRight', pageProperties.margins.right);
}
if (pageProperties.preferCssPageSize) {
data.append(
'preferCssPageSize',
String(pageProperties.preferCssPageSize)
);
}
if (pageProperties.printBackground) {
data.append('printBackground', String(pageProperties.printBackground));
}
if (pageProperties.landscape) {
data.append('landscape', String(pageProperties.landscape));
}
if (pageProperties.scale) {
GotenbergUtils.assert(
pageProperties.scale >= 0.1 && pageProperties.scale <= 2.0,
'scale is outside of [0.1 - 2] range'
);
data.append('scale', pageProperties.scale);
}
if (pageProperties.nativePageRanges) {
GotenbergUtils.assert(
pageProperties.nativePageRanges.from > 0 &&
pageProperties.nativePageRanges.to > 0 &&
pageProperties.nativePageRanges.to >=
pageProperties.nativePageRanges.from,
'page ranges syntax error'
);
data.append(
'nativePageRanges',
`${pageProperties.nativePageRanges.from}-${pageProperties.nativePageRanges.to}`
);
}
}
}

View File

@@ -0,0 +1,10 @@
import { Chromiumly } from './Chromiumly';
import { ChromiumRoute } from './_types';
export abstract class Converter {
readonly endpoint: string;
constructor(route: ChromiumRoute) {
this.endpoint = `${Chromiumly.GOTENBERG_ENDPOINT}/${Chromiumly.CHROMIUM_PATH}/${Chromiumly.CHROMIUM_ROUTES[route]}`;
}
}

View File

@@ -0,0 +1,24 @@
import FormData from 'form-data';
import Axios from 'axios';
export class GotenbergUtils {
public static assert(condition: boolean, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
public static async fetch(endpoint: string, data: FormData): Promise<Buffer> {
try {
const response = await Axios.post(endpoint, data, {
headers: {
...data.getHeaders(),
},
responseType: 'arraybuffer', // This ensures you get a Buffer bac
});
return response.data;
} catch (error) {
console.error(error);
}
}
}

View File

@@ -0,0 +1,38 @@
import { constants, createReadStream, PathLike, promises } from 'fs';
import FormData from 'form-data';
import { GotenbergUtils } from './GotenbergUtils';
import { IConverter, PageProperties } from './_types';
import { PdfFormat, ChromiumRoute } from './_types';
import { ConverterUtils } from './ConvertUtils';
import { Converter } from './Converter';
export class HtmlConverter extends Converter implements IConverter {
constructor() {
super(ChromiumRoute.HTML);
}
async convert({
html,
properties,
pdfFormat,
}: {
html: PathLike;
properties?: PageProperties;
pdfFormat?: PdfFormat;
}): Promise<Buffer> {
try {
await promises.access(html, constants.R_OK);
const data = new FormData();
if (pdfFormat) {
data.append('pdfFormat', pdfFormat);
}
data.append('index.html', createReadStream(html));
if (properties) {
ConverterUtils.injectPageProperties(data, properties);
}
return GotenbergUtils.fetch(this.endpoint, data);
} catch (error) {
throw error;
}
}
}

View File

@@ -0,0 +1,38 @@
import FormData from 'form-data';
import { IConverter, PageProperties, PdfFormat, ChromiumRoute } from './_types';
import { ConverterUtils } from './ConvertUtils';
import { Converter } from './Converter';
import { GotenbergUtils } from './GotenbergUtils';
export class UrlConverter extends Converter implements IConverter {
constructor() {
super(ChromiumRoute.URL);
}
async convert({
url,
properties,
pdfFormat,
}: {
url: string;
properties?: PageProperties;
pdfFormat?: PdfFormat;
}): Promise<Buffer> {
try {
const _url = new URL(url);
const data = new FormData();
if (pdfFormat) {
data.append('pdfFormat', pdfFormat);
}
data.append('url', _url.href);
if (properties) {
ConverterUtils.injectPageProperties(data, properties);
}
return GotenbergUtils.fetch(this.endpoint, data);
} catch (error) {
throw error;
}
}
}

View File

@@ -0,0 +1,51 @@
import { PathLike } from 'fs';
export type PageSize = {
width: number; // Paper width, in inches (default 8.5)
height: number; //Paper height, in inches (default 11)
};
export type PageMargins = {
top: number; // Top margin, in inches (default 0.39)
bottom: number; // Bottom margin, in inches (default 0.39)
left: number; // Left margin, in inches (default 0.39)
right: number; // Right margin, in inches (default 0.39)
};
export type PageProperties = {
size?: PageSize;
margins?: PageMargins;
preferCssPageSize?: boolean; // Define whether to prefer page size as defined by CSS (default false)
printBackground?: boolean; // Print the background graphics (default false)
landscape?: boolean; // Set the paper orientation to landscape (default false)
scale?: number; // The scale of the page rendering (default 1.0)
nativePageRanges?: { from: number; to: number }; // Page ranges to print
};
export interface IConverter {
convert({
...args
}: {
[x: string]: string | PathLike | PageProperties | PdfFormat;
}): Promise<Buffer>;
}
export enum PdfFormat {
A_1a = 'PDF/A-1a',
A_2b = 'PDF/A-2b',
A_3b = 'PDF/A-3b',
}
export enum ChromiumRoute {
URL = 'url',
HTML = 'html',
MARKDOWN = 'markdown',
}
export enum PdfEngineRoute {
MERGE = 'merge',
}
export enum LibreOfficeRoute {
CONVERT = 'convert',
}

View File

@@ -0,0 +1,350 @@
// @ts-nocheck
/**
* A simple dependency graph
*/
/**
* Helper for creating a Topological Sort using Depth-First-Search on a set of edges.
*
* Detects cycles and throws an Error if one is detected (unless the "circular"
* parameter is "true" in which case it ignores them).
*
* @param edges The set of edges to DFS through
* @param leavesOnly Whether to only return "leaf" nodes (ones who have no edges)
* @param result An array in which the results will be populated
* @param circular A boolean to allow circular dependencies
*/
function createDFS(edges, leavesOnly, result, circular) {
var visited = {};
return function (start) {
if (visited[start]) {
return;
}
var inCurrentPath = {};
var currentPath = [];
var todo = []; // used as a stack
todo.push({ node: start, processed: false });
while (todo.length > 0) {
var current = todo[todo.length - 1]; // peek at the todo stack
var processed = current.processed;
var node = current.node;
if (!processed) {
// Haven't visited edges yet (visiting phase)
if (visited[node]) {
todo.pop();
continue;
} else if (inCurrentPath[node]) {
// It's not a DAG
if (circular) {
todo.pop();
// If we're tolerating cycles, don't revisit the node
continue;
}
currentPath.push(node);
throw new DepGraphCycleError(currentPath);
}
inCurrentPath[node] = true;
currentPath.push(node);
var nodeEdges = edges[node];
// (push edges onto the todo stack in reverse order to be order-compatible with the old DFS implementation)
for (var i = nodeEdges.length - 1; i >= 0; i--) {
todo.push({ node: nodeEdges[i], processed: false });
}
current.processed = true;
} else {
// Have visited edges (stack unrolling phase)
todo.pop();
currentPath.pop();
inCurrentPath[node] = false;
visited[node] = true;
if (!leavesOnly || edges[node].length === 0) {
result.push(node);
}
}
}
};
}
/**
* Simple Dependency Graph
*/
var DepGraph = (DepGraph = function DepGraph(opts) {
this.nodes = {}; // Node -> Node/Data (treated like a Set)
this.outgoingEdges = {}; // Node -> [Dependency Node]
this.incomingEdges = {}; // Node -> [Dependant Node]
this.circular = opts && !!opts.circular; // Allows circular deps
});
DepGraph.fromArray = (
items,
options = { itemId: 'id', parentItemId: 'parent_id' }
) => {
const depGraph = new DepGraph();
items.forEach((item) => {
depGraph.addNode(item[options.itemId], item);
});
items.forEach((item) => {
if (item[options.parentItemId]) {
depGraph.addDependency(item[options.parentItemId], item[options.itemId]);
}
});
return depGraph;
};
DepGraph.prototype = {
/**
* The number of nodes in the graph.
*/
size: function () {
return Object.keys(this.nodes).length;
},
/**
* Add a node to the dependency graph. If a node already exists, this method will do nothing.
*/
addNode: function (node, data) {
if (!this.hasNode(node)) {
// Checking the arguments length allows the user to add a node with undefined data
if (arguments.length === 2) {
this.nodes[node] = data;
} else {
this.nodes[node] = node;
}
this.outgoingEdges[node] = [];
this.incomingEdges[node] = [];
}
},
/**
* Remove a node from the dependency graph. If a node does not exist, this method will do nothing.
*/
removeNode: function (node) {
if (this.hasNode(node)) {
delete this.nodes[node];
delete this.outgoingEdges[node];
delete this.incomingEdges[node];
[this.incomingEdges, this.outgoingEdges].forEach(function (edgeList) {
Object.keys(edgeList).forEach(function (key) {
var idx = edgeList[key].indexOf(node);
if (idx >= 0) {
edgeList[key].splice(idx, 1);
}
}, this);
});
}
},
/**
* Check if a node exists in the graph
*/
hasNode: function (node) {
return this.nodes.hasOwnProperty(node);
},
/**
* Get the data associated with a node name
*/
getNodeData: function (node) {
if (this.hasNode(node)) {
return this.nodes[node];
} else {
throw new Error('Node does not exist: ' + node);
}
},
/**
* Set the associated data for a given node name. If the node does not exist, this method will throw an error
*/
setNodeData: function (node, data) {
if (this.hasNode(node)) {
this.nodes[node] = data;
} else {
throw new Error('Node does not exist: ' + node);
}
},
/**
* Add a dependency between two nodes. If either of the nodes does not exist,
* an Error will be thrown.
*/
addDependency: function (from, to) {
if (!this.hasNode(from)) {
throw new Error('Node does not exist: ' + from);
}
if (!this.hasNode(to)) {
throw new Error('Node does not exist: ' + to);
}
if (this.outgoingEdges[from].indexOf(to) === -1) {
this.outgoingEdges[from].push(to);
}
if (this.incomingEdges[to].indexOf(from) === -1) {
this.incomingEdges[to].push(from);
}
return true;
},
/**
* Remove a dependency between two nodes.
*/
removeDependency: function (from, to) {
var idx;
if (this.hasNode(from)) {
idx = this.outgoingEdges[from].indexOf(to);
if (idx >= 0) {
this.outgoingEdges[from].splice(idx, 1);
}
}
if (this.hasNode(to)) {
idx = this.incomingEdges[to].indexOf(from);
if (idx >= 0) {
this.incomingEdges[to].splice(idx, 1);
}
}
},
/**
* Return a clone of the dependency graph. If any custom data is attached
* to the nodes, it will only be shallow copied.
*/
clone: function () {
var source = this;
var result = new DepGraph();
var keys = Object.keys(source.nodes);
keys.forEach(function (n) {
result.nodes[n] = source.nodes[n];
result.outgoingEdges[n] = source.outgoingEdges[n].slice(0);
result.incomingEdges[n] = source.incomingEdges[n].slice(0);
});
return result;
},
/**
* Get an array containing the nodes that the specified node depends on (transitively).
*
* Throws an Error if the graph has a cycle, or the specified node does not exist.
*
* If `leavesOnly` is true, only nodes that do not depend on any other nodes will be returned
* in the array.
*/
dependenciesOf: function (node, leavesOnly) {
if (this.hasNode(node)) {
var result = [];
var DFS = createDFS(
this.outgoingEdges,
leavesOnly,
result,
this.circular
);
DFS(node);
var idx = result.indexOf(node);
if (idx >= 0) {
result.splice(idx, 1);
}
return result;
} else {
throw new Error('Node does not exist: ' + node);
}
},
/**
* get an array containing the nodes that depend on the specified node (transitively).
*
* Throws an Error if the graph has a cycle, or the specified node does not exist.
*
* If `leavesOnly` is true, only nodes that do not have any dependants will be returned in the array.
*/
dependantsOf: function (node, leavesOnly) {
if (this.hasNode(node)) {
var result = [];
var DFS = createDFS(
this.incomingEdges,
leavesOnly,
result,
this.circular
);
DFS(node);
var idx = result.indexOf(node);
if (idx >= 0) {
result.splice(idx, 1);
}
return result;
} else {
throw new Error('Node does not exist: ' + node);
}
},
/**
* Construct the overall processing order for the dependency graph.
*
* Throws an Error if the graph has a cycle.
*
* If `leavesOnly` is true, only nodes that do not depend on any other nodes will be returned.
*/
overallOrder: function (leavesOnly) {
var self = this;
var result = [];
var keys = Object.keys(this.nodes);
if (keys.length === 0) {
return result; // Empty graph
} else {
if (!this.circular) {
// Look for cycles - we run the DFS starting at all the nodes in case there
// are several disconnected subgraphs inside this dependency graph.
var CycleDFS = createDFS(this.outgoingEdges, false, [], this.circular);
keys.forEach(function (n) {
CycleDFS(n);
});
}
var DFS = createDFS(
this.outgoingEdges,
leavesOnly,
result,
this.circular
);
// Find all potential starting points (nodes with nothing depending on them) an
// run a DFS starting at these points to get the order
keys
.filter(function (node) {
return self.incomingEdges[node].length === 0;
})
.forEach(function (n) {
DFS(n);
});
// If we're allowing cycles - we need to run the DFS against any remaining
// nodes that did not end up in the initial result (as they are part of a
// subgraph that does not have a clear starting point)
if (this.circular) {
keys
.filter(function (node) {
return result.indexOf(node) === -1;
})
.forEach(function (n) {
DFS(n);
});
}
return result;
}
},
mapNodes(mapper) {},
};
/**
* Cycle error, including the path of the cycle.
*/
var DepGraphCycleError = (exports.DepGraphCycleError = function (cyclePath) {
var message = 'Dependency Cycle Found: ' + cyclePath.join(' -> ');
var instance = new Error(message);
instance.cyclePath = cyclePath;
Object.setPrototypeOf(instance, Object.getPrototypeOf(this));
if (Error.captureStackTrace) {
Error.captureStackTrace(instance, DepGraphCycleError);
}
return instance;
});
DepGraphCycleError.prototype = Object.create(Error.prototype, {
constructor: {
value: Error,
enumerable: false,
writable: true,
configurable: true,
},
});
Object.setPrototypeOf(DepGraphCycleError, Error);
export default DepGraph;

View File

@@ -0,0 +1,176 @@
const OperationType = {
LOGIC: 'LOGIC',
STRING: 'STRING',
COMPARISON: 'COMPARISON',
MATH: 'MATH',
};
export class Lexer {
public currentIndex: number;
public input: string;
public tokenList: string[];
// operation table
static get optable() {
return {
'=': OperationType.LOGIC,
'&': OperationType.LOGIC,
'|': OperationType.LOGIC,
'?': OperationType.LOGIC,
':': OperationType.LOGIC,
'\'': OperationType.STRING,
'"': OperationType.STRING,
'!': OperationType.COMPARISON,
'>': OperationType.COMPARISON,
'<': OperationType.COMPARISON,
'(': OperationType.MATH,
')': OperationType.MATH,
'+': OperationType.MATH,
'-': OperationType.MATH,
'*': OperationType.MATH,
'/': OperationType.MATH,
'%': OperationType.MATH,
};
}
/**
* Constructor
* @param {*} expression -
*/
constructor(expression) {
this.currentIndex = 0;
this.input = expression;
this.tokenList = [];
}
getTokens() {
let tok;
do {
// read current token, so step should be -1
tok = this.pickNext(-1);
const pos = this.currentIndex;
switch (Lexer.optable[tok]) {
case OperationType.LOGIC:
// == && || ===
this.readLogicOpt(tok);
break;
case OperationType.STRING:
this.readString(tok);
break;
case OperationType.COMPARISON:
this.readCompare(tok);
break;
case OperationType.MATH:
this.receiveToken();
break;
default:
this.readValue(tok);
}
// if the pos not changed, this loop will go into a infinite loop, every step of while loop,
// we must move the pos forward
// so here we should throw error, for example `1 & 2`
if (pos === this.currentIndex && tok !== undefined) {
const err = new Error(`unkonw token ${tok} from input string ${this.input}`);
err.name = 'UnknowToken';
throw err;
}
} while (tok !== undefined)
return this.tokenList;
}
/**
* read next token, the index param can set next step, default go foward 1 step
*
* @param index next postion
*/
pickNext(index = 0) {
return this.input[index + this.currentIndex + 1];
}
/**
* Store token into result tokenList, and move the pos index
*
* @param index
*/
receiveToken(index = 1) {
const tok = this.input.slice(this.currentIndex, this.currentIndex + index).trim();
// skip empty string
if (tok) {
this.tokenList.push(tok);
}
this.currentIndex += index;
}
// ' or "
readString(tok) {
let next;
let index = 0;
do {
next = this.pickNext(index);
index += 1;
} while (next !== tok && next !== undefined);
this.receiveToken(index + 1);
}
// > or < or >= or <= or !==
// tok in (>, <, !)
readCompare(tok) {
if (this.pickNext() !== '=') {
this.receiveToken(1);
return;
}
// !==
if (tok === '!' && this.pickNext(1) === '=') {
this.receiveToken(3);
return;
}
this.receiveToken(2);
}
// === or ==
// && ||
readLogicOpt(tok) {
if (this.pickNext() === tok) {
// ===
if (tok === '=' && this.pickNext(1) === tok) {
return this.receiveToken(3);
}
// == && ||
return this.receiveToken(2);
}
// handle as &&
// a ? b : c is equal to a && b || c
if (tok === '?' || tok === ':') {
return this.receiveToken(1);
}
}
readValue(tok) {
if (!tok) {
return;
}
let index = 0;
while (!Lexer.optable[tok] && tok !== undefined) {
tok = this.pickNext(index);
index += 1;
}
this.receiveToken(index);
}
}
export default function token(expression) {
const lexer = new Lexer(expression);
return lexer.getTokens();
}

View File

@@ -0,0 +1,162 @@
export const OPERATION = {
'!': 5,
'*': 4,
'/': 4,
'%': 4,
'+': 3,
'-': 3,
'>': 2,
'<': 2,
'>=': 2,
'<=': 2,
'===': 2,
'!==': 2,
'==': 2,
'!=': 2,
'&&': 1,
'||': 1,
'?': 1,
':': 1,
};
// export interface Node {
// left: Node | string | null;
// right: Node | string | null;
// operation: string;
// grouped?: boolean;
// };
export class Parser {
public index: number;
public blockLevel: number;
public token: string[];
constructor(token) {
this.index = -1;
this.blockLevel = 0;
this.token = token;
}
/**
*
* @return {Node | string} =-
*/
parse() {
let tok;
let root = {
left: null,
right: null,
operation: null,
};
do {
tok = this.parseStatement();
if (tok === null || tok === undefined) {
break;
}
if (root.left === null) {
root.left = tok;
root.operation = this.nextToken();
if (!root.operation) {
return tok;
}
root.right = this.parseStatement();
} else {
if (typeof tok !== 'string') {
throw new Error('operation must be string, but get ' + JSON.stringify(tok));
}
root = this.addNode(tok, this.parseStatement(), root);
}
} while (tok);
return root;
}
nextToken() {
this.index += 1;
return this.token[this.index];
}
prevToken() {
return this.token[this.index - 1];
}
/**
*
* @param {string} operation
* @param {Node|String|null} right
* @param {Node} root
*/
addNode(operation, right, root) {
let pre = root;
if (this.compare(pre.operation, operation) < 0 && !pre.grouped) {
while (pre.right !== null &&
typeof pre.right !== 'string' &&
this.compare(pre.right.operation, operation) < 0 && !pre.right.grouped) {
pre = pre.right;
}
pre.right = {
operation,
left: pre.right,
right,
};
return root;
}
return {
left: pre,
right,
operation,
}
}
/**
*
* @param {String} a
* @param {String} b
*/
compare(a, b) {
if (!OPERATION.hasOwnProperty(a) || !OPERATION.hasOwnProperty(b)) {
throw new Error(`unknow operation ${a} or ${b}`);
}
return OPERATION[a] - OPERATION[b];
}
/**
* @return string | Node | null
*/
parseStatement() {
const token = this.nextToken();
if (token === '(') {
this.blockLevel += 1;
const node = this.parse();
this.blockLevel -= 1;
if (typeof node !== 'string') {
node.grouped = true;
}
return node;
}
if (token === ')') {
return null;
}
if (token === '!') {
return { left: null, operation: token, right: this.parseStatement() }
}
// 3 > -12 or -12 + 10
if (token === '-' && (OPERATION[this.prevToken()] > 0 || this.prevToken() === undefined)) {
return { left: '0', operation: token, right: this.parseStatement(), grouped: true };
}
return token;
}
}

View File

@@ -0,0 +1,64 @@
import { OPERATION } from './Parser';
export class QueryParser {
public tree: any;
public queries: any;
public query: any;
constructor(tree, queries) {
this.tree = tree;
this.queries = queries;
this.query = null;
}
setQuery(query) {
this.query = query.clone();
}
parse() {
return this.parseNode(this.tree);
}
parseNode(node) {
if (typeof node === 'string') {
const nodeQuery = this.getQuery(node);
return (query) => { nodeQuery(query); };
}
if (OPERATION[node.operation] === undefined) {
throw new Error(`unknow expression ${node.operation}`);
}
const leftQuery = this.getQuery(node.left);
const rightQuery = this.getQuery(node.right);
switch (node.operation) {
case '&&':
case 'AND':
default:
return (nodeQuery) => nodeQuery.where((query) => {
query.where((q) => { leftQuery(q); });
query.andWhere((q) => { rightQuery(q); });
});
case '||':
case 'OR':
return (nodeQuery) => nodeQuery.where((query) => {
query.where((q) => { leftQuery(q); });
query.orWhere((q) => { rightQuery(q); });
});
}
}
getQuery(node) {
if (typeof node !== 'string' && node !== null) {
return this.parseNode(node);
}
const value = parseFloat(node);
if (!isNaN(value)) {
if (typeof this.queries[node] === 'undefined') {
throw new Error(`unknow query under index ${node}`);
}
return this.queries[node];
}
return null;
}
}

View File

@@ -0,0 +1,100 @@
import * as path from 'path';
import * as fs from 'fs';
import { sortBy } from 'lodash';
import { promisify } from 'util';
import { MigrateItem } from './interfaces';
import { importWebpackSeedModule } from './Utils';
import { DEFAULT_LOAD_EXTENSIONS } from './constants';
import { filterMigrations } from './MigrateUtils';
class FsMigrations {
private sortDirsSeparately: boolean;
private migrationsPaths: string[];
private loadExtensions: string[];
/**
* Constructor method.
* @param migrationDirectories
* @param sortDirsSeparately
* @param loadExtensions
*/
constructor(
migrationDirectories: string[],
sortDirsSeparately: boolean,
loadExtensions: string[],
) {
this.sortDirsSeparately = sortDirsSeparately;
if (!Array.isArray(migrationDirectories)) {
migrationDirectories = [migrationDirectories];
}
this.migrationsPaths = migrationDirectories;
this.loadExtensions = loadExtensions || DEFAULT_LOAD_EXTENSIONS;
}
/**
* Gets the migration names
* @returns Promise<MigrateItem[]>
*/
public getMigrations(loadExtensions = null): Promise<MigrateItem[]> {
// Get a list of files in all specified migration directories
const readMigrationsPromises = this.migrationsPaths.map((configDir) => {
const absoluteDir = path.resolve(process.cwd(), configDir);
const readdir = promisify(fs.readdir);
return readdir(absoluteDir).then((files) => ({
files,
configDir,
absoluteDir,
}));
});
return Promise.all(readMigrationsPromises).then((allMigrations) => {
const migrations = allMigrations.reduce((acc, migrationDirectory) => {
// When true, files inside the folder should be sorted
if (this.sortDirsSeparately) {
migrationDirectory.files = migrationDirectory.files.sort();
}
migrationDirectory.files.forEach((file) =>
acc.push({ file, directory: migrationDirectory.configDir }),
);
return acc;
}, []);
// If true we have already sorted the migrations inside the folders
// return the migrations fully qualified
if (this.sortDirsSeparately) {
return filterMigrations(
this,
migrations,
loadExtensions || this.loadExtensions,
);
}
return filterMigrations(
this,
sortBy(migrations, 'file'),
loadExtensions || this.loadExtensions,
);
});
}
/**
* Retrieve the file name from given migrate item.
* @param {MigrateItem} migration
* @returns {string}
*/
public getMigrationName(migration: MigrateItem): string {
return migration.file;
}
/**
* Retrieve the migrate file content from given migrate item.
* @param {MigrateItem} migration
* @returns {string}
*/
public getMigration(migration: MigrateItem): string {
return importWebpackSeedModule(migration.file.replace('.ts', ''));
}
}
export { DEFAULT_LOAD_EXTENSIONS, FsMigrations };

View File

@@ -0,0 +1,193 @@
// @ts-nocheck
import { differenceWith } from 'lodash';
import * as path from 'path';
import { FsMigrations } from './FsMigrations';
import {
getTable,
getTableName,
getLockTableName,
getLockTableNameWithSchema,
} from './TableUtils';
import { ISeederConfig, MigrateItem } from './interfaces';
/**
* Get schema-aware schema builder for a given schema nam
* @param trxOrKnex
* @param {string} schemaName
* @returns
*/
function getSchemaBuilder(trxOrKnex, schemaName: string | null = null) {
return schemaName
? trxOrKnex.schema.withSchema(schemaName)
: trxOrKnex.schema;
}
/**
* Creates migration table of the given table name.
* @param {string} tableName
* @param {string} schemaName
* @param trxOrKnex
* @returns
*/
function createMigrationTable(
tableName: string,
schemaName: string,
trxOrKnex,
) {
return getSchemaBuilder(trxOrKnex, schemaName).createTable(
getTableName(tableName),
(t) => {
t.increments();
t.string('name');
t.integer('batch');
t.timestamp('migration_time');
},
);
}
/**
* Creates a migration lock table of the given table name.
* @param {string} tableName
* @param {string} schemaName
* @param trxOrKnex
* @returns
*/
function createMigrationLockTable(
tableName: string,
schemaName: string,
trxOrKnex,
) {
return getSchemaBuilder(trxOrKnex, schemaName).createTable(tableName, (t) => {
t.increments('index').primary();
t.integer('is_locked');
});
}
/**
*
* @param tableName
* @param schemaName
* @param trxOrKnex
* @returns
*/
export function ensureMigrationTables(
tableName: string,
schemaName: string,
trxOrKnex,
) {
const lockTable = getLockTableName(tableName);
const lockTableWithSchema = getLockTableNameWithSchema(tableName, schemaName);
return getSchemaBuilder(trxOrKnex, schemaName)
.hasTable(tableName)
.then((exists) => {
return !exists && createMigrationTable(tableName, schemaName, trxOrKnex);
})
.then(() => {
return getSchemaBuilder(trxOrKnex, schemaName).hasTable(lockTable);
})
.then((exists) => {
return (
!exists && createMigrationLockTable(lockTable, schemaName, trxOrKnex)
);
})
.then(() => {
return getTable(trxOrKnex, lockTable, schemaName).select('*');
})
.then((data) => {
return (
!data.length &&
trxOrKnex.into(lockTableWithSchema).insert({ is_locked: 0 })
);
});
}
/**
* Lists all available migration versions, as a sorted array.
* @param migrationSource
* @param loadExtensions
* @returns
*/
function listAll(
migrationSource: FsMigrations,
loadExtensions,
): Promise<MigrateItem[]> {
return migrationSource.getMigrations(loadExtensions);
}
/**
* Lists all migrations that have been completed for the current db, as an array.
* @param {string} tableName
* @param {string} schemaName
* @param {} trxOrKnex
* @returns Promise<string[]>
*/
export async function listCompleted(
tableName: string,
schemaName: string,
trxOrKnex,
): Promise<string[]> {
const completedMigrations = await trxOrKnex
.from(getTableName(tableName, schemaName))
.orderBy('id')
.select('name');
return completedMigrations.map((migration) => {
return migration.name;
});
}
/**
* Gets the migration list from the migration directory specified in config, as well as
* the list of completed migrations to check what should be run.
*/
export function listAllAndCompleted(config: ISeederConfig, trxOrKnex) {
return Promise.all([
listAll(config.migrationSource, config.loadExtensions),
listCompleted(config.tableName, config.schemaName, trxOrKnex),
]);
}
/**
*
* @param migrationSource
* @param all
* @param completed
* @returns
*/
export function getNewMigrations(
migrationSource: FsMigrations,
all: MigrateItem[],
completed: string[],
): MigrateItem[] {
return differenceWith(all, completed, (allMigration, completedMigration) => {
return (
completedMigration === migrationSource.getMigrationName(allMigration)
);
});
}
function startsWithNumber(str) {
return /^\d/.test(str);
}
/**
*
* @param {FsMigrations} migrationSource -
* @param {MigrateItem[]} migrations -
* @param {string[]} loadExtensions -
* @returns
*/
export function filterMigrations(
migrationSource: FsMigrations,
migrations: MigrateItem[],
loadExtensions: string[],
) {
return migrations.filter((migration) => {
const migrationName = migrationSource.getMigrationName(migration);
const extension = path.extname(migrationName);
return (
loadExtensions.includes(extension) && startsWithNumber(migrationName)
);
});
}

View File

@@ -0,0 +1,223 @@
// @ts-nocheck
import { Knex } from 'knex';
import * as Bluebird from 'bluebird';
import { getTable, getTableName, getLockTableName } from './TableUtils';
import getMergedConfig from './SeederConfig';
import {
listAllAndCompleted,
getNewMigrations,
listCompleted,
ensureMigrationTables,
} from './MigrateUtils';
import { MigrateItem, SeedMigrationContext, ISeederConfig } from './interfaces';
import { FsMigrations } from './FsMigrations';
export class SeedMigration {
knex: Knex;
config: ISeederConfig;
migrationSource: FsMigrations;
context: SeedMigrationContext;
/**
* Constructor method.
* @param {Knex} knex - Knex instance.
* @param {SeedMigrationContext} context -
*/
constructor(knex: Knex, context: SeedMigrationContext) {
this.knex = knex;
this.config = getMergedConfig(this.knex.client.config.seeds, undefined);
this.migrationSource = this.config.migrationSource;
this.context = context;
}
/**
* Latest migration.
* @returns {Promise<void>}
*/
async latest(config = null): Promise<void> {
// Merges the configuration.
this.config = getMergedConfig(config, this.config);
// Ensure migration tables.
await ensureMigrationTables(this.config.tableName, null, this.knex);
// Retrieve all and completed migrations.
const [all, completed] = await listAllAndCompleted(this.config, this.knex);
// Retrieve the new migrations.
const migrations = getNewMigrations(this.migrationSource, all, completed);
// Run the latest migration on one batch.
return this.knex.transaction((trx: Knex.Transaction) => {
return this.runBatch(migrations, 'up', trx);
});
}
/**
* Add migration lock flag.
* @param {Knex.Transaction} trx
* @returns
*/
private migrateLockTable(trx: Knex.Transaction) {
const tableName = getLockTableName(this.config.tableName);
return getTable(this.knex, tableName, this.config.schemaName)
.transacting(trx)
.where('is_locked', '=', 0)
.update({ is_locked: 1 })
.then((rowCount) => {
if (rowCount != 1) {
throw new Error('Migration table is already locked');
}
});
}
/**
* Add migration lock flag.
* @param {Knex.Transaction} trx
* @returns
*/
private migrationLock(trx: Knex.Transaction) {
return this.migrateLockTable(trx);
}
/**
* Free the migration lock flag.
* @param {Knex.Transaction} trx
* @returns
*/
private freeLock(trx = this.knex): Promise<void> {
const tableName = getLockTableName(this.config.tableName);
return getTable(trx, tableName, this.config.schemaName).update({
is_locked: 0,
});
}
/**
* Returns the latest batch number.
* @param trx
* @returns
*/
private latestBatchNumber(trx = this.knex): number {
return trx
.from(getTableName(this.config.tableName, this.config.schemaName))
.max('batch as max_batch')
.then((obj) => obj[0].max_batch || 0);
}
/**
* Runs a batch of `migrations` in a specified `direction`, saving the
* appropriate database information as the migrations are run.
* @param {number} batchNo
* @param {MigrateItem[]} migrations
* @param {string} direction
* @param {Knex.Transaction} trx
* @returns {Promise<void>}
*/
private waterfallBatch(
batchNo: number,
migrations: MigrateItem[],
direction: string,
trx: Knex.Transaction,
): Promise<void> {
const { tableName } = this.config;
return Bluebird.each(migrations, (migration) => {
const name = this.migrationSource.getMigrationName(migration);
return this.migrationSource
.getMigration(migration)
.then((migrationContent) =>
this.runMigrationContent(migrationContent.default, direction, trx),
)
.then(() => {
if (direction === 'up') {
return trx.into(getTableName(tableName)).insert({
name,
batch: batchNo,
migration_time: new Date(),
});
}
if (direction === 'down') {
return trx.from(getTableName(tableName)).where({ name }).del();
}
});
});
}
/**
* Runs and builds the given migration class.
*/
private runMigrationContent(Migration, direction, trx) {
const instance = new Migration(trx);
if (this.context.i18n) {
instance.setI18n(this.context.i18n);
}
instance.setTenant(this.context.tenant);
return instance[direction](trx);
}
/**
* Validates some migrations by requiring and checking for an `up` and `down`function.
* @param {MigrateItem} migration
* @returns {MigrateItem}
*/
async validateMigrationStructure(migration: MigrateItem): MigrateItem {
const migrationName = this.migrationSource.getMigrationName(migration);
// maybe promise
const migrationContent = await this.migrationSource.getMigration(migration);
if (
typeof migrationContent.up !== 'function' ||
typeof migrationContent.down !== 'function'
) {
throw new Error(
`Invalid migration: ${migrationName} must have both an up and down function`,
);
}
return migration;
}
/**
* Run a batch of current migrations, in sequence.
* @param {MigrateItem[]} migrations
* @param {string} direction
* @param {Knex.Transaction} trx
* @returns {Promise<void>}
*/
private async runBatch(
migrations: MigrateItem[],
direction: string,
trx: Knex.Transaction,
): Promise<void> {
// Adds flag to migration lock.
await this.migrationLock(trx);
// When there is a wrapping transaction, some migrations
// could have been done while waiting for the lock:
const completed = await listCompleted(
this.config.tableName,
this.config.schemaName,
trx,
);
// Differentiate between all and completed to get new migrations.
const newMigrations = getNewMigrations(
this.config.migrationSource,
migrations,
completed,
);
// Retrieve the latest batch number.
const batchNo = await this.latestBatchNumber(trx);
// Increment the next batch number.
const newBatchNo = direction === 'up' ? batchNo + 1 : batchNo;
// Run all migration files in waterfall.
await this.waterfallBatch(newBatchNo, newMigrations, direction, trx);
// Free the migration lock flag.
await this.freeLock(trx);
}
}

View File

@@ -0,0 +1,11 @@
export class Seeder {
knex: any;
constructor(knex) {
this.knex = knex;
}
up(knex) {}
down(knex) {}
}

View File

@@ -0,0 +1,44 @@
import { DEFAULT_LOAD_EXTENSIONS, FsMigrations } from './FsMigrations';
const CONFIG_DEFAULT = Object.freeze({
extension: 'js',
loadExtensions: DEFAULT_LOAD_EXTENSIONS,
tableName: 'bigcapital_seeds',
schemaName: null,
directory: './migrations',
disableTransactions: false,
disableMigrationsListValidation: false,
sortDirsSeparately: false,
});
export default function getMergedConfig(config, currentConfig) {
// config is the user specified config, mergedConfig has defaults and current config
// applied to it.
const mergedConfig = {
...CONFIG_DEFAULT,
...(currentConfig || {}),
...config,
};
if (
config &&
// If user specifies any FS related config,
// clear specified migrationSource to avoid ambiguity
(config.directory ||
config.sortDirsSeparately !== undefined ||
config.loadExtensions)
) {
mergedConfig.migrationSource = null;
}
// If the user has not specified any configs, we need to
// default to fs migrations to maintain compatibility
if (!mergedConfig.migrationSource) {
mergedConfig.migrationSource = new FsMigrations(
mergedConfig.directory,
mergedConfig.sortDirsSeparately,
mergedConfig.loadExtensions
);
}
return mergedConfig;
}

View File

@@ -0,0 +1,43 @@
/**
* Get schema-aware query builder for a given table and schema name.
* @param {Knex} trxOrKnex -
* @param {string} tableName -
* @param {string} schemaName -
* @returns {string}
*/
export function getTable(trx, tableName: string, schemaName = null) {
return schemaName ? trx(tableName).withSchema(schemaName) : trx(tableName);
}
/**
* Get schema-aware table name.
* @param {string} tableName -
* @returns {string}
*/
export function getTableName(tableName: string, schemaName = null): string {
return schemaName ? `${schemaName}.${tableName}` : tableName;
}
/**
* Retrieve the lock table name from given migration table name.
* @param {string} tableName
* @returns {string}
*/
export function getLockTableName(tableName: string): string {
return `${tableName}_lock`;
}
/**
* Retireve the lock table name from ginve migration table name with schema.
* @param {string} tableName
* @param {string} schemaName
* @returns {string}
*/
export function getLockTableNameWithSchema(
tableName: string,
schemaName = null
): string {
return schemaName
? `${schemaName} + ${getLockTableName(tableName)}`
: getLockTableName(tableName);
}

View File

@@ -0,0 +1,27 @@
// @ts-nocheck
import { I18nService } from 'nestjs-i18n';
import { Seeder } from './Seeder';
export class TenantSeeder extends Seeder {
public knex: any;
public i18n: I18nService;
public models: any;
public tenant: any;
constructor(knex) {
super(knex);
this.knex = knex;
}
setI18n(i18n) {
this.i18n = i18n;
}
setModels(models) {
this.models = models;
}
setTenant(tenant) {
this.tenant = tenant;
}
}

View File

@@ -0,0 +1,42 @@
// @ts-nocheck
import * as fs from 'fs';
/**
* Detarmines the module type of the given file path.
* @param {string} filepath
* @returns {boolean}
*/
async function isModuleType(filepath: string): boolean {
if (process.env.npm_package_json) {
const { promisify } = require('util');
const readFile = promisify(fs.readFile);
// npm >= 7.0.0
const packageJson = JSON.parse(
await readFile(process.env.npm_package_json, 'utf-8'),
);
if (packageJson.type === 'module') {
return true;
}
}
return process.env.npm_package_type === 'module' || filepath.endsWith('.mjs');
}
/**
* Imports content of the given file path.
* @param {string} filepath
* @returns
*/
export async function importFile(filepath: string): any {
return (await isModuleType(filepath))
? import(require('url').pathToFileURL(filepath))
: require(filepath);
}
/**
*
* @param {string} moduleName
* @returns
*/
export async function importWebpackSeedModule(moduleName: string): any {
return import(`../../database/seeds/core/${moduleName}`);
}

View File

@@ -0,0 +1,12 @@
// Default load extensions.
export const DEFAULT_LOAD_EXTENSIONS = [
'.co',
'.coffee',
'.eg',
'.iced',
'.js',
'.cjs',
'.litcoffee',
'.ls',
'.ts',
];

View File

@@ -0,0 +1,20 @@
import { TenantModel } from '@/modules/System/models/TenantModel';
export interface FsMigrations {}
export interface ISeederConfig {
tableName: string;
migrationSource: FsMigrations;
schemaName?: string;
loadExtensions: string[];
}
export interface MigrateItem {
file: string;
directory: string;
}
export interface SeedMigrationContext {
i18n: any;
tenant: TenantModel;
}