mirror of
https://github.com/codex-team/codex.docs.git
synced 2025-07-18 20:59:42 +02:00
🤩MongoDB support 🤩 (#272)
* implement configuration through YAML * remove rcparser * use password from appConfig * update docker configs * fix dockerignore * implement mongodb driver * update eslint packages * fix bugs * refactor code for grouping by parent * fix yet another bug * use unique symbol to the EntityId type * fix more bugs * implement db converter * fix bug with parent selector * fix eslint * db-converter refactoring * create cli program for db-converter * add readme and gitignore * update development docs * update development docs and default config * add docs about converter * add src/test to docker ignore * move database code from utils * improve docs * eslint fix * add more docs * fix docs * remove env_file from docker-compose * implement duplicate detection in db-converter * use published version of the config-loader * fix bug * Update DEVELOPMENT.md Co-authored-by: Ilya Maroz <37909603+ilyamore88@users.noreply.github.com> * fix bugs * fix next/prev buttons * fix more bugs * fix sorting Co-authored-by: Ilya Maroz <37909603+ilyamore88@users.noreply.github.com>
This commit is contained in:
parent
13762096c4
commit
55b4b3ee61
72 changed files with 12614 additions and 665 deletions
|
@ -9,7 +9,7 @@ import * as dotenv from 'dotenv';
|
|||
import HawkCatcher from '@hawk.so/nodejs';
|
||||
import os from 'os';
|
||||
import { downloadFavicon, FaviconData } from './utils/downloadFavicon.js';
|
||||
import appConfig from "./utils/appConfig.js";
|
||||
import appConfig from './utils/appConfig.js';
|
||||
|
||||
/**
|
||||
* The __dirname CommonJS variables are not available in ES modules.
|
||||
|
@ -28,7 +28,7 @@ if (appConfig.hawk?.backendToken) {
|
|||
}
|
||||
|
||||
// Get url to upload favicon from config
|
||||
const favicon = appConfig.favicon
|
||||
const favicon = appConfig.favicon;
|
||||
|
||||
app.locals.config = localConfig;
|
||||
// Set client error tracking token as app local.
|
||||
|
|
|
@ -4,6 +4,8 @@ import PagesOrder from './pagesOrder.js';
|
|||
import PageOrder from '../models/pageOrder.js';
|
||||
import HttpException from '../exceptions/httpException.js';
|
||||
import PagesFlatArray from '../models/pagesFlatArray.js';
|
||||
import { EntityId } from '../database/types.js';
|
||||
import { isEqualIds } from '../database/index.js';
|
||||
|
||||
type PageDataFields = keyof PageData;
|
||||
|
||||
|
@ -27,7 +29,7 @@ class Pages {
|
|||
* @param {string} id - page id
|
||||
* @returns {Promise<Page>}
|
||||
*/
|
||||
public static async get(id: string): Promise<Page> {
|
||||
public static async get(id: EntityId): Promise<Page> {
|
||||
const page = await Page.get(id);
|
||||
|
||||
if (!page._id) {
|
||||
|
@ -42,7 +44,7 @@ class Pages {
|
|||
*
|
||||
* @returns {Promise<Page[]>}
|
||||
*/
|
||||
public static async getAll(): Promise<Page[]> {
|
||||
public static async getAllPages(): Promise<Page[]> {
|
||||
return Page.getAll();
|
||||
}
|
||||
|
||||
|
@ -52,8 +54,8 @@ class Pages {
|
|||
* @param {string} parent - id of current page
|
||||
* @returns {Promise<Page[]>}
|
||||
*/
|
||||
public static async getAllExceptChildren(parent: string): Promise<Page[]> {
|
||||
const pagesAvailable = this.removeChildren(await Pages.getAll(), parent);
|
||||
public static async getAllExceptChildren(parent: EntityId): Promise<Page[]> {
|
||||
const pagesAvailable = this.removeChildren(await Pages.getAllPages(), parent);
|
||||
|
||||
const nullFilteredPages: Page[] = [];
|
||||
|
||||
|
@ -66,6 +68,24 @@ class Pages {
|
|||
return nullFilteredPages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get all pages as map
|
||||
*/
|
||||
private static async getPagesMap(): Promise<Map<string, Page>> {
|
||||
const pages = await Pages.getAllPages();
|
||||
const pagesMap = new Map<string, Page>();
|
||||
|
||||
pages.forEach(page => {
|
||||
if (page._id) {
|
||||
pagesMap.set(page._id.toString(), page);
|
||||
} else {
|
||||
throw new Error('Page id is not defined');
|
||||
}
|
||||
});
|
||||
|
||||
return pagesMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group all pages by their parents
|
||||
* If the pageId is passed, it excludes passed page from result pages
|
||||
|
@ -73,12 +93,9 @@ class Pages {
|
|||
* @param {string} pageId - pageId to exclude from result pages
|
||||
* @returns {Page[]}
|
||||
*/
|
||||
public static async groupByParent(pageId = ''): Promise<Page[]> {
|
||||
const result: Page[] = [];
|
||||
const orderGroupedByParent: Record<string, string[]> = {};
|
||||
const rootPageOrder = await PagesOrder.getRootPageOrder();
|
||||
const childPageOrder = await PagesOrder.getChildPageOrder();
|
||||
const orphanPageOrder: PageOrder[] = [];
|
||||
public static async groupByParent(pageId = '' as EntityId): Promise<Page[]> {
|
||||
const rootPageOrder = await PagesOrder.getRootPageOrder(); // get order of the root pages
|
||||
const childPageOrder = await PagesOrder.getChildPageOrder(); // get order of the all other pages
|
||||
|
||||
/**
|
||||
* If there is no root and child page order, then it returns an empty array
|
||||
|
@ -87,81 +104,35 @@ class Pages {
|
|||
return [];
|
||||
}
|
||||
|
||||
const pages = (await this.getAll()).reduce((map, _page) => {
|
||||
map.set(_page._id, _page);
|
||||
|
||||
return map;
|
||||
}, new Map);
|
||||
const pagesMap = await this.getPagesMap();
|
||||
const idsOfRootPages = rootPageOrder.order;
|
||||
|
||||
/**
|
||||
* It groups root pages and 1 level pages by its parent
|
||||
*/
|
||||
idsOfRootPages.reduce((prev, curr, idx) => {
|
||||
const childPages:PageOrder[] = [];
|
||||
const getChildrenOrder = (pageId: EntityId): EntityId[] => {
|
||||
const order = childPageOrder.find((order) => isEqualIds(order.page, pageId))?.order || [];
|
||||
|
||||
childPageOrder.forEach((pageOrder, _idx) => {
|
||||
if (pageOrder.page === curr) {
|
||||
childPages.push(pageOrder);
|
||||
childPageOrder.splice(_idx, 1);
|
||||
}
|
||||
});
|
||||
|
||||
const hasChildPage = childPages.length > 0;
|
||||
|
||||
prev[curr] = [];
|
||||
prev[curr].push(curr);
|
||||
|
||||
/**
|
||||
* It attaches 1 level page id to its parent page id
|
||||
*/
|
||||
if (hasChildPage) {
|
||||
prev[curr].push(...childPages[0].order);
|
||||
if (order.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const expandedOrder = order.map((id) => [id, ...getChildrenOrder(id)]);
|
||||
|
||||
/**
|
||||
* If non-attached childPages which is not 1 level page still remains,
|
||||
* It is stored as an orphan page so that it can be processed in the next statements
|
||||
*/
|
||||
if (idx === idsOfRootPages.length - 1 && childPageOrder.length > 0) {
|
||||
orphanPageOrder.push(...childPageOrder);
|
||||
}
|
||||
return expandedOrder.flat();
|
||||
};
|
||||
|
||||
return prev;
|
||||
}, orderGroupedByParent);
|
||||
const orderGroupedByParent = idsOfRootPages.reduce((acc, curr) => {
|
||||
const pageOrder = getChildrenOrder(curr);
|
||||
|
||||
let count = 0;
|
||||
acc[curr.toString()] = [curr, ...pageOrder];
|
||||
|
||||
/**
|
||||
* It groups remained ungrouped pages by its parent
|
||||
*/
|
||||
while (orphanPageOrder.length > 0) {
|
||||
if (count >= 1000) {
|
||||
throw new HttpException(500, `Page cannot be processed`);
|
||||
}
|
||||
|
||||
orphanPageOrder.forEach((orphanOrder, idx) => {
|
||||
// It loops each of grouped orders formatted as [root page id(1): corresponding child pages id(2)]
|
||||
Object.entries(orderGroupedByParent).forEach(([parentPageId, value]) => {
|
||||
// If (2) contains orphanOrder's parent id(page)
|
||||
if (orphanOrder.page && orphanOrder.order && value.includes(orphanOrder.page)) {
|
||||
// Append orphanOrder's id(order) into its parent id
|
||||
orderGroupedByParent[parentPageId].splice(value.indexOf(orphanOrder.page) + 1, 0, ...orphanOrder.order);
|
||||
// Finally, remove orphanOrder from orphanPageOrder
|
||||
orphanPageOrder.splice(idx, 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
count += 1;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, EntityId[]>);
|
||||
|
||||
/**
|
||||
* It converts grouped pages(object) to array
|
||||
*/
|
||||
Object.values(orderGroupedByParent).flatMap(arr => [ ...arr ])
|
||||
.forEach(arr => {
|
||||
result.push(pages.get(arr));
|
||||
const result = Object.values(orderGroupedByParent)
|
||||
.flatMap(ids => [ ...ids ])
|
||||
.map(id => {
|
||||
return pagesMap.get(id.toString()) as Page;
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -188,9 +159,9 @@ class Pages {
|
|||
* @param {string} parent - id of parent page
|
||||
* @returns {Array<?Page>}
|
||||
*/
|
||||
public static removeChildren(pagesAvailable: Array<Page | null>, parent: string | undefined): Array<Page | null> {
|
||||
public static removeChildren(pagesAvailable: Array<Page | null>, parent: EntityId | undefined): Array<Page | null> {
|
||||
pagesAvailable.forEach(async (item, index) => {
|
||||
if (item === null || item._parent !== parent) {
|
||||
if (item === null || !isEqualIds(item._parent, parent)) {
|
||||
return;
|
||||
}
|
||||
pagesAvailable[index] = null;
|
||||
|
@ -238,7 +209,7 @@ class Pages {
|
|||
* @param {PageData} data - info about page
|
||||
* @returns {Promise<Page>}
|
||||
*/
|
||||
public static async update(id: string, data: PageData): Promise<Page> {
|
||||
public static async update(id: EntityId, data: PageData): Promise<Page> {
|
||||
const page = await Page.get(id);
|
||||
const previousUri = page.uri;
|
||||
|
||||
|
@ -278,7 +249,7 @@ class Pages {
|
|||
* @param {string} id - page id
|
||||
* @returns {Promise<Page>}
|
||||
*/
|
||||
public static async remove(id: string): Promise<Page> {
|
||||
public static async remove(id: EntityId): Promise<Page> {
|
||||
const page = await Page.get(id);
|
||||
|
||||
if (!page._id) {
|
||||
|
@ -291,6 +262,7 @@ class Pages {
|
|||
await alias.destroy();
|
||||
}
|
||||
const removedPage = page.destroy();
|
||||
|
||||
await PagesFlatArray.regenerate();
|
||||
|
||||
return removedPage;
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import PageOrder from '../models/pageOrder.js';
|
||||
import Page from '../models/page.js';
|
||||
import PagesFlatArray from '../models/pagesFlatArray.js';
|
||||
import { EntityId } from '../database/types.js';
|
||||
import { isEqualIds, toEntityId } from '../database/index.js';
|
||||
|
||||
/**
|
||||
* @class PagesOrder
|
||||
|
@ -15,7 +17,7 @@ class PagesOrder {
|
|||
* @param {string} parentId - of which page we want to get children order
|
||||
* @returns {Promise<PageOrder>}
|
||||
*/
|
||||
public static async get(parentId: string): Promise<PageOrder> {
|
||||
public static async get(parentId: EntityId): Promise<PageOrder> {
|
||||
const order = await PageOrder.get(parentId);
|
||||
|
||||
if (!order._id) {
|
||||
|
@ -58,7 +60,7 @@ class PagesOrder {
|
|||
* @param {string} parentId - parent page's id
|
||||
* @param {string} childId - new page pushed to the order
|
||||
*/
|
||||
public static async push(parentId: string, childId: string): Promise<void> {
|
||||
public static async push(parentId: EntityId, childId: EntityId): Promise<void> {
|
||||
const order = await PageOrder.get(parentId);
|
||||
|
||||
order.push(childId);
|
||||
|
@ -73,7 +75,7 @@ class PagesOrder {
|
|||
* @param {string} newParentId - new parent page's id
|
||||
* @param {string} targetPageId - page's id which is changing the parent page
|
||||
*/
|
||||
public static async move(oldParentId: string, newParentId: string, targetPageId: string): Promise<void> {
|
||||
public static async move(oldParentId: EntityId, newParentId: EntityId, targetPageId: EntityId): Promise<void> {
|
||||
const oldParentOrder = await PageOrder.get(oldParentId);
|
||||
|
||||
oldParentOrder.remove(targetPageId);
|
||||
|
@ -96,18 +98,20 @@ class PagesOrder {
|
|||
* @param {boolean} ignoreSelf - should we ignore current page in list or not
|
||||
* @returns {Page[]}
|
||||
*/
|
||||
public static async getOrderedChildren(pages: Page[], currentPageId: string, parentPageId: string, ignoreSelf = false): Promise<Page[]> {
|
||||
public static async getOrderedChildren(pages: Page[], currentPageId: EntityId, parentPageId: EntityId, ignoreSelf = false): Promise<Page[]> {
|
||||
const children = await PageOrder.get(parentPageId);
|
||||
const unordered = pages.filter(page => page._parent === parentPageId).map(page => page._id);
|
||||
const unordered = pages.filter(page => isEqualIds(page._parent, parentPageId)).map(page => page._id);
|
||||
|
||||
// Create unique array with ordered and unordered pages id
|
||||
const ordered = Array.from(new Set([...children.order, ...unordered]));
|
||||
const ordered = Array.from(new Set([...children.order, ...unordered].map(id => id?.toString())));
|
||||
|
||||
const result: Page[] = [];
|
||||
|
||||
ordered.forEach(pageId => {
|
||||
const id = pageId ? toEntityId(pageId): undefined;
|
||||
|
||||
pages.forEach(page => {
|
||||
if (page._id === pageId && (pageId !== currentPageId || !ignoreSelf)) {
|
||||
if (isEqualIds(page._id, id) && (!isEqualIds(id, currentPageId) || !ignoreSelf)) {
|
||||
result.push(page);
|
||||
}
|
||||
});
|
||||
|
@ -122,11 +126,13 @@ class PagesOrder {
|
|||
* @param {string} parentPageId - parent page's id that contains both two pages
|
||||
* @param {string} putAbovePageId - page's id above which we put the target page
|
||||
*/
|
||||
public static async update(unordered: string[], currentPageId: string, parentPageId: string, putAbovePageId: string): Promise<void> {
|
||||
public static async update(unordered: EntityId[], currentPageId: EntityId, parentPageId: EntityId, putAbovePageId: EntityId): Promise<void> {
|
||||
const pageOrder = await PageOrder.get(parentPageId);
|
||||
|
||||
// Create unique array with ordered and unordered pages id
|
||||
pageOrder.order = Array.from(new Set([...pageOrder.order, ...unordered]));
|
||||
pageOrder.order = Array
|
||||
.from(new Set([...pageOrder.order, ...unordered].map(id => id?.toString())))
|
||||
.map(toEntityId);
|
||||
pageOrder.putAbove(currentPageId, putAbovePageId);
|
||||
await pageOrder.save();
|
||||
await PagesFlatArray.regenerate();
|
||||
|
@ -136,7 +142,7 @@ class PagesOrder {
|
|||
* @param {string} parentId - identity of parent page
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public static async remove(parentId: string): Promise<void> {
|
||||
public static async remove(parentId: EntityId): Promise<void> {
|
||||
const order = await PageOrder.get(parentId);
|
||||
|
||||
if (!order._id) {
|
||||
|
|
|
@ -5,7 +5,7 @@ import nodePath from 'path';
|
|||
import File, { FileData } from '../models/file.js';
|
||||
import crypto from '../utils/crypto.js';
|
||||
import deepMerge from '../utils/objects.js';
|
||||
import appConfig from "../utils/appConfig.js";
|
||||
import appConfig from '../utils/appConfig.js';
|
||||
|
||||
const random16 = crypto.random16;
|
||||
|
||||
|
@ -29,7 +29,6 @@ class Transport {
|
|||
* @param {string} multerData.path - path to the uploaded file
|
||||
* @param {number} multerData.size - size of the uploaded file
|
||||
* @param {string} multerData.mimetype - MIME type of the uploaded file
|
||||
*
|
||||
* @param {object} map - object that represents how should fields of File object should be mapped to response
|
||||
* @returns {Promise<FileData>}
|
||||
*/
|
||||
|
@ -108,11 +107,10 @@ class Transport {
|
|||
*
|
||||
* @param {File} file - file object
|
||||
* @param {object} map - object that represents how should fields of File object should be mapped to response
|
||||
*
|
||||
*/
|
||||
public static composeResponse(file: File, map: Dict): Dict {
|
||||
const response: Dict = {};
|
||||
const { data } = file;
|
||||
const data = file.data as Record<string, string | number | undefined>;
|
||||
|
||||
Object.entries(map).forEach(([name, path]) => {
|
||||
const fields: string[] = path.split(':');
|
||||
|
|
50
src/backend/database/index.ts
Normal file
50
src/backend/database/index.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { AliasData } from '../models/alias.js';
|
||||
import { FileData } from '../models/file.js';
|
||||
import { PageData } from '../models/page.js';
|
||||
import { PageOrderData } from '../models/pageOrder.js';
|
||||
import appConfig from '../utils/appConfig.js';
|
||||
import LocalDatabaseDriver from './local.js';
|
||||
import MongoDatabaseDriver from './mongodb.js';
|
||||
import { EntityId } from './types.js';
|
||||
import { ObjectId } from 'mongodb';
|
||||
|
||||
const Database = appConfig.database.driver === 'mongodb' ? MongoDatabaseDriver : LocalDatabaseDriver;
|
||||
|
||||
/**
|
||||
* Convert a string to an EntityId (string or ObjectId depending on the database driver)
|
||||
*
|
||||
* @param id - id to convert
|
||||
*/
|
||||
export function toEntityId(id: string): EntityId {
|
||||
if (id === '0') {
|
||||
return id as EntityId;
|
||||
}
|
||||
|
||||
return (appConfig.database.driver === 'mongodb' ? new ObjectId(id) : id) as EntityId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if provided ids are equal
|
||||
*
|
||||
* @param id1 - first id
|
||||
* @param id2 - second id
|
||||
*/
|
||||
export function isEqualIds(id1?: EntityId, id2?: EntityId): boolean {
|
||||
return id1?.toString() === id2?.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if provided ids are valid
|
||||
*
|
||||
* @param id - id to check
|
||||
*/
|
||||
export function isEntityId(id?: EntityId): id is EntityId {
|
||||
return typeof id === 'string' || id instanceof ObjectId;
|
||||
}
|
||||
|
||||
export default {
|
||||
pages: new Database<PageData>('pages'),
|
||||
aliases: new Database<AliasData>('aliases'),
|
||||
pagesOrder: new Database<PageOrderData>('pagesOrder'),
|
||||
files: new Database<FileData>('files'),
|
||||
};
|
|
@ -1,55 +1,64 @@
|
|||
import Datastore from 'nedb';
|
||||
import { AliasData } from '../../models/alias.js';
|
||||
import { FileData } from '../../models/file.js';
|
||||
import { PageData } from '../../models/page.js';
|
||||
import { PageOrderData } from '../../models/pageOrder.js';
|
||||
import initDb from './initDb.js';
|
||||
import { DatabaseDriver, Options } from './types.js';
|
||||
import path from 'path';
|
||||
import appConfig from '../utils/appConfig.js';
|
||||
|
||||
/**
|
||||
* @typedef Options - optional params
|
||||
* @param {boolean} multi - (false) allows to take action to several documents
|
||||
* @param {boolean} upsert - (false) if true, upsert document with update fields.
|
||||
* Method will return inserted doc or number of affected docs if doc hasn't been inserted
|
||||
* @param {boolean} returnUpdatedDocs - (false) if true, returns affected docs
|
||||
* Init function for nedb instance
|
||||
*
|
||||
* @param {string} name - name of the data file
|
||||
* @returns {Datastore} db - nedb instance
|
||||
*/
|
||||
interface Options {
|
||||
multi?: boolean;
|
||||
upsert?: boolean;
|
||||
returnUpdatedDocs?: boolean;
|
||||
function initDb(name: string): Datastore {
|
||||
const dbConfig = appConfig.database.driver === 'local' ? appConfig.database.local : null;
|
||||
|
||||
if (!dbConfig) {
|
||||
throw new Error('Database config is not initialized');
|
||||
}
|
||||
|
||||
return new Datastore({
|
||||
filename: path.resolve(`${dbConfig.path}/${name}.db`),
|
||||
autoload: true,
|
||||
});
|
||||
}
|
||||
|
||||
interface ResolveFunction {
|
||||
/**
|
||||
* Resolve function helper
|
||||
*/
|
||||
export interface ResolveFunction {
|
||||
(value: any): void;
|
||||
}
|
||||
|
||||
interface RejectFunction {
|
||||
/**
|
||||
* Reject function helper
|
||||
*/
|
||||
export interface RejectFunction {
|
||||
(reason?: unknown): void;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @class Database
|
||||
* @classdesc Simple decorator class to work with nedb datastore
|
||||
*
|
||||
* @property {Datastore} db - nedb Datastore object
|
||||
* Simple decorator class to work with nedb datastore
|
||||
*/
|
||||
export class Database<DocType> {
|
||||
private db: Datastore;
|
||||
export default class LocalDatabaseDriver<DocType> implements DatabaseDriver<DocType> {
|
||||
/**
|
||||
* @class
|
||||
*
|
||||
* @param {Object} nedbInstance - nedb Datastore object
|
||||
* nedb Datastore object
|
||||
*/
|
||||
constructor(nedbInstance: Datastore) {
|
||||
this.db = nedbInstance;
|
||||
private db: Datastore;
|
||||
|
||||
/**
|
||||
* @param collectionName - collection name for storing data
|
||||
*/
|
||||
constructor(collectionName: string) {
|
||||
this.db = initDb(collectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new document into the database
|
||||
*
|
||||
* @see https://github.com/louischatriot/nedb#inserting-documents
|
||||
*
|
||||
* @param {Object} doc - object to insert
|
||||
* @returns {Promise<Object|Error>} - inserted doc or Error object
|
||||
* @param {object} doc - object to insert
|
||||
* @returns {Promise<object | Error>} - inserted doc or Error object
|
||||
*/
|
||||
public async insert(doc: DocType): Promise<DocType> {
|
||||
return new Promise((resolve, reject) => this.db.insert(doc, (err, newDoc) => {
|
||||
|
@ -65,10 +74,9 @@ export class Database<DocType> {
|
|||
* Find documents that match passed query
|
||||
*
|
||||
* @see https://github.com/louischatriot/nedb#finding-documents
|
||||
*
|
||||
* @param {Object} query - query object
|
||||
* @param {Object} projection - projection object
|
||||
* @returns {Promise<Array<Object>|Error>} - found docs or Error object
|
||||
* @param {object} query - query object
|
||||
* @param {object} projection - projection object
|
||||
* @returns {Promise<Array<object> | Error>} - found docs or Error object
|
||||
*/
|
||||
public async find(query: Record<string, unknown>, projection?: DocType): Promise<Array<DocType>> {
|
||||
const cbk = (resolve: ResolveFunction, reject: RejectFunction) => (err: Error | null, docs: DocType[]) => {
|
||||
|
@ -92,10 +100,9 @@ export class Database<DocType> {
|
|||
* Find one document matches passed query
|
||||
*
|
||||
* @see https://github.com/louischatriot/nedb#finding-documents
|
||||
*
|
||||
* @param {Object} query - query object
|
||||
* @param {Object} projection - projection object
|
||||
* @returns {Promise<Object|Error>} - found doc or Error object
|
||||
* @param {object} query - query object
|
||||
* @param {object} projection - projection object
|
||||
* @returns {Promise<object | Error>} - found doc or Error object
|
||||
*/
|
||||
public async findOne(query: Record<string, unknown>, projection?: DocType): Promise<DocType> {
|
||||
const cbk = (resolve: ResolveFunction, reject: RejectFunction) => (err: Error | null, doc: DocType) => {
|
||||
|
@ -119,11 +126,10 @@ export class Database<DocType> {
|
|||
* Update document matches query
|
||||
*
|
||||
* @see https://github.com/louischatriot/nedb#updating-documents
|
||||
*
|
||||
* @param {Object} query - query object
|
||||
* @param {Object} update - fields to update
|
||||
* @param {object} query - query object
|
||||
* @param {object} update - fields to update
|
||||
* @param {Options} options - optional params
|
||||
* @returns {Promise<number|Object|Object[]|Error>} - number of updated rows or affected docs or Error object
|
||||
* @returns {Promise<number | object | object[] | Error>} - number of updated rows or affected docs or Error object
|
||||
*/
|
||||
public async update(query: Record<string, unknown>, update: DocType, options: Options = {}): Promise<number|boolean|Array<DocType>> {
|
||||
return new Promise((resolve, reject) => this.db.update(query, update, options, (err, result, affectedDocs) => {
|
||||
|
@ -151,8 +157,7 @@ export class Database<DocType> {
|
|||
* Remove document matches passed query
|
||||
*
|
||||
* @see https://github.com/louischatriot/nedb#removing-documents
|
||||
*
|
||||
* @param {Object} query - query object
|
||||
* @param {object} query - query object
|
||||
* @param {Options} options - optional params
|
||||
* @returns {Promise<number|Error>} - number of removed rows or Error object
|
||||
*/
|
||||
|
@ -166,10 +171,3 @@ export class Database<DocType> {
|
|||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
pages: new Database<PageData>(initDb('pages')),
|
||||
aliases: new Database<AliasData>(initDb('aliases')),
|
||||
pagesOrder: new Database<PageOrderData>(initDb('pagesOrder')),
|
||||
files: new Database<FileData>(initDb('files')),
|
||||
};
|
122
src/backend/database/mongodb.ts
Normal file
122
src/backend/database/mongodb.ts
Normal file
|
@ -0,0 +1,122 @@
|
|||
import { Collection, Filter, MongoClient, OptionalUnlessRequiredId, UpdateFilter } from 'mongodb';
|
||||
import { DatabaseDriver, Options } from './types.js';
|
||||
import appConfig from '../utils/appConfig.js';
|
||||
|
||||
const mongodbUri = appConfig.database.driver === 'mongodb' ? appConfig.database.mongodb.uri : null;
|
||||
const mongodbClient = mongodbUri ? await MongoClient.connect(mongodbUri): null;
|
||||
|
||||
/**
|
||||
* MongoDB driver for working with database
|
||||
*/
|
||||
export default class MongoDatabaseDriver<DocType> implements DatabaseDriver<DocType> {
|
||||
/**
|
||||
* Mongo client instance
|
||||
*/
|
||||
private db: MongoClient;
|
||||
|
||||
/**
|
||||
* Collection instance
|
||||
*/
|
||||
private collection: Collection<DocType>;
|
||||
|
||||
/**
|
||||
* Creates driver instance
|
||||
*
|
||||
* @param collectionName - collection to work with
|
||||
*/
|
||||
constructor(collectionName: string) {
|
||||
if (!mongodbClient) {
|
||||
throw new Error('MongoDB client is not initialized');
|
||||
}
|
||||
this.db = mongodbClient;
|
||||
this.collection = mongodbClient.db().collection(collectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new document into the database
|
||||
*
|
||||
* @param {object} doc - object to insert
|
||||
* @returns {Promise<object | Error>} - inserted doc or Error object
|
||||
*/
|
||||
public async insert(doc: DocType): Promise<DocType> {
|
||||
const result = await this.collection.insertOne(doc as OptionalUnlessRequiredId<DocType>);
|
||||
|
||||
return {
|
||||
...doc,
|
||||
_id: result.insertedId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find documents that match passed query
|
||||
*
|
||||
* @param {object} query - query object
|
||||
* @param {object} projection - projection object
|
||||
* @returns {Promise<Array<object> | Error>} - found docs or Error object
|
||||
*/
|
||||
public async find(query: Record<string, unknown>, projection?: DocType): Promise<Array<DocType>> {
|
||||
const cursor = this.collection.find(query as Filter<DocType>);
|
||||
|
||||
if (projection) {
|
||||
cursor.project(projection);
|
||||
}
|
||||
|
||||
const docs = await cursor.toArray();
|
||||
|
||||
return docs as unknown as Array<DocType>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find one document matches passed query
|
||||
*
|
||||
* @param {object} query - query object
|
||||
* @param {object} projection - projection object
|
||||
* @returns {Promise<object | Error>} - found doc or Error object
|
||||
*/
|
||||
public async findOne(query: Record<string, unknown>, projection?: DocType): Promise<DocType> {
|
||||
const doc = await this.collection.findOne(query as Filter<DocType>, { projection });
|
||||
|
||||
return doc as unknown as DocType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update document matches query
|
||||
*
|
||||
* @param {object} query - query object
|
||||
* @param {object} update - fields to update
|
||||
* @param {Options} options - optional params
|
||||
* @returns {Promise<number | object | object[] | Error>} - number of updated rows or affected docs or Error object
|
||||
*/
|
||||
public async update(query: Record<string, unknown>, update: DocType, options: Options = {}): Promise<number|boolean|Array<DocType>> {
|
||||
const updateDocument = {
|
||||
$set: update,
|
||||
} as UpdateFilter<DocType>;
|
||||
const result = await this.collection.updateMany(query as Filter<DocType>, updateDocument, options);
|
||||
|
||||
switch (true) {
|
||||
case options.returnUpdatedDocs:
|
||||
return result.modifiedCount;
|
||||
case options.upsert:
|
||||
if (result.modifiedCount) {
|
||||
return result.modifiedCount;
|
||||
}
|
||||
|
||||
return result as DocType[];
|
||||
default:
|
||||
return result as DocType[];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove document matches passed query
|
||||
*
|
||||
* @param {object} query - query object
|
||||
* @param {Options} options - optional params
|
||||
* @returns {Promise<number|Error>} - number of removed rows or Error object
|
||||
*/
|
||||
public async remove(query: Record<string, unknown>, options: Options = {}): Promise<number> {
|
||||
const result = await this.collection.deleteMany(query as Filter<DocType>);
|
||||
|
||||
return result.deletedCount;
|
||||
}
|
||||
}
|
70
src/backend/database/types.ts
Normal file
70
src/backend/database/types.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
import { ObjectId } from 'mongodb';
|
||||
|
||||
/**
|
||||
* Represents database driver functionality
|
||||
*/
|
||||
export interface DatabaseDriver<DocType> {
|
||||
/**
|
||||
* Insert new document into the database
|
||||
*
|
||||
* @param {object} doc - object to insert
|
||||
* @returns {Promise<object | Error>} - inserted doc or Error object
|
||||
*/
|
||||
insert(doc: DocType): Promise<DocType>;
|
||||
|
||||
/**
|
||||
* Find documents that match passed query
|
||||
*
|
||||
* @param {object} query - query object
|
||||
* @param {object} projection - projection object
|
||||
* @returns {Promise<Array<object> | Error>} - found docs or Error object
|
||||
*/
|
||||
find(query: Record<string, unknown>, projection?: DocType): Promise<Array<DocType>>;
|
||||
|
||||
/**
|
||||
* Find one document matches passed query
|
||||
*
|
||||
* @param {object} query - query object
|
||||
* @param {object} projection - projection object
|
||||
* @returns {Promise<object | Error>} - found doc or Error object
|
||||
*/
|
||||
findOne(query: Record<string, unknown>, projection?: DocType): Promise<DocType>;
|
||||
|
||||
/**
|
||||
* Update document matches query
|
||||
*
|
||||
* @param {object} query - query object
|
||||
* @param {object} update - fields to update
|
||||
* @param {Options} options - optional params
|
||||
* @returns {Promise<number | object | object[] | Error>} - number of updated rows or affected docs or Error object
|
||||
*/
|
||||
update(query: Record<string, unknown>, update: DocType, options: Options): Promise<number|boolean|Array<DocType>>
|
||||
|
||||
/**
|
||||
* Remove document matches passed query
|
||||
*
|
||||
* @param {object} query - query object
|
||||
* @param {Options} options - optional params
|
||||
* @returns {Promise<number|Error>} - number of removed rows or Error object
|
||||
*/
|
||||
remove(query: Record<string, unknown>, options: Options): Promise<number>
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents unique database entity id
|
||||
* unique symbol to prevent type widening (read more https://todayilearned.net/2022/07/typescript-primitive-type-aliases-unique-symbols)
|
||||
*/
|
||||
export type EntityId = (string | ObjectId) & {readonly id: unique symbol};
|
||||
|
||||
/**
|
||||
* @typedef Options - optional params
|
||||
* @param {boolean} multi - (false) allows to take action to several documents
|
||||
* @param {boolean} upsert - (false) if true, upsert document with update fields.
|
||||
* Method will return inserted doc or number of affected docs if doc hasn't been inserted
|
||||
* @param {boolean} returnUpdatedDocs - (false) if true, returns affected docs
|
||||
*/
|
||||
export interface Options {
|
||||
multi?: boolean;
|
||||
upsert?: boolean;
|
||||
returnUpdatedDocs?: boolean;
|
||||
}
|
|
@ -1,46 +1,71 @@
|
|||
import crypto from '../utils/crypto.js';
|
||||
import database from '../utils/database/index.js';
|
||||
import database from '../database/index.js';
|
||||
import { EntityId } from '../database/types.js';
|
||||
|
||||
const binaryMD5 = crypto.binaryMD5;
|
||||
const aliasesDb = database['aliases'];
|
||||
|
||||
/**
|
||||
* @typedef {object} AliasData
|
||||
* @property {string} _id - alias id
|
||||
* @property {string} hash - alias binary hash
|
||||
* @property {string} type - entity type
|
||||
* @property {boolean} deprecated - indicate if alias deprecated
|
||||
* @property {string} id - entity id
|
||||
*
|
||||
* Describe an alias
|
||||
*/
|
||||
export interface AliasData {
|
||||
_id?: string;
|
||||
/**
|
||||
* Alias id
|
||||
*/
|
||||
_id?: EntityId;
|
||||
|
||||
/**
|
||||
* Alias binary hash
|
||||
*/
|
||||
hash?: string;
|
||||
|
||||
/**
|
||||
* Entity type
|
||||
*/
|
||||
type?: string;
|
||||
|
||||
/**
|
||||
* Indicate if alias deprecated
|
||||
*/
|
||||
deprecated?: boolean;
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* Entity id
|
||||
*/
|
||||
id?: EntityId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @class Alias
|
||||
* @classdesc Alias model
|
||||
*
|
||||
* @property {string} _id - alias id
|
||||
* @property {string} hash - alias binary hash
|
||||
* @property {string} type - entity type
|
||||
* @property {boolean} deprecated - indicate if alias deprecated
|
||||
* @property {string} id - entity title
|
||||
* Alias model
|
||||
*/
|
||||
class Alias {
|
||||
public _id?: string;
|
||||
/**
|
||||
* Alias id
|
||||
*/
|
||||
public _id?: EntityId;
|
||||
|
||||
/**
|
||||
* Alias binary hash
|
||||
*/
|
||||
public hash?: string;
|
||||
|
||||
/**
|
||||
* Entity type
|
||||
*/
|
||||
public type?: string;
|
||||
|
||||
/**
|
||||
* Indicate if alias deprecated
|
||||
*/
|
||||
public deprecated?: boolean;
|
||||
public id?: string;
|
||||
|
||||
/**
|
||||
* Entity id
|
||||
*/
|
||||
public id?: EntityId;
|
||||
|
||||
/**
|
||||
* @class
|
||||
*
|
||||
* @param {AliasData} data - info about alias
|
||||
* @param {string} aliasName - alias of entity
|
||||
*/
|
||||
|
@ -108,7 +133,7 @@ class Alias {
|
|||
*/
|
||||
public async save(): Promise<Alias> {
|
||||
if (!this._id) {
|
||||
const insertedRow = await aliasesDb.insert(this.data) as { _id: string };
|
||||
const insertedRow = await aliasesDb.insert(this.data) as { _id: EntityId };
|
||||
|
||||
this._id = insertedRow._id;
|
||||
} else {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import database from '../utils/database/index.js';
|
||||
import database from '../database/index.js';
|
||||
import { EntityId } from '../database/types.js';
|
||||
|
||||
const filesDb = database['files'];
|
||||
|
||||
/**
|
||||
* @typedef {object} FileData
|
||||
*
|
||||
* @property {string} _id - file id
|
||||
* @property {string} name - original file name
|
||||
* @property {string} filename - name of uploaded file
|
||||
|
@ -14,20 +14,18 @@ const filesDb = database['files'];
|
|||
* @property {number} size - size of the file in
|
||||
*/
|
||||
export interface FileData {
|
||||
_id?: string;
|
||||
_id?: EntityId;
|
||||
name?: string;
|
||||
filename?: string;
|
||||
path?: string;
|
||||
mimetype?: string;
|
||||
url?: string;
|
||||
size?: number;
|
||||
[key: string]: string | number | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @class File
|
||||
* @class File model
|
||||
*
|
||||
* @property {string} _id - file id
|
||||
* @property {string} name - original file name
|
||||
* @property {string} filename - name of uploaded file
|
||||
|
@ -36,7 +34,7 @@ export interface FileData {
|
|||
* @property {number} size - size of the file in
|
||||
*/
|
||||
class File {
|
||||
public _id?: string;
|
||||
public _id?: EntityId;
|
||||
public name?: string;
|
||||
public filename?: string;
|
||||
public path?: string;
|
||||
|
@ -46,7 +44,6 @@ class File {
|
|||
|
||||
/**
|
||||
* @class
|
||||
*
|
||||
* @param {FileData} data - info about file
|
||||
*/
|
||||
constructor(data: FileData = {}) {
|
||||
|
@ -136,7 +133,7 @@ class File {
|
|||
*/
|
||||
public async save(): Promise<File> {
|
||||
if (!this._id) {
|
||||
const insertedRow = await filesDb.insert(this.data) as { _id: string };
|
||||
const insertedRow = await filesDb.insert(this.data) as { _id: EntityId };
|
||||
|
||||
this._id = insertedRow._id;
|
||||
} else {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import urlify from '../utils/urlify.js';
|
||||
import database from '../utils/database/index.js';
|
||||
import database, {isEqualIds} from '../database/index.js';
|
||||
import { EntityId } from '../database/types.js';
|
||||
|
||||
const pagesDb = database['pages'];
|
||||
|
||||
|
@ -12,17 +13,16 @@ const pagesDb = database['pages'];
|
|||
* @property {string} parent - id of parent page
|
||||
*/
|
||||
export interface PageData {
|
||||
_id?: string;
|
||||
_id?: EntityId;
|
||||
title?: string;
|
||||
uri?: string;
|
||||
body?: any;
|
||||
parent?: string;
|
||||
parent?: EntityId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @class Page
|
||||
* @class Page model
|
||||
*
|
||||
* @property {string} _id - page id
|
||||
* @property {string} title - page title
|
||||
* @property {string} uri - page uri
|
||||
|
@ -30,15 +30,14 @@ export interface PageData {
|
|||
* @property {string} _parent - id of parent page
|
||||
*/
|
||||
class Page {
|
||||
public _id?: string;
|
||||
public _id?: EntityId;
|
||||
public body?: any;
|
||||
public title?: string;
|
||||
public uri?: string;
|
||||
public _parent?: string;
|
||||
public _parent?: EntityId;
|
||||
|
||||
/**
|
||||
* @class
|
||||
*
|
||||
* @param {PageData} data - page's data
|
||||
*/
|
||||
constructor(data: PageData = {}) {
|
||||
|
@ -59,7 +58,7 @@ class Page {
|
|||
* @param {string} _id - page id
|
||||
* @returns {Promise<Page>}
|
||||
*/
|
||||
public static async get(_id: string): Promise<Page> {
|
||||
public static async get(_id: EntityId): Promise<Page> {
|
||||
const data = await pagesDb.findOne({ _id });
|
||||
|
||||
return new Page(data);
|
||||
|
@ -86,7 +85,7 @@ class Page {
|
|||
public static async getAll(query: Record<string, unknown> = {}): Promise<Page[]> {
|
||||
const docs = await pagesDb.find(query);
|
||||
|
||||
return Promise.all(docs.map(doc => new Page(doc)));
|
||||
return docs.map(doc => new Page(doc));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -100,7 +99,7 @@ class Page {
|
|||
this.body = body || this.body;
|
||||
this.title = this.extractTitleFromBody();
|
||||
this.uri = uri || '';
|
||||
this._parent = parent || this._parent || '0';
|
||||
this._parent = parent || this._parent || '0' as EntityId;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -161,7 +160,7 @@ class Page {
|
|||
}
|
||||
|
||||
if (!this._id) {
|
||||
const insertedRow = await pagesDb.insert(this.data) as { _id: string };
|
||||
const insertedRow = await pagesDb.insert(this.data) as { _id: EntityId };
|
||||
|
||||
this._id = insertedRow._id;
|
||||
} else {
|
||||
|
@ -209,7 +208,7 @@ class Page {
|
|||
if (uri) {
|
||||
let pageWithSameUri = await Page.getByUri(uri);
|
||||
|
||||
while (pageWithSameUri._id && pageWithSameUri._id !== this._id) {
|
||||
while (pageWithSameUri._id && !isEqualIds(pageWithSameUri._id, this._id)) {
|
||||
pageWithSameUriCount++;
|
||||
pageWithSameUri = await Page.getByUri(uri + `-${pageWithSameUriCount}`);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import database from '../utils/database/index.js';
|
||||
import database, { isEntityId, isEqualIds } from '../database/index.js';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { EntityId } from '../database/types.js';
|
||||
|
||||
const db = database['pagesOrder'];
|
||||
|
||||
|
@ -9,9 +11,9 @@ const db = database['pagesOrder'];
|
|||
* @property {Array<string>} order - list of ordered pages
|
||||
*/
|
||||
export interface PageOrderData {
|
||||
_id?: string;
|
||||
page?: string;
|
||||
order?: string[];
|
||||
_id?: EntityId;
|
||||
page?: EntityId;
|
||||
order?: EntityId[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -21,14 +23,13 @@ export interface PageOrderData {
|
|||
* Creates order for Pages with children
|
||||
*/
|
||||
class PageOrder {
|
||||
public _id?: string;
|
||||
public page?: string;
|
||||
private _order?: string[];
|
||||
public _id?: EntityId;
|
||||
public page?: EntityId;
|
||||
private _order?: EntityId[];
|
||||
|
||||
|
||||
/**
|
||||
* @class
|
||||
*
|
||||
* @param {PageOrderData} data - info about pageOrder
|
||||
*/
|
||||
constructor(data: PageOrderData = {}) {
|
||||
|
@ -49,7 +50,7 @@ class PageOrder {
|
|||
* @param {string} pageId - page's id
|
||||
* @returns {Promise<PageOrder>}
|
||||
*/
|
||||
public static async get(pageId: string): Promise<PageOrder> {
|
||||
public static async get(pageId: EntityId): Promise<PageOrder> {
|
||||
const order = await db.findOne({ page: pageId });
|
||||
|
||||
let data: PageOrderData = {};
|
||||
|
@ -103,7 +104,7 @@ class PageOrder {
|
|||
* @param {PageOrderData} pageOrderData - info about pageOrder
|
||||
*/
|
||||
public set data(pageOrderData: PageOrderData) {
|
||||
this.page = pageOrderData.page || '0';
|
||||
this.page = pageOrderData.page || '0' as EntityId;
|
||||
this.order = pageOrderData.order || [];
|
||||
}
|
||||
|
||||
|
@ -115,7 +116,7 @@ class PageOrder {
|
|||
public get data(): PageOrderData {
|
||||
return {
|
||||
_id: this._id,
|
||||
page: '' + this.page,
|
||||
page: this.page,
|
||||
order: this.order,
|
||||
};
|
||||
}
|
||||
|
@ -125,8 +126,8 @@ class PageOrder {
|
|||
*
|
||||
* @param {string} pageId - page's id
|
||||
*/
|
||||
public push(pageId: string | number): void {
|
||||
if (typeof pageId === 'string') {
|
||||
public push(pageId: EntityId): void {
|
||||
if (isEntityId(pageId)) {
|
||||
if (this.order === undefined) {
|
||||
this.order = [];
|
||||
}
|
||||
|
@ -141,12 +142,12 @@ class PageOrder {
|
|||
*
|
||||
* @param {string} pageId - page's id
|
||||
*/
|
||||
public remove(pageId: string): void {
|
||||
public remove(pageId: EntityId): void {
|
||||
if (this.order === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const found = this.order.indexOf(pageId);
|
||||
const found = this.order.findIndex(order => isEqualIds(order, pageId));
|
||||
|
||||
if (found >= 0) {
|
||||
this.order.splice(found, 1);
|
||||
|
@ -156,16 +157,15 @@ class PageOrder {
|
|||
/**
|
||||
* @param {string} currentPageId - page's id that changes the order
|
||||
* @param {string} putAbovePageId - page's id above which we put the target page
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
public putAbove(currentPageId: string, putAbovePageId: string): void {
|
||||
public putAbove(currentPageId: EntityId, putAbovePageId: EntityId): void {
|
||||
if (this.order === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const found1 = this.order.indexOf(putAbovePageId);
|
||||
const found2 = this.order.indexOf(currentPageId);
|
||||
const found1 = this.order.findIndex(order => isEqualIds(order, putAbovePageId));
|
||||
const found2 = this.order.findIndex(order => isEqualIds(order, currentPageId));
|
||||
|
||||
if (found1 === -1 || found2 === -1) {
|
||||
return;
|
||||
|
@ -182,12 +182,12 @@ class PageOrder {
|
|||
*
|
||||
* @param {string} pageId - identity of page
|
||||
*/
|
||||
public getSubPageBefore(pageId: string): string | null {
|
||||
public getSubPageBefore(pageId: EntityId): EntityId | null {
|
||||
if (this.order === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentPageInOrder = this.order.indexOf(pageId);
|
||||
const currentPageInOrder = this.order.findIndex(order => isEqualIds(order, pageId));
|
||||
|
||||
/**
|
||||
* If page not found or first return nothing
|
||||
|
@ -204,12 +204,12 @@ class PageOrder {
|
|||
*
|
||||
* @param pageId - identity of page
|
||||
*/
|
||||
public getSubPageAfter(pageId: string): string | null {
|
||||
public getSubPageAfter(pageId: EntityId): EntityId | null {
|
||||
if (this.order === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentPageInOrder = this.order.indexOf(pageId);
|
||||
const currentPageInOrder = this.order.findIndex(order => isEqualIds(order, pageId));
|
||||
|
||||
/**
|
||||
* If page not found or is last
|
||||
|
@ -224,7 +224,7 @@ class PageOrder {
|
|||
/**
|
||||
* @param {string[]} order - define new order
|
||||
*/
|
||||
public set order(order: string[]) {
|
||||
public set order(order: EntityId[]) {
|
||||
this._order = order;
|
||||
}
|
||||
|
||||
|
@ -233,7 +233,7 @@ class PageOrder {
|
|||
*
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public get order(): string[] {
|
||||
public get order(): EntityId[] {
|
||||
return this._order || [];
|
||||
}
|
||||
|
||||
|
@ -244,7 +244,7 @@ class PageOrder {
|
|||
*/
|
||||
public async save(): Promise<PageOrder> {
|
||||
if (!this._id) {
|
||||
const insertedRow = await db.insert(this.data) as { _id: string};
|
||||
const insertedRow = await db.insert(this.data) as { _id: EntityId};
|
||||
|
||||
this._id = insertedRow._id;
|
||||
} else {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import Page from './page.js';
|
||||
import PageOrder from './pageOrder.js';
|
||||
import NodeCache from 'node-cache';
|
||||
import { EntityId } from '../database/types.js';
|
||||
import { isEqualIds } from '../database/index.js';
|
||||
|
||||
// Create cache for flat array
|
||||
const cache = new NodeCache({ stdTTL: 120 });
|
||||
|
@ -14,12 +16,12 @@ export interface PagesFlatArrayData {
|
|||
/**
|
||||
* Page id
|
||||
*/
|
||||
id: string;
|
||||
id: EntityId;
|
||||
|
||||
/**
|
||||
* Page parent id
|
||||
*/
|
||||
parentId?: string;
|
||||
parentId?: EntityId;
|
||||
|
||||
/**
|
||||
* id of parent with parent id '0'
|
||||
|
@ -105,10 +107,10 @@ class PagesFlatArray {
|
|||
* @param pageId - page id
|
||||
* @returns {Promise<PagesFlatArrayData | undefined>}
|
||||
*/
|
||||
public static async getPageBefore(pageId: string): Promise<PagesFlatArrayData | undefined> {
|
||||
public static async getPageBefore(pageId: EntityId): Promise<PagesFlatArrayData | undefined> {
|
||||
const arr = await this.get();
|
||||
|
||||
const pageIndex = arr.findIndex( (item) => item.id == pageId);
|
||||
const pageIndex = arr.findIndex((item) => isEqualIds(item.id, pageId));
|
||||
|
||||
// Check if index is not the first
|
||||
if (pageIndex && pageIndex > 0) {
|
||||
|
@ -125,10 +127,10 @@ class PagesFlatArray {
|
|||
* @param pageId - page id
|
||||
* @returns {Promise<PagesFlatArrayData | undefined>}
|
||||
*/
|
||||
public static async getPageAfter(pageId: string): Promise<PagesFlatArrayData | undefined> {
|
||||
public static async getPageAfter(pageId: EntityId): Promise<PagesFlatArrayData | undefined> {
|
||||
const arr = await this.get();
|
||||
|
||||
const pageIndex = arr.findIndex( (item) => item.id == pageId );
|
||||
const pageIndex = arr.findIndex( (item) => isEqualIds(item.id, pageId));
|
||||
|
||||
// Check if index is not the last
|
||||
if (pageIndex < arr.length -1) {
|
||||
|
@ -148,11 +150,11 @@ class PagesFlatArray {
|
|||
* @param orders - all page orders
|
||||
* @returns {Promise<Array<PagesFlatArrayData>>}
|
||||
*/
|
||||
private static getChildrenFlatArray(pageId: string, level: number,
|
||||
private static getChildrenFlatArray(pageId: EntityId, level: number,
|
||||
pages: Array<Page>, orders: Array<PageOrder>): Array<PagesFlatArrayData> {
|
||||
let arr: Array<PagesFlatArrayData> = new Array<PagesFlatArrayData>();
|
||||
|
||||
const page = pages.find( item => item._id == pageId );
|
||||
const page = pages.find(item => isEqualIds(item._id, pageId));
|
||||
|
||||
// Add element to child array
|
||||
if (page) {
|
||||
|
@ -166,7 +168,7 @@ class PagesFlatArray {
|
|||
} );
|
||||
}
|
||||
|
||||
const order = orders.find(item => item.page == pageId);
|
||||
const order = orders.find(item => isEqualIds(item.page, pageId));
|
||||
|
||||
if (order) {
|
||||
for (const childPageId of order.order) {
|
||||
|
|
|
@ -2,6 +2,8 @@ import express, { Request, Response } from 'express';
|
|||
import multerFunc from 'multer';
|
||||
import Pages from '../../controllers/pages.js';
|
||||
import PagesOrder from '../../controllers/pagesOrder.js';
|
||||
import { EntityId } from '../../database/types.js';
|
||||
import { isEntityId, isEqualIds, toEntityId } from '../../database/index.js';
|
||||
|
||||
const router = express.Router();
|
||||
const multer = multerFunc();
|
||||
|
@ -14,7 +16,7 @@ const multer = multerFunc();
|
|||
|
||||
router.get('/page/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const page = await Pages.get(req.params.id);
|
||||
const page = await Pages.get(toEntityId(req.params.id));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
|
@ -35,7 +37,7 @@ router.get('/page/:id', async (req: Request, res: Response) => {
|
|||
*/
|
||||
router.get('/pages', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const pages = await Pages.getAll();
|
||||
const pages = await Pages.getAllPages();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
|
@ -56,7 +58,8 @@ router.get('/pages', async (req: Request, res: Response) => {
|
|||
*/
|
||||
router.put('/page', multer.none(), async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { title, body, parent } = req.body;
|
||||
const { title, body } = req.body;
|
||||
const parent = toEntityId(req.body.parent);
|
||||
const page = await Pages.insert({
|
||||
title,
|
||||
body,
|
||||
|
@ -88,11 +91,12 @@ router.put('/page', multer.none(), async (req: Request, res: Response) => {
|
|||
* Update page data in the database
|
||||
*/
|
||||
router.post('/page/:id', multer.none(), async (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
const id = toEntityId(req.params.id);
|
||||
|
||||
try {
|
||||
const { title, body, parent, putAbovePageId, uri } = req.body;
|
||||
const pages = await Pages.getAll();
|
||||
const { title, body, putAbovePageId, uri } = req.body;
|
||||
const parent = toEntityId(req.body.parent);
|
||||
const pages = await Pages.getAllPages();
|
||||
let page = await Pages.get(id);
|
||||
|
||||
if (page._id === undefined) {
|
||||
|
@ -103,16 +107,16 @@ router.post('/page/:id', multer.none(), async (req: Request, res: Response) => {
|
|||
throw new Error('Parent not found');
|
||||
}
|
||||
|
||||
if (page._parent !== parent) {
|
||||
if (!isEqualIds(page._parent, parent)) {
|
||||
await PagesOrder.move(page._parent, parent, id);
|
||||
} else {
|
||||
if (putAbovePageId && putAbovePageId !== '0') {
|
||||
const unordered = pages.filter(_page => _page._parent === page._parent).map(_page => _page._id);
|
||||
const unordered = pages.filter(_page => isEqualIds(_page._parent, page._parent)).map(_page => _page._id);
|
||||
|
||||
const unOrdered: string[] = [];
|
||||
const unOrdered: EntityId[] = [];
|
||||
|
||||
unordered.forEach(item => {
|
||||
if (typeof item === 'string') {
|
||||
if (isEntityId(item)) {
|
||||
unOrdered.push(item);
|
||||
}
|
||||
});
|
||||
|
@ -146,7 +150,7 @@ router.post('/page/:id', multer.none(), async (req: Request, res: Response) => {
|
|||
*/
|
||||
router.delete('/page/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const pageId = req.params.id;
|
||||
const pageId = toEntityId(req.params.id);
|
||||
const page = await Pages.get(pageId);
|
||||
|
||||
if (page._id === undefined) {
|
||||
|
@ -177,8 +181,8 @@ router.delete('/page/:id', async (req: Request, res: Response) => {
|
|||
* @param {string} startFrom - start point to delete
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const deleteRecursively = async (startFrom: string): Promise<void> => {
|
||||
let order: string[] = [];
|
||||
const deleteRecursively = async (startFrom: EntityId): Promise<void> => {
|
||||
let order: EntityId[] = [];
|
||||
|
||||
try {
|
||||
const children = await PagesOrder.get(startFrom);
|
||||
|
@ -200,10 +204,12 @@ router.delete('/page/:id', async (req: Request, res: Response) => {
|
|||
}
|
||||
};
|
||||
|
||||
await deleteRecursively(req.params.id);
|
||||
const id = toEntityId(req.params.id);
|
||||
|
||||
await deleteRecursively(id);
|
||||
|
||||
// remove also from parent's order
|
||||
parentPageOrder.remove(req.params.id);
|
||||
parentPageOrder.remove(id);
|
||||
await parentPageOrder.save();
|
||||
|
||||
res.json({
|
||||
|
|
|
@ -4,7 +4,7 @@ import mime from 'mime';
|
|||
import mkdirp from 'mkdirp';
|
||||
import Transport from '../../controllers/transport.js';
|
||||
import { random16 } from '../../utils/crypto.js';
|
||||
import appConfig from "../../utils/appConfig.js";
|
||||
import appConfig from '../../utils/appConfig.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import express, { Request, Response } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import csrf from 'csurf';
|
||||
import appConfig from "../utils/appConfig.js";
|
||||
import appConfig from '../utils/appConfig.js';
|
||||
|
||||
const router = express.Router();
|
||||
const csrfProtection = csrf({ cookie: true });
|
||||
|
|
|
@ -4,6 +4,8 @@ import PagesOrder from '../../controllers/pagesOrder.js';
|
|||
import Page from '../../models/page.js';
|
||||
import asyncMiddleware from '../../utils/asyncMiddleware.js';
|
||||
import PageOrder from '../../models/pageOrder.js';
|
||||
import { EntityId } from '../../database/types.js';
|
||||
import { isEqualIds } from '../../database/index.js';
|
||||
|
||||
/**
|
||||
* Process one-level pages list to parent-children list
|
||||
|
@ -13,11 +15,10 @@ import PageOrder from '../../models/pageOrder.js';
|
|||
* @param {PagesOrder[]} pagesOrder - list of pages order
|
||||
* @param {number} level - max level recursion
|
||||
* @param {number} currentLevel - current level of element
|
||||
*
|
||||
* @returns {Page[]}
|
||||
*/
|
||||
function createMenuTree(parentPageId: string, pages: Page[], pagesOrder: PageOrder[], level = 1, currentLevel = 1): Page[] {
|
||||
const childrenOrder = pagesOrder.find(order => order.data.page === parentPageId);
|
||||
function createMenuTree(parentPageId: EntityId, pages: Page[], pagesOrder: PageOrder[], level = 1, currentLevel = 1): Page[] {
|
||||
const childrenOrder = pagesOrder.find(order => isEqualIds(order.data.page, parentPageId));
|
||||
|
||||
/**
|
||||
* branch is a page children in tree
|
||||
|
@ -27,12 +28,12 @@ function createMenuTree(parentPageId: string, pages: Page[], pagesOrder: PageOrd
|
|||
let ordered: any[] = [];
|
||||
|
||||
if (childrenOrder) {
|
||||
ordered = childrenOrder.order.map((pageId: string) => {
|
||||
return pages.find(page => page._id === pageId);
|
||||
ordered = childrenOrder.order.map((pageId: EntityId) => {
|
||||
return pages.find(page => isEqualIds(page._id, pageId));
|
||||
});
|
||||
}
|
||||
|
||||
const unordered = pages.filter(page => page._parent === parentPageId);
|
||||
const unordered = pages.filter(page => isEqualIds(page._parent, parentPageId));
|
||||
const branch = Array.from(new Set([...ordered, ...unordered]));
|
||||
|
||||
/**
|
||||
|
@ -65,10 +66,10 @@ export default asyncMiddleware(async (req: Request, res: Response, next: NextFun
|
|||
*
|
||||
* @type {string}
|
||||
*/
|
||||
const parentIdOfRootPages = '0';
|
||||
const parentIdOfRootPages = '0' as EntityId;
|
||||
|
||||
try {
|
||||
const pages = await Pages.getAll();
|
||||
const pages = await Pages.getAllPages();
|
||||
const pagesOrder = await PagesOrder.getAll();
|
||||
|
||||
res.locals.menu = createMenuTree(parentIdOfRootPages, pages, pagesOrder, 2);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { NextFunction, Request, Response } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import appConfig from "../../utils/appConfig.js";
|
||||
import appConfig from '../../utils/appConfig.js';
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,6 +4,7 @@ import PagesOrder from '../controllers/pagesOrder.js';
|
|||
import verifyToken from './middlewares/token.js';
|
||||
import allowEdit from './middlewares/locals.js';
|
||||
import PagesFlatArray from '../models/pagesFlatArray.js';
|
||||
import { toEntityId } from '../database/index.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
@ -14,6 +15,8 @@ router.get('/page/new', verifyToken, allowEdit, async (req: Request, res: Respon
|
|||
try {
|
||||
const pagesAvailableGrouped = await Pages.groupByParent();
|
||||
|
||||
console.log(pagesAvailableGrouped);
|
||||
|
||||
res.render('pages/form', {
|
||||
pagesAvailableGrouped,
|
||||
page: null,
|
||||
|
@ -28,7 +31,7 @@ router.get('/page/new', verifyToken, allowEdit, async (req: Request, res: Respon
|
|||
* Edit page form
|
||||
*/
|
||||
router.get('/page/edit/:id', verifyToken, allowEdit, async (req: Request, res: Response, next: NextFunction) => {
|
||||
const pageId = req.params.id;
|
||||
const pageId = toEntityId(req.params.id);
|
||||
|
||||
try {
|
||||
const page = await Pages.get(pageId);
|
||||
|
@ -56,7 +59,7 @@ router.get('/page/edit/:id', verifyToken, allowEdit, async (req: Request, res: R
|
|||
* View page
|
||||
*/
|
||||
router.get('/page/:id', verifyToken, async (req: Request, res: Response, next: NextFunction) => {
|
||||
const pageId = req.params.id;
|
||||
const pageId = toEntityId(req.params.id);
|
||||
|
||||
try {
|
||||
const page = await Pages.get(pageId);
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { loadConfig } from 'config-loader';
|
||||
import { loadConfig } from '@codex-team/config-loader';
|
||||
import * as process from 'process';
|
||||
import arg from 'arg';
|
||||
import path from 'path';
|
||||
import { z } from "zod";
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Configuration for Hawk errors catcher
|
||||
|
@ -10,19 +10,38 @@ import { z } from "zod";
|
|||
const HawkConfig = z.object({
|
||||
backendToken: z.string().optional(), // Hawk backend token
|
||||
frontendToken: z.string().optional(), // Hawk frontend token
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* Config for local database driver
|
||||
*/
|
||||
const LocalDatabaseConfig = z.object({
|
||||
driver: z.literal('local'),
|
||||
local: z.object({
|
||||
path: z.string()
|
||||
})
|
||||
})
|
||||
path: z.string(), // path to the database directory
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Config for MongoDB database driver
|
||||
*/
|
||||
const MongoDatabaseConfig = z.object({
|
||||
driver: z.literal('mongodb'),
|
||||
mongodb: z.object({
|
||||
uri: z.string(), // MongoDB connection URI
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Config for authentication
|
||||
*/
|
||||
const AuthConfig = z.object({
|
||||
secret: z.string() // Secret for JWT
|
||||
})
|
||||
secret: z.string(), // Secret for JWT
|
||||
});
|
||||
|
||||
/**
|
||||
* Frontend configuration
|
||||
*/
|
||||
const FrontendConfig = z.object({
|
||||
title: z.string(), // Title for pages
|
||||
description: z.string(), // Description for pages
|
||||
|
@ -33,8 +52,9 @@ const FrontendConfig = z.object({
|
|||
serve: z.string().optional(), // Carbon serve url
|
||||
placement: z.string().optional(), // Carbon placement
|
||||
}),
|
||||
menu: z.array(z.union([z.string(), z.object({title: z.string(), uri: z.string()})])), // Menu for pages
|
||||
})
|
||||
menu: z.array(z.union([z.string(), z.object({ title: z.string(),
|
||||
uri: z.string() })])), // Menu for pages
|
||||
});
|
||||
|
||||
/**
|
||||
* Application configuration
|
||||
|
@ -48,8 +68,8 @@ const AppConfig = z.object({
|
|||
password: z.string(), // Password for admin panel
|
||||
frontend: FrontendConfig, // Frontend configuration
|
||||
auth: AuthConfig, // Auth configuration
|
||||
database: LocalDatabaseConfig, // Database configuration
|
||||
})
|
||||
database: z.union([LocalDatabaseConfig, MongoDatabaseConfig]), // Database configuration
|
||||
});
|
||||
|
||||
export type AppConfig = z.infer<typeof AppConfig>;
|
||||
|
||||
|
@ -59,7 +79,7 @@ const args = arg({ /* eslint-disable @typescript-eslint/naming-convention */
|
|||
});
|
||||
|
||||
const cwd = process.cwd();
|
||||
const paths = (args['--config'] || ['./app-config.yaml']).map((configPath) => {
|
||||
const paths = (args['--config'] || [ './app-config.yaml' ]).map((configPath) => {
|
||||
if (path.isAbsolute(configPath)) {
|
||||
return configPath;
|
||||
}
|
||||
|
@ -69,6 +89,6 @@ const paths = (args['--config'] || ['./app-config.yaml']).map((configPath) => {
|
|||
|
||||
const loadedConfig = loadConfig<AppConfig>(...paths);
|
||||
|
||||
const appConfig = AppConfig.parse(loadedConfig)
|
||||
const appConfig = AppConfig.parse(loadedConfig);
|
||||
|
||||
export default appConfig;
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
import Datastore from 'nedb';
|
||||
import path from 'path';
|
||||
import appConfig from "../appConfig.js";
|
||||
|
||||
/**
|
||||
* Init function for nedb instance
|
||||
*
|
||||
* @param {string} name - name of the data file
|
||||
* @returns {Datastore} db - nedb instance
|
||||
*/
|
||||
export default function initDb(name: string): Datastore {
|
||||
return new Datastore({
|
||||
filename: path.resolve(`${appConfig.database.local.path}/${name}.db`),
|
||||
autoload: true,
|
||||
});
|
||||
}
|
|
@ -54,4 +54,28 @@ export default (function () {
|
|||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Converts object to string
|
||||
*
|
||||
* @param {object} object - object to be converted
|
||||
* @returns {string} stringified object
|
||||
*/
|
||||
twig.extendFunction('toString', function (object: object): string {
|
||||
if (!object) {
|
||||
return object;
|
||||
}
|
||||
|
||||
return object.toString();
|
||||
});
|
||||
|
||||
/**
|
||||
* Converts JSON to string
|
||||
*
|
||||
* @param {string} data - data to be converted
|
||||
* @returns {string} - converted data
|
||||
*/
|
||||
twig.extendFilter('json_stringify', function (data: any): string {
|
||||
return JSON.stringify(data);
|
||||
});
|
||||
}());
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
<a
|
||||
class="docs-sidebar__section-list-item-wrapper"
|
||||
href="{{ child.uri ? '/' ~ child.uri : '/page/' ~ child._id }}">
|
||||
<div class="docs-sidebar__section-list-item {{page is defined and page._id == child._id ? 'docs-sidebar__section-list-item--active' : ''}}">
|
||||
<div class="docs-sidebar__section-list-item {{page is defined and toString(page._id) == toString(child._id) ? 'docs-sidebar__section-list-item--active' : ''}}">
|
||||
<span>{{ child.title | striptags }}</span>
|
||||
</div>
|
||||
</a>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<section data-module="writing">
|
||||
<textarea name="module-settings" hidden>
|
||||
{
|
||||
"page": {{ page | json_encode | escape }}
|
||||
"page": {{ page | json_stringify | escape }}
|
||||
}
|
||||
</textarea>
|
||||
<header class="writing-header">
|
||||
|
@ -27,8 +27,8 @@
|
|||
<select id="parent" name="parent">
|
||||
<option value="0">Root</option>
|
||||
{% for _page in pagesAvailableGrouped %}
|
||||
{% if _page._id != currentPageId %}
|
||||
<option value="{{ _page._id }}" {{ page is not empty and page._parent == _page._id ? 'selected' : ''}}>
|
||||
{% if toString(_page._id) != toString(currentPageId) %}
|
||||
<option value="{{ toString(_page._id) }}" {{ page is not empty and toString(page._parent) == toString(_page._id) ? 'selected' : ''}}>
|
||||
{% if _page._parent != "0" %}
|
||||
|
||||
|
||||
|
@ -45,7 +45,7 @@
|
|||
<select id="above" name="above">
|
||||
<option value="0">—</option>
|
||||
{% for _page in parentsChildrenOrdered %}
|
||||
<option value="{{ _page._id }}">{{ _page.title }}</option>
|
||||
<option value="{{ toString(_page._id) }}">{{ _page.title }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@ import config from 'config';
|
|||
import { expect } from 'chai';
|
||||
import Datastore from 'nedb';
|
||||
|
||||
import { Database } from '../backend/utils/database/index.js';
|
||||
import { Database } from '../backend/database/index.js';
|
||||
|
||||
interface Document {
|
||||
data?: any;
|
||||
|
|
|
@ -4,7 +4,7 @@ import path from 'path';
|
|||
import config from 'config';
|
||||
import Alias from '../../backend/models/alias.js';
|
||||
import { binaryMD5 } from '../../backend/utils/crypto.js';
|
||||
import database from '../../backend/utils/database/index.js';
|
||||
import database from '../../backend/database/index.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const aliases = database['aliases'];
|
||||
|
|
|
@ -3,7 +3,7 @@ import fs from 'fs';
|
|||
import path from 'path';
|
||||
import config from 'config';
|
||||
import File from '../../backend/models/file.js';
|
||||
import database from '../../backend/utils/database/index.js';
|
||||
import database from '../../backend/database/index.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,7 +4,7 @@ import path from 'path';
|
|||
import config from 'config';
|
||||
import Page from '../../backend/models/page.js';
|
||||
import translateString from '../../backend/utils/translation.js';
|
||||
import database from '../../backend/utils/database/index.js';
|
||||
import database from '../../backend/database/index.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,7 +3,7 @@ import fs from 'fs';
|
|||
import path from 'path';
|
||||
import config from 'config';
|
||||
import PageOrder from '../../backend/models/pageOrder.js';
|
||||
import database from '../../backend/utils/database/index.js';
|
||||
import database from '../../backend/database/index.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue