mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
feat(nestjs): migrate to NestJS
This commit is contained in:
101
packages/server/src/libs/accounts-utils/AccountTypesUtils.ts
Normal file
101
packages/server/src/libs/accounts-utils/AccountTypesUtils.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
26
packages/server/src/libs/chromiumly/Chromiumly.ts
Normal file
26
packages/server/src/libs/chromiumly/Chromiumly.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
66
packages/server/src/libs/chromiumly/ConvertUtils.ts
Normal file
66
packages/server/src/libs/chromiumly/ConvertUtils.ts
Normal 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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
packages/server/src/libs/chromiumly/Converter.ts
Normal file
10
packages/server/src/libs/chromiumly/Converter.ts
Normal 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]}`;
|
||||
}
|
||||
}
|
||||
24
packages/server/src/libs/chromiumly/GotenbergUtils.ts
Normal file
24
packages/server/src/libs/chromiumly/GotenbergUtils.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
packages/server/src/libs/chromiumly/HTMLConvert.ts
Normal file
38
packages/server/src/libs/chromiumly/HTMLConvert.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
packages/server/src/libs/chromiumly/UrlConvert.ts
Normal file
38
packages/server/src/libs/chromiumly/UrlConvert.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
packages/server/src/libs/chromiumly/_types.ts
Normal file
51
packages/server/src/libs/chromiumly/_types.ts
Normal 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',
|
||||
}
|
||||
350
packages/server/src/libs/dependency-graph/index.ts
Normal file
350
packages/server/src/libs/dependency-graph/index.ts
Normal 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;
|
||||
176
packages/server/src/libs/logic-evaluation/Lexer.ts
Normal file
176
packages/server/src/libs/logic-evaluation/Lexer.ts
Normal 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();
|
||||
}
|
||||
162
packages/server/src/libs/logic-evaluation/Parser.ts
Normal file
162
packages/server/src/libs/logic-evaluation/Parser.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
64
packages/server/src/libs/logic-evaluation/QueryParser.ts
Normal file
64
packages/server/src/libs/logic-evaluation/QueryParser.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
100
packages/server/src/libs/migration-seed/FsMigrations.ts
Normal file
100
packages/server/src/libs/migration-seed/FsMigrations.ts
Normal 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 };
|
||||
193
packages/server/src/libs/migration-seed/MigrateUtils.ts
Normal file
193
packages/server/src/libs/migration-seed/MigrateUtils.ts
Normal 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)
|
||||
);
|
||||
});
|
||||
}
|
||||
223
packages/server/src/libs/migration-seed/SeedMigration.ts
Normal file
223
packages/server/src/libs/migration-seed/SeedMigration.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
11
packages/server/src/libs/migration-seed/Seeder.ts
Normal file
11
packages/server/src/libs/migration-seed/Seeder.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
export class Seeder {
|
||||
knex: any;
|
||||
|
||||
constructor(knex) {
|
||||
this.knex = knex;
|
||||
}
|
||||
up(knex) {}
|
||||
down(knex) {}
|
||||
}
|
||||
|
||||
44
packages/server/src/libs/migration-seed/SeederConfig.ts
Normal file
44
packages/server/src/libs/migration-seed/SeederConfig.ts
Normal 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;
|
||||
}
|
||||
43
packages/server/src/libs/migration-seed/TableUtils.ts
Normal file
43
packages/server/src/libs/migration-seed/TableUtils.ts
Normal 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);
|
||||
}
|
||||
27
packages/server/src/libs/migration-seed/TenantSeeder.ts
Normal file
27
packages/server/src/libs/migration-seed/TenantSeeder.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
42
packages/server/src/libs/migration-seed/Utils.ts
Normal file
42
packages/server/src/libs/migration-seed/Utils.ts
Normal 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}`);
|
||||
}
|
||||
12
packages/server/src/libs/migration-seed/constants.ts
Normal file
12
packages/server/src/libs/migration-seed/constants.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// Default load extensions.
|
||||
export const DEFAULT_LOAD_EXTENSIONS = [
|
||||
'.co',
|
||||
'.coffee',
|
||||
'.eg',
|
||||
'.iced',
|
||||
'.js',
|
||||
'.cjs',
|
||||
'.litcoffee',
|
||||
'.ls',
|
||||
'.ts',
|
||||
];
|
||||
20
packages/server/src/libs/migration-seed/interfaces.ts
Normal file
20
packages/server/src/libs/migration-seed/interfaces.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user