├── benchmark
├── suites
│ ├── .gitignore
│ ├── native-nedb.js
│ └── transform.js
├── index.js
├── .eslintrc.js
├── utils.js
├── generate-result.js
└── results
│ ├── transform
│ ├── README.md
│ └── benchmark_results.json
│ └── common
│ └── README.md
├── examples
├── multi-tenants
│ ├── .gitignore
│ ├── plenty.js
│ └── index.js
├── index.js
├── knex-migration
│ ├── migrations
│ │ ├── 20210425193759_author.js
│ │ └── 20210425191836_init.js
│ └── index.js
├── connect
│ └── index.js
├── global-pool
│ └── index.js
├── many
│ └── index.js
└── simple
│ └── index.js
├── .vscode
├── settings.json
└── launch.json
├── .npmignore
├── prettier.config.js
├── test
├── scripts
│ ├── mysql-create-databases.sql
│ └── pg-create-databases.sql
├── integration
│ ├── utils.js
│ ├── transform.test.js
│ ├── index.spec.js
│ └── adapter.test.js
├── leak-detection
│ ├── self-check.spec.js
│ └── index.spec.js
├── unit
│ ├── utils.spec.js
│ └── schema.spec.js
└── docker-compose.yml
├── src
├── errors.js
├── utils.js
├── adapters
│ ├── index.js
│ ├── base.js
│ └── nedb.js
├── constants.js
├── schema.js
├── transform.js
├── monitoring.js
├── index.js
└── actions.js
├── index.js
├── .github
└── workflows
│ ├── notification.yml
│ ├── unit.yml
│ ├── dependencies.yml
│ ├── benchmark.yml
│ └── integration.yml
├── .editorconfig
├── .eslintrc.js
├── LICENSE
├── CHANGELOG.md
├── TODO.md
├── .gitignore
├── docs
└── adapters
│ ├── MongoDB.md
│ ├── NeDB.md
│ └── Knex.md
├── package.json
├── README.md
└── CLAUDE.md
/benchmark/suites/.gitignore:
--------------------------------------------------------------------------------
1 | tmp/
2 |
--------------------------------------------------------------------------------
/examples/multi-tenants/.gitignore:
--------------------------------------------------------------------------------
1 | posts/
2 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 |
3 | "jest.autoRun": "off"
4 | }
5 |
--------------------------------------------------------------------------------
/benchmark/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | require("./suites/" + (process.argv[2] || "common"));
--------------------------------------------------------------------------------
/examples/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const moduleName = process.argv[2] || "simple";
4 | process.argv.splice(2, 1);
5 |
6 | require("./" + moduleName);
7 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .vscode/
2 | .github/
3 | benchmark/
4 | coverage/
5 | dev/
6 | docs/
7 | example/
8 | examples/
9 | typings/
10 | test/
11 | *.db
12 | .editorconfig
13 | .eslintrc.js
14 | prettier.config.js
15 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | useTabs: true,
3 | printWidth: 100,
4 | trailingComma: "none",
5 | tabWidth: 4,
6 | singleQuote: false,
7 | semi: true,
8 | bracketSpacing: true,
9 | arrowParens: "avoid"
10 | };
11 |
--------------------------------------------------------------------------------
/test/scripts/mysql-create-databases.sql:
--------------------------------------------------------------------------------
1 | CREATE DATABASE IF NOT EXISTS bench_test;
2 | CREATE DATABASE IF NOT EXISTS db_int_test;
3 | CREATE DATABASE IF NOT EXISTS db_int_posts_1000;
4 | CREATE DATABASE IF NOT EXISTS db_int_posts_1001;
5 | CREATE DATABASE IF NOT EXISTS db_int_posts_1002;
6 | CREATE DATABASE IF NOT EXISTS db_int_posts_1003;
7 |
--------------------------------------------------------------------------------
/examples/knex-migration/migrations/20210425193759_author.js:
--------------------------------------------------------------------------------
1 | exports.up = function (knex) {
2 | return knex.schema.alterTable("posts", table => {
3 | table.integer("author");
4 | });
5 | };
6 |
7 | exports.down = function (knex) {
8 | return knex.schema.alterTable("posts", table => {
9 | table.dropColumn("author");
10 | });
11 | };
12 |
--------------------------------------------------------------------------------
/test/scripts/pg-create-databases.sql:
--------------------------------------------------------------------------------
1 | CREATE DATABASE bench_test;
2 | CREATE DATABASE db_int_test;
3 | CREATE DATABASE db_int_posts_1000;
4 | CREATE DATABASE db_int_posts_1001;
5 | CREATE DATABASE db_int_posts_1002;
6 | CREATE DATABASE db_int_posts_1003;
7 |
8 | \connect db_int_test;
9 | CREATE SCHEMA tenant_1000;
10 | CREATE SCHEMA tenant_1001;
11 | CREATE SCHEMA tenant_1002;
12 | CREATE SCHEMA tenant_1003;
13 |
--------------------------------------------------------------------------------
/test/integration/utils.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | function equalAtLeast(test, orig) {
4 | Object.keys(orig).forEach(key => {
5 | expect(test[key]).toEqual(orig[key]);
6 | });
7 | }
8 |
9 | function addExpectAnyFields(doc, def) {
10 | const res = Object.assign({}, doc);
11 | Object.keys(def).forEach(key => {
12 | res[key] = expect.any(def[key]);
13 | });
14 | return res;
15 | }
16 |
17 | module.exports = {
18 | equalAtLeast,
19 | addExpectAnyFields
20 | };
21 |
--------------------------------------------------------------------------------
/examples/knex-migration/migrations/20210425191836_init.js:
--------------------------------------------------------------------------------
1 | exports.up = function (knex) {
2 | return knex.schema.createTable("posts", table => {
3 | table.increments("id");
4 | table.string("title");
5 | table.string("content");
6 | table.integer("votes");
7 | table.boolean("status");
8 | table.bigInteger("createdAt");
9 | table.bigInteger("updatedAt");
10 | });
11 | };
12 |
13 | exports.down = function (knex) {
14 | return knex.schema.dropTable("posts");
15 | };
16 |
--------------------------------------------------------------------------------
/src/errors.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @moleculer/database
3 | * Copyright (c) 2022 MoleculerJS (https://github.com/moleculerjs/database)
4 | * MIT Licensed
5 | */
6 |
7 | "use strict";
8 |
9 | const { MoleculerClientError } = require("moleculer").Errors;
10 |
11 | class EntityNotFoundError extends MoleculerClientError {
12 | constructor(id) {
13 | super("Entity not found", 404, "ENTITY_NOT_FOUND", {
14 | id
15 | });
16 | }
17 | }
18 |
19 | module.exports = { EntityNotFoundError };
20 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @moleculer/database
3 | * Copyright (c) 2022 MoleculerJS (https://github.com/moleculerjs/database)
4 | * MIT Licensed
5 | */
6 |
7 | "use strict";
8 |
9 | const Schema = require("./src/schema");
10 |
11 | module.exports = {
12 | Service: require("./src"),
13 | Adapters: require("./src/adapters"),
14 | Errors: require("./src/errors"),
15 | generateValidatorSchemaFromFields: Schema.generateValidatorSchemaFromFields,
16 | generateFieldValidatorSchema: Schema.generateFieldValidatorSchema
17 | };
18 |
--------------------------------------------------------------------------------
/.github/workflows/notification.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: Discord Notification
4 |
5 | on:
6 | release:
7 | types:
8 | - published
9 |
10 | jobs:
11 | notify:
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - name: Discord notification
16 | env:
17 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
18 | uses: Ilshidur/action-discord@master
19 | with:
20 | args: ":tada: **The {{ EVENT_PAYLOAD.repository.name }} {{ EVENT_PAYLOAD.release.tag_name }} has been released.**:tada:\nChangelog: {{EVENT_PAYLOAD.release.html_url}}"
21 |
--------------------------------------------------------------------------------
/benchmark/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | node: true,
4 | commonjs: true,
5 | es6: true
6 | },
7 | extends: ["eslint:recommended"],
8 | parserOptions: {
9 | sourceType: "module",
10 | ecmaVersion: 2017,
11 | ecmaFeatures: {
12 | experimentalObjectRestSpread: true
13 | }
14 | },
15 | rules: {
16 | indent: ["warn", "tab", { SwitchCase: 1 }],
17 | quotes: ["warn", "double"],
18 | semi: ["error", "always"],
19 | "no-var": ["warn"],
20 | "no-console": ["off"],
21 | "no-unused-vars": ["off"],
22 | "no-trailing-spaces": ["error"],
23 | "security/detect-possible-timing-attacks": ["off"]
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 |
9 | # Change these settings to your own preference
10 | indent_style = tab
11 | indent_size = 4
12 | space_after_anon_function = true
13 |
14 | # We recommend you to keep these unchanged
15 | end_of_line = lf
16 | charset = utf-8
17 | trim_trailing_whitespace = true
18 | insert_final_newline = true
19 |
20 | [*.md]
21 | trim_trailing_whitespace = false
22 | indent_style = space
23 | indent_size = 4
24 |
25 | [{package,bower}.json]
26 | indent_style = space
27 | indent_size = 2
28 |
29 | [*.{yml,yaml}]
30 | indent_style = space
31 | indent_size = 2
32 |
33 | [*.js]
34 | quote_type = "double"
35 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | node: true,
4 | commonjs: true,
5 | es6: true,
6 | jquery: false,
7 | jest: true,
8 | jasmine: true
9 | },
10 | extends: ["eslint:recommended", "plugin:security/recommended", "plugin:prettier/recommended"],
11 | parserOptions: {
12 | sourceType: "module",
13 | ecmaVersion: 2018
14 | },
15 | plugins: ["node", "promise", "security"],
16 | rules: {
17 | "no-var": ["error"],
18 | "no-console": ["warn"],
19 | "no-unused-vars": ["warn"],
20 | "no-trailing-spaces": ["error"],
21 | "security/detect-object-injection": ["off"],
22 | "security/detect-non-literal-require": ["off"],
23 | "security/detect-non-literal-fs-filename": ["off"],
24 | "no-process-exit": ["off"],
25 | "node/no-unpublished-require": 0,
26 | "require-atomic-updates": 0,
27 | "object-curly-spacing": ["warn", "always"]
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/test/leak-detection/self-check.spec.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const memwatch = require("@icebob/node-memwatch");
4 |
5 | jest.setTimeout(3 * 60 * 1000); // 3mins
6 |
7 | describe("leak detector", function () {
8 | // let leakCB = jest.fn();
9 | // memwatch.on("leak", leakCB);
10 |
11 | it("should detect a basic leak", done => {
12 | const hd = new memwatch.HeapDiff();
13 | let iterations = 0;
14 | const leaks = [];
15 | const interval = setInterval(() => {
16 | if (iterations >= 10) {
17 | memwatch.gc();
18 | const diff = hd.end();
19 | console.log(diff); // eslint-disable-line no-console
20 | expect(diff.change.size_bytes).toBeGreaterThan(50 * 1024 * 1024);
21 | clearInterval(interval);
22 | return done();
23 | }
24 | iterations++;
25 | for (let i = 0; i < 1000000; i++) {
26 | const str = "leaky string";
27 | leaks.push(str);
28 | }
29 | }, 10);
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/.github/workflows/unit.yml:
--------------------------------------------------------------------------------
1 | name: Unit Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - '**'
7 | paths-ignore:
8 | - '.vscode/**'
9 | - 'benchmark/**'
10 | - 'docs/**'
11 | - 'examples/**'
12 | - '*.md'
13 |
14 | pull_request:
15 | branches:
16 | - master
17 | paths-ignore:
18 | - '.vscode/**'
19 | - 'benchmark/**'
20 | - 'docs/**'
21 | - 'examples/**'
22 | - '*.md'
23 |
24 | jobs:
25 | test:
26 | runs-on: ubuntu-latest
27 |
28 | strategy:
29 | matrix:
30 | node-version: [20.x, 22.x, 24.x]
31 | fail-fast: false
32 | steps:
33 | - uses: actions/checkout@v4
34 |
35 | - name: Use Node.js ${{ matrix.node-version }}
36 | uses: actions/setup-node@v4
37 | with:
38 | node-version: ${{ matrix.node-version }}
39 |
40 | - name: Install dependencies
41 | run: npm ci
42 |
43 | - name: Run unit tests
44 | run: npm run test:unit
45 |
--------------------------------------------------------------------------------
/.github/workflows/dependencies.yml:
--------------------------------------------------------------------------------
1 | name: Update dependencies
2 |
3 | on:
4 | schedule:
5 | - cron: '0 0 1 * *'
6 | workflow_dispatch:
7 | jobs:
8 | update-dependencies:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - name: Use Node.js
13 | uses: actions/setup-node@v4
14 | with:
15 | node-version: '22.x'
16 |
17 | - run: npm ci
18 |
19 | - run: |
20 | git config user.name "GitHub Actions Bot"
21 | git config user.email "hello@moleculer.services"
22 | git checkout -b update-deps-$GITHUB_RUN_ID
23 |
24 | - run: npm run ci-update-deps
25 | - run: npm i
26 | - run: npm audit fix || true
27 |
28 | - run: |
29 | git commit -am "Update dependencies (auto)"
30 | git push origin update-deps-$GITHUB_RUN_ID
31 | - run: |
32 | gh pr create --title "[deps]: Update all dependencies" --body ""
33 | env:
34 | GITHUB_TOKEN: ${{secrets.CI_ACCESS_TOKEN}}
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 MoleculerJS
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/test/unit/utils.spec.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const utils = require("../../src/utils");
4 |
5 | describe("Test Utils", () => {
6 | describe("Test flatten method", () => {
7 | it("should flatten objects", async () => {
8 | const obj = {
9 | name: "John Doe",
10 | email: "john.doe@moleculer.services",
11 | address: {
12 | zip: "1234",
13 | street: "Main Street 15",
14 | city: "London",
15 | country: "England",
16 | extra: "some"
17 | },
18 | roles: ["admin", 1234],
19 | phones: [
20 | { type: "home", number: "+1-555-1234", primary: true },
21 | { type: "mobile", number: "+1-555-9999" }
22 | ]
23 | };
24 |
25 | expect(utils.flatten(obj)).toStrictEqual({
26 | name: "John Doe",
27 | email: "john.doe@moleculer.services",
28 | "address.city": "London",
29 | "address.country": "England",
30 | "address.extra": "some",
31 | "address.street": "Main Street 15",
32 | "address.zip": "1234",
33 | "phones.0.number": "+1-555-1234",
34 | "phones.0.primary": true,
35 | "phones.0.type": "home",
36 | "phones.1.number": "+1-555-9999",
37 | "phones.1.type": "mobile",
38 | "roles.0": "admin",
39 | "roles.1": 1234
40 | });
41 | });
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # [0.3.0](https://github.com/moleculerjs/database/compare/v0.2.1...v0.3.0) (2025-05-31)
4 |
5 | - update dependencies.
6 | - Minimum node version is 20.
7 | - switch from `nedb` to `@seald-io/nedb` to support Node 24+.
8 | -
9 |
10 |
11 | # [0.2.1](https://github.com/moleculerjs/database/compare/v0.2.0...v0.2.1) (2024-07-28)
12 |
13 | - update dependencies
14 | - add moleculer 0.15 peer dependency
15 |
16 |
17 |
18 | # [0.2.0](https://github.com/moleculerjs/database/compare/v0.1.1...v0.2.0) (2024-04-01)
19 |
20 | **Breaking changes**
21 | - upgrade `knex` to 3.1.0
22 | - upgrade `mongodb` to 6.5.0
23 | - minimum Node version bumped to 18
24 |
25 |
26 |
27 | # [0.1.1](https://github.com/moleculerjs/database/compare/v0.1.0...v0.1.1) (2023-04-23)
28 |
29 | - fix permission/permissive type [#31](https://github.com/moleculerjs/moleculer-channels/pull/31)
30 | - fix TypeError `createFromHexString` [#40](https://github.com/moleculerjs/moleculer-channels/pull/40)
31 | - fix waiting for adapter in connecting state [#2d9888e](https://github.com/moleculerjs/database/commit/2d9888e497363ac88aa3b62c354d680d53b3213b)
32 |
33 |
34 |
35 | # v0.1.0 (2022-10-02)
36 |
37 | First public version.
38 |
--------------------------------------------------------------------------------
/benchmark/utils.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const { makeDirs } = require("moleculer").Utils;
4 | const path = require("path");
5 | const fs = require("fs");
6 | const qs = require("qs");
7 |
8 | module.exports = {
9 | slug(text) {
10 | return text
11 | .toString()
12 | .toLowerCase()
13 | .replace(/\s+/g, "-") // Replace spaces with -
14 | .replace(/[^\w\-]+/g, "") // Remove all non-word chars
15 | .replace(/\-\-+/g, "-") // Replace multiple - with single -
16 | .replace(/^-+/, "") // Trim - from start of text
17 | .replace(/-+$/, ""); // Trim - from end of text
18 | },
19 |
20 | writeResult(folderName, filename, content) {
21 | const folder = path.join(__dirname, "results", folderName);
22 | makeDirs(folder);
23 |
24 | fs.writeFileSync(path.join(folder, filename), JSON.stringify(content, null, 2), "utf8");
25 | },
26 |
27 | saveMarkdown(filename, rows) {
28 | const content = rows.join("\n");
29 | fs.writeFileSync(filename, content, "utf8");
30 | },
31 |
32 | createChartURL(opts) {
33 | return `https://image-charts.com/chart?${qs.stringify(opts)}`;
34 | },
35 |
36 | numToStr(num, digits = 2) {
37 | return new Intl.NumberFormat("en-US", { maximumFractionDigits: digits }).format(num);
38 | },
39 |
40 | makeTableRow(cells) {
41 | return "| " + cells.join(" | ") + " |";
42 | }
43 | };
44 |
--------------------------------------------------------------------------------
/.github/workflows/benchmark.yml:
--------------------------------------------------------------------------------
1 | name: Benchmark Test
2 |
3 | on: [workflow_dispatch]
4 |
5 | jobs:
6 | test:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - uses: actions/checkout@v4
11 |
12 | - name: Use Node.js
13 | uses: actions/setup-node@v4
14 | with:
15 | node-version: 22.x
16 |
17 | - name: Install dependencies
18 | run: npm ci
19 |
20 | - name: Start containers
21 | run: docker compose up -d
22 | working-directory: ./test
23 |
24 | - name: Sleeping 30 secs
25 | run: sleep 30
26 |
27 | - name: Check containers
28 | run: docker compose ps
29 | working-directory: ./test
30 |
31 | - name: Run benchmark tests
32 | run: npm run bench
33 | timeout-minutes: 15
34 | env:
35 | GITHUB_ACTIONS_CI: true
36 |
37 | - name: Show container logs (in case of failure)
38 | run: docker compose logs
39 | if: failure()
40 | working-directory: ./test
41 |
42 | - name: Stop containers
43 | run: docker compose down -v
44 | working-directory: ./test
45 |
46 | - name: Commit the result
47 | if: success()
48 | uses: EndBug/add-and-commit@v7 # You can change this to use a specific version
49 | with:
50 | # The arguments for the `git add` command (see the paragraph below for more info)
51 | # Default: '.'
52 | add: './benchmark/results'
53 | default_author: github_actions
54 | push: true
55 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible Node.js debug attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 |
8 | {
9 | "type": "node",
10 | "request": "launch",
11 | "name": "Launch demo",
12 | "program": "${workspaceRoot}/examples/index.js",
13 | "cwd": "${workspaceRoot}",
14 | "args": [
15 | "simple"
16 | ]
17 | },
18 | {
19 | "type": "node",
20 | "request": "launch",
21 | "name": "Launch selected demo",
22 | "program": "${file}",
23 | "cwd": "${workspaceRoot}"
24 | },
25 | {
26 | "type": "node",
27 | "request": "launch",
28 | "name": "Jest",
29 | "program": "${workspaceRoot}/node_modules/jest-cli/bin/jest.js",
30 | "args": ["--testMatch", "\"**/integration/**/*.spec.js\"", "--runInBand"],
31 | "cwd": "${workspaceRoot}",
32 | "runtimeArgs": [
33 | "--nolazy"
34 | ],
35 | "env": {
36 | "ADAPTER": "Knex-Postgresql"
37 | }
38 | },
39 | {
40 | "type": "node",
41 | "request": "launch",
42 | "name": "Jest single",
43 | "program": "${workspaceRoot}/node_modules/jest-cli/bin/jest.js",
44 | "args": ["--runInBand", "${fileBasenameNoExtension}"],
45 | "console": "internalConsole",
46 | "cwd": "${workspaceRoot}",
47 | "runtimeArgs": [
48 | "--nolazy"
49 | ]
50 | },
51 | ]
52 | }
53 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @moleculer/database
3 | * Copyright (c) 2022 MoleculerJS (https://github.com/moleculerjs/database)
4 | * MIT Licensed
5 | */
6 |
7 | "use strict";
8 |
9 | module.exports = {
10 | /**
11 | * Flatten an object.
12 | * Credits: "flat" library
13 | *
14 | * @param {Object} target
15 | * @param {Object?} opts
16 | * @returns {Object}
17 | */
18 | flatten: (target, opts) => {
19 | opts = opts || {};
20 |
21 | const delimiter = opts.delimiter || ".";
22 | const maxDepth = opts.maxDepth;
23 | const transformKey = opts.transformKey || (key => key);
24 | const output = {};
25 |
26 | const step = (object, prev, currentDepth) => {
27 | currentDepth = currentDepth || 1;
28 | Object.keys(object).forEach(key => {
29 | const value = object[key];
30 | const isarray = opts.safe && Array.isArray(value);
31 | const type = Object.prototype.toString.call(value);
32 | const isbuffer = Buffer.isBuffer(value);
33 | const isobject = type === "[object Object]" || type === "[object Array]";
34 |
35 | const newKey = prev ? prev + delimiter + transformKey(key) : transformKey(key);
36 |
37 | if (
38 | !isarray &&
39 | !isbuffer &&
40 | isobject &&
41 | Object.keys(value).length &&
42 | (!opts.maxDepth || currentDepth < maxDepth)
43 | ) {
44 | return step(value, newKey, currentDepth + 1);
45 | }
46 |
47 | output[newKey] = value;
48 | });
49 | };
50 |
51 | step(target);
52 |
53 | return output;
54 | }
55 | };
56 |
--------------------------------------------------------------------------------
/src/adapters/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @moleculer/database
3 | * Copyright (c) 2022 MoleculerJS (https://github.com/moleculerjs/database)
4 | * MIT Licensed
5 | */
6 |
7 | "use strict";
8 |
9 | const { isObject, isString } = require("lodash");
10 | const { ServiceSchemaError } = require("moleculer").Errors;
11 |
12 | const Adapters = {
13 | Base: require("./base"),
14 | Knex: require("./knex"),
15 | MongoDB: require("./mongodb"),
16 | NeDB: require("./nedb")
17 | };
18 |
19 | function getByName(name) {
20 | if (!name) return null;
21 |
22 | let n = Object.keys(Adapters).find(n => n.toLowerCase() == name.toLowerCase());
23 | if (n) return Adapters[n];
24 | }
25 |
26 | /**
27 | * Resolve adapter by name
28 | *
29 | * @param {object|string} opt
30 | * @returns {Adapter}
31 | */
32 | function resolve(opt) {
33 | if (opt instanceof Adapters.Base) {
34 | return opt;
35 | } else if (isString(opt)) {
36 | const AdapterClass = getByName(opt);
37 | if (AdapterClass) {
38 | return new AdapterClass();
39 | } else {
40 | throw new ServiceSchemaError(`Invalid Adapter type '${opt}'.`, { type: opt });
41 | }
42 | } else if (isObject(opt)) {
43 | const AdapterClass = getByName(opt.type || "NeDB");
44 | if (AdapterClass) {
45 | return new AdapterClass(opt.options);
46 | } else {
47 | throw new ServiceSchemaError(`Invalid Adapter type '${opt.type}'.`, {
48 | type: opt.type
49 | });
50 | }
51 | }
52 |
53 | return new Adapters.NeDB();
54 | }
55 |
56 | function register(name, value) {
57 | Adapters[name] = value;
58 | }
59 |
60 | module.exports = Object.assign(Adapters, { resolve, register });
61 |
--------------------------------------------------------------------------------
/examples/connect/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | "use strict";
3 |
4 | /**
5 | * It's an example to test the connect/disconnect logic of adapters.
6 | */
7 |
8 | const { ServiceBroker } = require("moleculer");
9 | const { inspect } = require("util");
10 | const DbService = require("../../index").Service;
11 |
12 | // Create broker
13 | const broker = new ServiceBroker({
14 | logger: {
15 | type: "Console",
16 | options: {
17 | level: {
18 | POSTS: "debug",
19 | "*": "info"
20 | },
21 | objectPrinter: obj =>
22 | inspect(obj, {
23 | breakLength: 50,
24 | colors: true,
25 | depth: 3
26 | })
27 | }
28 | }
29 | });
30 |
31 | // Create a service
32 | broker.createService({
33 | name: "posts",
34 | mixins: [
35 | DbService({
36 | adapter: {
37 | type: "MongoDB"
38 | }
39 | })
40 | ],
41 |
42 | settings: {
43 | fields: {
44 | id: { type: "string", primaryKey: true, columnName: "_id" },
45 | title: {
46 | type: "string",
47 | max: 255,
48 | trim: true,
49 | required: true
50 | },
51 | content: { type: "string" },
52 | votes: { type: "number", integer: true, min: 0, default: 0, columnType: "int" },
53 | status: { type: "boolean", default: true }
54 | }
55 | },
56 |
57 | async started() {
58 | this.logger.info("Creating multiple adapters...");
59 | const adapters = await Promise.all([
60 | this.getAdapter(),
61 | this.getAdapter(),
62 | this.getAdapter()
63 | ]);
64 | this.logger.info(
65 | "Adapters created.",
66 | adapters.map(a => a.constructor.name)
67 | );
68 | }
69 | });
70 |
71 | // Start server
72 | broker
73 | .start()
74 | .then(() => broker.repl())
75 | .catch(err => {
76 | broker.logger.error(err);
77 | process.exit(1);
78 | });
79 |
--------------------------------------------------------------------------------
/examples/knex-migration/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /**
4 | * This example demonstrates how to use the Knex
5 | * migration with this service.
6 | * The migrations scripts are in the "migration" folder.
7 | */
8 |
9 | const { ServiceBroker } = require("moleculer");
10 | const DbService = require("../../index").Service;
11 |
12 | const broker = new ServiceBroker();
13 |
14 | broker.createService({
15 | name: "posts",
16 | mixins: [
17 | DbService({
18 | adapter: {
19 | type: "Knex",
20 | options: {
21 | knex: {
22 | client: "sqlite3",
23 | connection: {
24 | filename: "posts.db"
25 | },
26 | useNullAsDefault: true
27 | }
28 | }
29 | }
30 | })
31 | ],
32 |
33 | settings: {
34 | fields: {
35 | id: { type: "number", primaryKey: true },
36 | title: {
37 | type: "string",
38 | max: 255,
39 | trim: true,
40 | required: true
41 | },
42 | content: { type: "string" },
43 | author: { type: "number" },
44 | votes: { type: "number", integer: true, min: 0, default: 0 },
45 | status: { type: "boolean", default: true },
46 | createdAt: { type: "number", readonly: true, onCreate: () => Date.now() },
47 | updatedAt: { type: "number", readonly: true, onUpdate: () => Date.now() }
48 | }
49 | },
50 |
51 | async started() {
52 | const adapter = await this.getAdapter();
53 |
54 | // Migrate to the latest version.
55 | await adapter.client.migrate.latest();
56 | }
57 | });
58 |
59 | // Start server
60 | broker
61 | .start()
62 | .then(async () => {
63 | broker.repl();
64 | // Create a new post
65 | /*let post = await broker.call("posts.create", {
66 | title: "My first post",
67 | content: "Content of my first post..."
68 | });*/
69 |
70 | // Get all posts
71 | let posts = await broker.call("posts.find");
72 | console.log("Find:", posts);
73 | })
74 | .catch(err => broker.logger.error(err));
75 |
--------------------------------------------------------------------------------
/examples/global-pool/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /**
4 | * Global pool handling.
5 | *
6 | * As all services are loaded to the same broker and all services use the same adapter,
7 | * the service only establish one connection and all services use this one.
8 | * The connection remains open even if a service is destroyed. Only the last service destroy
9 | * will close the connection.
10 | */
11 |
12 | const { ServiceBroker } = require("moleculer");
13 | const DbService = require("../../index").Service;
14 |
15 | const broker = new ServiceBroker();
16 |
17 | const adapter = {
18 | type: "MongoDB",
19 | options: "mongodb://127.0.0.1:27017/example-pool"
20 | };
21 |
22 | const fields = {
23 | id: { type: "string", primaryKey: true, columnName: "_id" },
24 | title: "string"
25 | };
26 |
27 | broker.createService({
28 | name: "posts",
29 | mixins: [DbService({ adapter })],
30 | settings: { fields }
31 | });
32 | broker.createService({
33 | name: "users",
34 | mixins: [DbService({ adapter })],
35 | settings: { fields }
36 | });
37 | broker.createService({
38 | name: "comments",
39 | mixins: [DbService({ adapter })],
40 | settings: { fields }
41 | });
42 | broker.createService({
43 | name: "likes",
44 | mixins: [DbService({ adapter })],
45 | settings: { fields }
46 | });
47 |
48 | // Start server
49 | broker
50 | .start()
51 | .then(() => broker.repl())
52 | .then(async () => {
53 | await broker.call("posts.list");
54 | await broker.call("users.list");
55 | await broker.call("comments.list");
56 | await broker.call("likes.list");
57 |
58 | await broker.Promise.delay(5000);
59 |
60 | await broker.destroyService("posts");
61 | await broker.Promise.delay(1000);
62 | await broker.call("users.list");
63 | await broker.destroyService("users");
64 | await broker.Promise.delay(1000);
65 | await broker.destroyService("comments");
66 | await broker.Promise.delay(1000);
67 | await broker.destroyService("likes");
68 | })
69 | .catch(err => broker.logger.error(err));
70 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | # TODO
2 |
3 | - [ ] **More integration tests**
4 | - [ ] createIndex, removeIndex
5 | - [ ] Knex createCursor operators `$`
6 | - [ ] global pool handling
7 | - [x] soft delete
8 |
9 | ## Actions
10 | - [ ] `aggregate` action with params: `type: "sum", "avg", "count", "min", "max"` & `field: "price"`
11 |
12 | ## Fields
13 | - [ ] `hidden: "inLists"`
14 | - [ ] using projections, get only required fields in adapters.
15 | - [ ] `projection: []` for getter/setters
16 |
17 | ## Methods
18 | - [x] wrap the args to obj in custom functions: `({ ctx, id, field })`
19 | - [ ] same for `get`
20 | - [ ] multiple get option for `get` in transform (same as populating to avoid hundreds sub-calls)
21 | - [x] if `field.validate` is string, call the method by name
22 | - [ ] same for `get`
23 | - [x] same for `set`
24 | - [ ] option to disable validators `opts.validation: false`
25 | - [x] add `scope` param for `removeEntities` and `updateEntities` and pass to the `findEntities` call inside the method.
26 | - [x] skipping softDelete in methods `opts.softDelete: false` to make real delete in maintenance processes
27 | - [ ] Add `events: false` to options to disable entity changed events. (e.g. when `softDelete: false` we don't want to send event about changes)
28 |
29 |
30 | ## Just if I bored to death
31 | - [ ] generate OpenAPI schema
32 | - [ ] `introspect: true`
33 | - [ ] create REST API to get the field definitions, and actions with params
34 | - [ ] Generate sample data based on fields with Fakerator. `username: { type: "string", fake: "entity.user.userName" }`
35 | - [ ] validate `raw: true` update fields (`$set`, `$inc`)
36 | - [ ] client-side (Vue) module which can communicate with service via REST or GraphQL.
37 | - [ ] ad-hoc populate in find/list actions `populate: ["author", { key: "createdBy", action: "users.resolve", fields: ["name", "avatar"] }]` { }
38 | - [ ] auto revision handling (`_rev: 1`). Autoincrement on every update/replace and always checks the rev value to avoid concurrent updating.
39 | - [ ]
40 |
--------------------------------------------------------------------------------
/examples/multi-tenants/plenty.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /**
4 | * In collection-based or database-based multi-tenant mode each tenant
5 | * opens a DB connection to the own database/collection. It can generates
6 | * a lot of connections. This example demonstrates how to work the `maximumAdapters`
7 | * mixin option which limits the number of connections.
8 | */
9 |
10 | const _ = require("lodash");
11 | const { ServiceBroker } = require("moleculer");
12 | const DbService = require("../../index").Service;
13 |
14 | // Create broker
15 | const broker = new ServiceBroker();
16 |
17 | // Create a service
18 | const svc = broker.createService({
19 | name: "posts",
20 | mixins: [DbService({ maximumAdapters: 5 })],
21 | settings: {
22 | fields: {
23 | title: { type: "string", required: true },
24 | content: { type: "string", required: false },
25 | tenantId: {
26 | type: "number",
27 | set: ({ ctx }) => ctx.meta.tenantId,
28 | required: true
29 | }
30 | },
31 | scopes: {
32 | tenant(q, ctx) {
33 | const tenantId = ctx.meta.tenantId;
34 | if (!tenantId) throw new Error("Missing tenantId!");
35 |
36 | q.tenantId = ctx.meta.tenantId;
37 | return q;
38 | }
39 | },
40 | defaultScopes: ["tenant"]
41 | },
42 | methods: {
43 | getAdapterByContext(ctx) {
44 | const tenantId = ctx.meta.tenantId;
45 | if (!tenantId) throw new Error("Missing tenantId!");
46 |
47 | return [
48 | tenantId,
49 | /*{
50 | type: "NeDB",
51 | options: __dirname + `/posts/${_.padStart(tenantId, 4, "0")}.db`
52 | }*/
53 | {
54 | type: "MongoDB",
55 | options: {
56 | uri: "mongodb://127.0.0.1:27017",
57 | dbName: `tenant-posts`,
58 | collection: `posts-${tenantId}`
59 | }
60 | }
61 | ];
62 | }
63 | },
64 | async started() {}
65 | });
66 |
67 | // Start server
68 | let count = 0;
69 | broker.start().then(async () => {
70 | setInterval(async () => {
71 | const tenantId = 1 + Math.floor(Math.random() * 10);
72 |
73 | await broker.call(
74 | "posts.create",
75 | { title: `New post ${++count}` },
76 | {
77 | meta: { tenantId }
78 | }
79 | );
80 |
81 | console.log("TenantId:", tenantId, " Adapters:", svc.adapters.size);
82 | }, 2000);
83 | });
84 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
106 | # Sqlite DB files
107 | *.db-journal
108 | *.sqlite3
109 | *.sqlite3-journal
110 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | module.exports = {
4 | METRIC_ADAPTER_TOTAL: "moleculer.database.adapter.total",
5 | METRIC_ADAPTER_ACTIVE: "moleculer.database.adapter.active",
6 |
7 | METRIC_ENTITIES_FIND_TOTAL: "moleculer.database.entities.find.total",
8 | METRIC_ENTITIES_FIND_TIME: "moleculer.database.entities.find.time",
9 |
10 | METRIC_ENTITIES_STREAM_TOTAL: "moleculer.database.entities.stream.total",
11 | METRIC_ENTITIES_STREAM_TIME: "moleculer.database.entities.stream.time",
12 |
13 | METRIC_ENTITIES_COUNT_TOTAL: "moleculer.database.entities.count.total",
14 | METRIC_ENTITIES_COUNT_TIME: "moleculer.database.entities.count.time",
15 |
16 | METRIC_ENTITIES_FINDONE_TOTAL: "moleculer.database.entities.find-one.total",
17 | METRIC_ENTITIES_FINDONE_TIME: "moleculer.database.entities.find-one.time",
18 |
19 | METRIC_ENTITIES_RESOLVE_TOTAL: "moleculer.database.entities.resolve.total",
20 | METRIC_ENTITIES_RESOLVE_TIME: "moleculer.database.entities.resolve.time",
21 |
22 | METRIC_ENTITIES_CREATEONE_TOTAL: "moleculer.database.entities.create-one.total",
23 | METRIC_ENTITIES_CREATEONE_TIME: "moleculer.database.entities.create-one.time",
24 |
25 | METRIC_ENTITIES_CREATEMANY_TOTAL: "moleculer.database.entities.create-many.total",
26 | METRIC_ENTITIES_CREATEMANY_TIME: "moleculer.database.entities.create-many.time",
27 |
28 | METRIC_ENTITIES_UPDATEONE_TOTAL: "moleculer.database.entities.update-one.total",
29 | METRIC_ENTITIES_UPDATEONE_TIME: "moleculer.database.entities.update-one.time",
30 |
31 | METRIC_ENTITIES_UPDATEMANY_TOTAL: "moleculer.database.entities.update-many.total",
32 | METRIC_ENTITIES_UPDATEMANY_TIME: "moleculer.database.entities.update-many.time",
33 |
34 | METRIC_ENTITIES_REPLACEONE_TOTAL: "moleculer.database.entities.replace-one.total",
35 | METRIC_ENTITIES_REPLACEONE_TIME: "moleculer.database.entities.replace-one.time",
36 |
37 | METRIC_ENTITIES_REMOVEONE_TOTAL: "moleculer.database.entities.remove-one.total",
38 | METRIC_ENTITIES_REMOVEONE_TIME: "moleculer.database.entities.remove-one.time",
39 |
40 | METRIC_ENTITIES_REMOVEMANY_TOTAL: "moleculer.database.entities.remove-many.total",
41 | METRIC_ENTITIES_REMOVEMANY_TIME: "moleculer.database.entities.remove-many.time",
42 |
43 | METRIC_ENTITIES_CLEAR_TOTAL: "moleculer.database.entities.clear.total",
44 | METRIC_ENTITIES_CLEAR_TIME: "moleculer.database.entities.clear.time"
45 | };
46 |
--------------------------------------------------------------------------------
/test/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.3"
2 |
3 | services:
4 |
5 | mongo:
6 | image: mongo:4
7 | ports:
8 | - "27017:27017"
9 |
10 | postgres:
11 | image: postgres:12
12 | ports:
13 | - "5432:5432"
14 | environment:
15 | - POSTGRES_PASSWORD=moleculer
16 | healthcheck:
17 | test: /usr/bin/psql postgres://postgres:moleculer@localhost/postgres -c "SELECT 1"
18 | volumes:
19 | - ./scripts/pg-create-databases.sql:/docker-entrypoint-initdb.d/pg-create-databases.sql
20 |
21 | # pgadmin:
22 | # image: dpage/pgadmin4
23 | # environment:
24 | # PGADMIN_DEFAULT_EMAIL: "admin@example.com"
25 | # PGADMIN_DEFAULT_PASSWORD: "admin"
26 | # ports:
27 | # - "8080:80"
28 | # depends_on:
29 | # - postgres
30 |
31 | mysql:
32 | image: mysql:8.3
33 | ports:
34 | - "3306:3306"
35 | environment:
36 | - MYSQL_ROOT_PASSWORD=moleculer
37 | - MYSQL_DATABASE=db_int_test
38 | healthcheck:
39 | test: /usr/bin/mysql -hlocalhost -uroot -pmoleculer -e "SELECT 1"
40 | command: --default-authentication-plugin=mysql_native_password
41 | volumes:
42 | - ./scripts/mysql-create-databases.sql:/docker-entrypoint-initdb.d/mysql-create-databases.sql
43 |
44 | # adminer:
45 | # image: adminer
46 | # ports:
47 | # - 8081:8080
48 |
49 | mssql:
50 | image: mcr.microsoft.com/mssql/server:2022-latest
51 | ports:
52 | - '1433:1433'
53 | environment:
54 | - ACCEPT_EULA=Y
55 | - MSSQL_PID=Developer
56 | - SA_PASSWORD=Moleculer@Pass1234
57 | healthcheck:
58 | test:
59 | [
60 | 'CMD',
61 | '/opt/mssql-tools18/bin/sqlcmd',
62 | '-S',
63 | 'localhost',
64 | '-No',
65 | '-U',
66 | 'SA',
67 | '-P',
68 | 'Moleculer@Pass1234',
69 | '-l',
70 | '30',
71 | '-Q',
72 | 'SELECT 1',
73 | ]
74 | interval: 3s
75 | timeout: 1s
76 | retries: 10
77 |
78 | mssql-create-db:
79 | image: mcr.microsoft.com/mssql/server:2022-latest
80 | links:
81 | - mssql
82 | depends_on:
83 | - mssql
84 | entrypoint:
85 | - bash
86 | - -c
87 | # https://docs.microsoft.com/en-us/sql/relational-databases/logs/control-transaction-durability?view=sql-server-ver15#bkmk_DbControl
88 | - 'until /opt/mssql-tools18/bin/sqlcmd -C -S mssql -U sa -P Moleculer@Pass1234 -d master -No -Q "CREATE DATABASE bench_test; CREATE DATABASE db_int_test; CREATE DATABASE db_int_posts_1000; CREATE DATABASE db_int_posts_1001; CREATE DATABASE db_int_posts_1002; CREATE DATABASE db_int_posts_1003;"; do sleep 5; done'
89 |
--------------------------------------------------------------------------------
/docs/adapters/MongoDB.md:
--------------------------------------------------------------------------------
1 | # MongoDB adapter
2 | This adapter gives access to MongoDB databases. It uses the official [`mongodb`](https://docs.mongodb.com/drivers/node/current/) library.
3 |
4 | ## Install
5 | This module contains the source code of the adapter. You just need to install the dependent library.
6 |
7 | ```bash
8 | npm install mongodb@^4.1.4
9 | ```
10 |
11 | ## Usage
12 |
13 | ### Use the default localhost URI
14 | If you not define any options, the adapter uses the `"mongodb://127.0.0.1:27017"` connection string.
15 |
16 | ```js
17 | // posts.service.js
18 | const DbService = require("@moleculer/database").Service;
19 |
20 | module.exports = {
21 | name: "posts",
22 | mixins: [DbService({
23 | adapter: { type: "MongoDB" }
24 | })]
25 | }
26 | ```
27 |
28 | ## With options
29 | ```js
30 | // posts.service.js
31 | const DbService = require("@moleculer/database").Service;
32 |
33 | module.exports = {
34 | name: "posts",
35 | mixins: [DbService({
36 | adapter: {
37 | type: "MongoDB",
38 | options: {
39 | uri: "mongodb+srv://server_name:27017/?maxPoolSize=20",
40 | mongoClientOptions: {
41 | auth: {
42 | username: "user",
43 | password: "secret"
44 | }
45 | }
46 | }
47 | }
48 | })]
49 | }
50 | ```
51 |
52 | ## Options
53 | | Property | Type | Default | Description |
54 | | -------- | ---- | ------- | ----------- |
55 | | `uri` | `String` | `"mongodb://127.0.0.1:27017"` | MongoDB connection URI. |
56 | | `mongoClientOptions` | `Object` | `null` | Available options: https://mongodb.github.io/node-mongodb-native/4.1/interfaces/MongoClientOptions.html |
57 | | `dbOptions` | `Object` | `null` | Available options: https://mongodb.github.io/node-mongodb-native/4.1/interfaces/DbOptions.html |
58 |
59 |
60 | ## Raw update
61 | If you want to update entity and using raw changes, use the [`updateEntity`](../README.md#updateentity) method with `{ raw: true }` options. In this case, you can use [MongoDB update operators](https://docs.mongodb.com/manual/reference/operator/update/) in the `params` parameter.
62 |
63 | ### Example
64 | ```js
65 | const row = await this.updateEntity(ctx, {
66 | id: "YVdnh5oQCyEIRja0",
67 |
68 | $set: {
69 | status: false,
70 | height: 192
71 | },
72 | $inc: {
73 | age: 1
74 | },
75 | $unset: {
76 | dob: true
77 | }
78 | }, { raw: true });
79 | ```
80 |
81 | ## Additional methods
82 |
83 | ### `stringToObjectID`
84 | `stringToObjectID(id: any): ObjectID|any`
85 |
86 | This method convert the `id` parameter to `ObjectID` if the `id` is `String` as a valid `ObjectID` hex string. Otherwise returns the intact `id` value.
87 |
88 | ### `objectIDToString`
89 | `objectIDToString(id: ObjectID): String`
90 |
91 | This method convert the `id` parameter which is an `ObjectID` to `String`.
92 |
--------------------------------------------------------------------------------
/.github/workflows/integration.yml:
--------------------------------------------------------------------------------
1 | name: Integration Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - '**'
7 | paths-ignore:
8 | - '.vscode/**'
9 | - 'benchmark/**'
10 | - 'docs/**'
11 | - 'examples/**'
12 | - '*.md'
13 |
14 | pull_request:
15 | branches:
16 | - master
17 | paths-ignore:
18 | - '.vscode/**'
19 | - 'benchmark/**'
20 | - 'docs/**'
21 | - 'examples/**'
22 | - '*.md'
23 |
24 | jobs:
25 | test:
26 | runs-on: ubuntu-latest
27 |
28 | strategy:
29 | matrix:
30 | node-version: [20.x, 22.x, 24.x]
31 | adapter: ["NeDB", "MongoDB", "Knex-SQLite", "Knex-Postgresql", "Knex-MySQL", "Knex-MySQL2", "Knex-MSSQL"]
32 | fail-fast: false
33 | steps:
34 | - uses: actions/checkout@v4
35 |
36 | - name: Use Node.js ${{ matrix.node-version }}
37 | uses: actions/setup-node@v4
38 | with:
39 | node-version: ${{ matrix.node-version }}
40 |
41 | - name: Install dependencies
42 | run: npm ci
43 |
44 | - name: Start containers for ${{ matrix.adapter }} adapter
45 | run: docker compose up -d mongo && sleep 30
46 | if: ${{ matrix.adapter == 'MongoDB' }}
47 | working-directory: ./test
48 |
49 | - name: Start containers for ${{ matrix.adapter }} adapter
50 | run: docker compose up -d postgres && sleep 30
51 | if: ${{ matrix.adapter == 'Knex-Postgresql' }}
52 | working-directory: ./test
53 |
54 | - name: Start containers for ${{ matrix.adapter }} adapter
55 | run: docker compose up -d mysql && sleep 30
56 | if: ${{ matrix.adapter == 'Knex-MySQL' || matrix.adapter == 'Knex-MySQL2' }}
57 | working-directory: ./test
58 |
59 | - name: Start containers for ${{ matrix.adapter }} adapter
60 | run: docker compose up -d mssql mssql-create-db && sleep 60
61 | if: ${{ matrix.adapter == 'Knex-MSSQL' }}
62 | working-directory: ./test
63 |
64 | - name: Check containers
65 | run: docker compose ps
66 | working-directory: ./test
67 |
68 | - name: Check logs
69 | run: docker compose logs
70 | working-directory: ./test
71 |
72 | - name: Run integration tests
73 | run: npm run test:integration
74 | timeout-minutes: 10
75 | env:
76 | GITHUB_ACTIONS_CI: true
77 | ADAPTER: ${{ matrix.adapter }}
78 |
79 | # - name: Run leak detection tests
80 | # run: npm run test:leak
81 | # env:
82 | # GITHUB_ACTIONS_CI: true
83 |
84 | - name: Show container logs (in case of failure)
85 | run: docker compose logs
86 | if: failure()
87 | working-directory: ./test
88 |
89 | - name: Stop containers
90 | run: docker compose down -v
91 | working-directory: ./test
92 |
93 | # - name: Upload code coverage
94 | # run: npm run coverall
95 | # if: success() && github.ref == 'refs/heads/master'
96 | # env:
97 | # COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
98 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@moleculer/database",
3 | "version": "0.3.0",
4 | "description": "Advanced Database Access Service for Moleculer microservices framework",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "nodemon examples/index.js",
8 | "ci:unit": "jest --testMatch \"**/unit/**/*.spec.js\" --watch",
9 | "ci:integration": "jest --testMatch \"**/integration/**/*.spec.js\" --runInBand --watch",
10 | "ci:leak": "jest --testMatch \"**/leak-detection/**/index.spec.js\" --runInBand --watch",
11 | "test:unit": "jest --testMatch \"**/unit/**/*.spec.js\"",
12 | "test:integration": "jest --testMatch \"**/integration/**/*.spec.js\" --runInBand --coverage",
13 | "test:leak": "jest --testMatch \"**/leak-detection/**/*.spec.js\" --runInBand",
14 | "test": "jest --testMatch \"**/*.spec.js\" --runInBand --coverage",
15 | "lint": "eslint --ext=.js src examples test",
16 | "bench": "node benchmark/index.js",
17 | "bench:watch": "nodemon benchmark/index.js",
18 | "deps": "npm-check -u && npm audit fix",
19 | "ci-update-deps": "ncu -u --target minor",
20 | "release": "npm publish --access public && git push --tags"
21 | },
22 | "keywords": [
23 | "moleculer",
24 | "microservice"
25 | ],
26 | "repository": {
27 | "type": "git",
28 | "url": "https://github.com/moleculerjs/database.git"
29 | },
30 | "author": "MoleculerJS",
31 | "license": "MIT",
32 | "peerDependencies": {
33 | "moleculer": "^0.14.12 || ^0.15.0-0"
34 | },
35 | "devDependencies": {
36 | "@seald-io/nedb": "^4.1.1",
37 | "@vscode/sqlite3": "^5.1.2",
38 | "axios": "^1.9.0",
39 | "benchmarkify": "^4.0.0",
40 | "eslint": "^8.57.0",
41 | "eslint-config-prettier": "^9.1.0",
42 | "eslint-plugin-node": "^11.1.0",
43 | "eslint-plugin-prettier": "^5.4.1",
44 | "eslint-plugin-promise": "^6.6.0",
45 | "eslint-plugin-security": "^2.1.1",
46 | "fakerator": "^0.3.6",
47 | "globby": "^13.2.2",
48 | "jest": "^29.7.0",
49 | "jest-cli": "^29.7.0",
50 | "kleur": "^4.1.5",
51 | "knex": "^3.1.0",
52 | "moleculer": "^0.14.35",
53 | "moleculer-repl": "^0.7.4",
54 | "moleculer-web": "^0.10.8",
55 | "mongodb": "^6.16.0",
56 | "mysql": "^2.18.1",
57 | "mysql2": "^3.14.1",
58 | "nodemon": "^3.1.10",
59 | "npm-check-updates": "^16.14.20",
60 | "pg": "^8.16.0",
61 | "prettier": "^3.5.3",
62 | "qs": "^6.14.0",
63 | "sequelize": "^6.37.7",
64 | "tedious": "^18.6.2"
65 | },
66 | "jest": {
67 | "testEnvironment": "node",
68 | "rootDir": "./src",
69 | "roots": [
70 | "../test"
71 | ],
72 | "coverageDirectory": "../coverage",
73 | "coveragePathIgnorePatterns": [
74 | "/node_modules/",
75 | "/test/services/"
76 | ]
77 | },
78 | "engines": {
79 | "node": ">= 20.x.x"
80 | },
81 | "dependencies": {
82 | "fastest-validator": "^1.19.1",
83 | "lodash": "^4.17.21",
84 | "semver": "^7.7.2",
85 | "sqlite3": "^5.1.7"
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/examples/many/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /**
4 | * This example created a lot of (1000) entities
5 | * and measure the exection times.
6 | */
7 |
8 | const { ServiceBroker } = require("moleculer");
9 | const DbService = require("../../index").Service;
10 | const path = require("path");
11 | const fs = require("fs");
12 |
13 | const Fakerator = require("fakerator");
14 | const fakerator = new Fakerator();
15 |
16 | // Create broker
17 | const broker = new ServiceBroker();
18 |
19 | const sqliteFilename = path.join(__dirname, "db.sqlite3");
20 |
21 | if (fs.existsSync(sqliteFilename)) fs.unlinkSync(sqliteFilename);
22 | if (fs.existsSync(sqliteFilename + "-journal")) fs.unlinkSync(sqliteFilename + "-journal");
23 |
24 | // Create a service
25 | const svc = broker.createService({
26 | name: "posts",
27 | mixins: [
28 | DbService({
29 | adapter: {
30 | //type: "MongoDB"
31 | type: "Knex",
32 | options: {
33 | knex: {
34 | client: "sqlite3",
35 | connection: {
36 | filename: sqliteFilename
37 | },
38 | useNullAsDefault: true,
39 | pool: {
40 | min: 1,
41 | max: 1
42 | },
43 | log: {
44 | warn(message) {},
45 | error(message) {},
46 | deprecate(message) {},
47 | debug(message) {}
48 | }
49 | }
50 | }
51 | }
52 | })
53 | ],
54 |
55 | settings: {
56 | fields: {
57 | id: { type: "string", primaryKey: true, columnName: "_id" },
58 | title: {
59 | type: "string",
60 | max: 255,
61 | trim: true,
62 | required: true
63 | },
64 | content: { type: "string" },
65 | votes: { type: "number", integer: true, min: 0, default: 0, columnType: "integer" },
66 | status: { type: "boolean", default: true },
67 | createdAt: {
68 | type: "number",
69 | readonly: true,
70 | onCreate: () => Date.now(),
71 | columnType: "real"
72 | },
73 | updatedAt: {
74 | type: "number",
75 | readonly: true,
76 | onUpdate: () => Date.now(),
77 | columnType: "real"
78 | }
79 | }
80 | },
81 | async started() {
82 | const adapter = await this.getAdapter();
83 | await adapter.createTable();
84 |
85 | await this.clearEntities();
86 | }
87 | });
88 |
89 | // Start server
90 | broker
91 | .start()
92 | .then(async () => {
93 | // Create posts
94 |
95 | const COUNT = 1000;
96 | const POSTS = fakerator.times(fakerator.entity.post, COUNT);
97 |
98 | const span = broker.tracer.startSpan("create posts");
99 |
100 | /*for (const post of POSTS) {
101 | await broker.call("posts.create", post);
102 | }*/
103 | const res = await broker.call("posts.createMany", POSTS);
104 | span.finish();
105 |
106 | //broker.logger.info("Inserted result", res);
107 | broker.logger.info(`Created ${COUNT} posts. Time:`, span.duration.toFixed(2), "ms");
108 |
109 | // List posts with pagination
110 | const posts = await broker.call("posts.list", {
111 | fields: ["id", "title", "votes", "status"]
112 | });
113 | broker.logger.info("List:", posts);
114 | })
115 | .catch(err => broker.logger.error(err));
116 |
--------------------------------------------------------------------------------
/benchmark/generate-result.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const fs = require("fs");
4 | const path = require("path");
5 | const globby = require("globby");
6 | const humanize = require("tiny-human-time");
7 | const { saveMarkdown, createChartURL, numToStr, makeTableRow } = require("./utils");
8 |
9 | async function generateMarkdown(folderName) {
10 | const folder = path.join(__dirname, "results", folderName);
11 | const files = await globby(["*.json"], { cwd: folder });
12 | if (files.length == 0) return;
13 |
14 | console.log("Found files:", files);
15 |
16 | const results = files
17 | .map(filename => fs.readFileSync(path.join(folder, filename), "utf8"))
18 | .map(content => JSON.parse(content));
19 |
20 | const rows = ["", ""];
21 |
22 | for (const result of results) {
23 | console.log("Process test:", result.name);
24 |
25 | rows.push(`# ${result.name}`);
26 | rows.push(`${result.description || {}}`);
27 |
28 | if (result.meta.adapters) {
29 | rows.push(
30 | "## Test configurations",
31 | "",
32 | "| Name | Adapter | Options |",
33 | "| ---- | ------- | ------- |"
34 | );
35 | for (const adapter of result.meta.adapters) {
36 | let options = JSON.stringify(adapter.options);
37 | if (options) {
38 | //options = options.replace(/\n/g, "
").replace(/ /g, " ");
39 | options = "`" + options + "`";
40 | }
41 | rows.push(
42 | makeTableRow([adapter.name || adapter.type, adapter.type, options || "-"])
43 | );
44 | }
45 | }
46 |
47 | for (const suite of result.suites) {
48 | rows.push(`## ${suite.name}`);
49 | if (suite.meta && suite.meta.description) rows.push(suite.meta.description);
50 | rows.push("", "### Result");
51 | rows.push("");
52 |
53 | rows.push(
54 | "",
55 | "| Adapter config | Time | Diff | ops/sec |",
56 | "| -------------- | ----:| ----:| -------:|"
57 | );
58 |
59 | suite.tests.forEach(test => {
60 | rows.push(
61 | makeTableRow([
62 | test.name,
63 | humanize.short(test.stat.avg * 1000),
64 | numToStr(test.stat.percent) + "%",
65 | numToStr(test.stat.rps)
66 | ])
67 | );
68 | });
69 | rows.push("");
70 |
71 | rows.push(
72 | "`,
76 | chf: "b0,lg,90,03a9f4,0,3f51b5,1",
77 | chg: "0,50",
78 | chma: "0,0,10,10",
79 | cht: "bvs",
80 | chxt: "x,y",
81 | chxs: "0,333,10|1,333,10",
82 |
83 | chxl: "0:|" + suite.tests.map(s => s.name).join("|"),
84 | chd: "a:" + suite.tests.map(s => s.stat.rps).join(",")
85 | }) +
86 | ")"
87 | );
88 | rows.push("");
89 | }
90 | }
91 |
92 | rows.push("--------------------");
93 | rows.push(`_Generated at ${new Date().toISOString()}_`);
94 |
95 | const filename = path.join(folder, "README.md");
96 | console.log("Write to file...");
97 | saveMarkdown(filename, rows);
98 |
99 | console.log("Done. Result:", filename);
100 | }
101 |
102 | module.exports = { generateMarkdown };
103 |
104 | //generateMarkdown("common");
105 |
--------------------------------------------------------------------------------
/docs/adapters/NeDB.md:
--------------------------------------------------------------------------------
1 | # NeDB adapter
2 | The NeDB adapter is the default adapter. It uses the [@seald-io/nedb](https://github.com/seald/nedb) library which is a lightweight embedded persistent or in-memory database. The [API](https://github.com/seald/nedb/blob/master/API.md) is a subset of MongoDB's API.
3 |
4 | > Use this adapter for prototyping and testing.
5 |
6 | ## Install
7 | This module contains the source code of the adapter. You just need to install the dependent library.
8 |
9 | ```bash
10 | npm install @seald-io/nedb
11 | ```
12 |
13 | ## Usage
14 |
15 | ### In memory NeDB adapter
16 | This is the default adapter, no need any configuration.
17 |
18 | ```js
19 | // posts.service.js
20 | const DbService = require("@moleculer/database").Service;
21 |
22 | module.exports = {
23 | name: "posts",
24 | mixins: [DbService()]
25 | }
26 | ```
27 |
28 | ### Persistent database
29 | To use a persistent database, set the filename.
30 | ```js
31 | // posts.service.js
32 | const DbService = require("@moleculer/database").Service;
33 |
34 | module.exports = {
35 | name: "posts",
36 | mixins: [DbService({
37 | adapter: {
38 | type: "NeDB",
39 | options: "./posts.db"
40 | }
41 | })]
42 | }
43 | ```
44 |
45 | ### Using NeDB options
46 | ```js
47 | // posts.service.js
48 | const DbService = require("@moleculer/database").Service;
49 |
50 | module.exports = {
51 | name: "posts",
52 | mixins: [DbService({
53 | adapter: {
54 | type: "NeDB",
55 | options: {
56 | neDB: {
57 | inMemoryOnly: true,
58 | corruptAlertThreshold: 0.5
59 | }
60 | }
61 | }
62 | })]
63 | }
64 | ```
65 |
66 | ### Using custom NeDB instance
67 | ```js
68 | // posts.service.js
69 | const DbService = require("@moleculer/database").Service;
70 | const MyNeDB = require("...");
71 |
72 | module.exports = {
73 | name: "posts",
74 | mixins: [DbService({
75 | adapter: {
76 | type: "NeDB",
77 | options: {
78 | neDB: new MyNeDB({ filename: "./posts.db" })
79 | }
80 | }
81 | })]
82 | }
83 | ```
84 |
85 | ## Options
86 | | Property | Type | Default | Description |
87 | | -------- | ---- | ------- | ----------- |
88 | | `neDB` | `Object|DataStore` | `null` | [NeDB constructor options](https://github.com/seald/nedb?tab=readme-ov-file#creatingloading-a-database). If it's a `DataStore` instance, it uses it instead of creating a new one. |
89 |
90 |
91 | ## Raw update
92 | If you want to update entity and using raw changes, use the [`updateEntity`](../README.md#updateentity) method with `{ raw: true }` options. In this case, you can use [NeDB modifiers](https://github.com/seald/nedb?tab=readme-ov-file#updating-documents) in the `params` parameter.
93 |
94 | ### Example
95 | ```js
96 | const row = await this.updateEntity(ctx, {
97 | id: "YVdnh5oQCyEIRja0",
98 |
99 | $set: {
100 | status: false,
101 | height: 192
102 | },
103 | $inc: {
104 | age: 1
105 | },
106 | $unset: {
107 | dob: true
108 | }
109 | }, { raw: true });
110 | ```
111 |
112 | ## Additional methods
113 | No any additional methods.
114 |
--------------------------------------------------------------------------------
/test/leak-detection/index.spec.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const memwatch = require("@icebob/node-memwatch");
4 | const { ServiceBroker } = require("moleculer");
5 | const DbService = require("../..").Service;
6 |
7 | const ACCEPTABLE_LIMIT = 1 * 1024 * 1024; // 1MB
8 |
9 | jest.setTimeout(60000);
10 |
11 | describe("Moleculer Database memory leak test", () => {
12 | const broker = new ServiceBroker({ logger: false });
13 | const svc = broker.createService({
14 | name: "posts",
15 | mixins: [
16 | DbService({
17 | adapter: {
18 | type: "MongoDB",
19 | options: { dbName: "db_int_test", collection: "posts" }
20 | },
21 | createActions: true
22 | })
23 | ],
24 | settings: {
25 | fields: {
26 | id: { type: "string", primaryKey: true, columnName: "_id" },
27 | title: { type: "string", required: true },
28 | content: { type: "string", required: true }
29 | }
30 | }
31 | });
32 |
33 | const posts = [];
34 |
35 | beforeAll(async () => {
36 | await broker.start();
37 |
38 | // Warm up
39 | for (let i = 0; i < 100; i++) {
40 | await broker.call("posts.create", {
41 | title: "Post title",
42 | content: "Post content"
43 | });
44 | }
45 | memwatch.gc();
46 | });
47 |
48 | afterAll(() => broker.stop());
49 |
50 | async function execute(actionName, params) {
51 | const hd = new memwatch.HeapDiff();
52 |
53 | const paramsIsFunc = typeof params == "function";
54 |
55 | for (let i = 0; i < 1000; i++) {
56 | const p = paramsIsFunc ? params() : params;
57 | const res = await broker.call(actionName, p);
58 | if (actionName == "posts.create") {
59 | posts.push(res.id);
60 | }
61 | }
62 |
63 | memwatch.gc();
64 | const diff = hd.end();
65 | if (diff.change.size_bytes >= ACCEPTABLE_LIMIT) console.log("Diff:", diff); // eslint-disable-line no-console
66 |
67 | expect(diff.change.size_bytes).toBeLessThan(ACCEPTABLE_LIMIT);
68 | }
69 |
70 | it("should not leak when create records", async () => {
71 | await execute("posts.create", {
72 | title: "Post title",
73 | content: "Post content"
74 | });
75 | });
76 |
77 | it("should not leak when find records", async () => {
78 | await execute("posts.find", { offset: 0, limit: 20 });
79 | });
80 |
81 | it("should not leak when list records", async () => {
82 | await execute("posts.list", { page: 1, pageSize: 20 });
83 | });
84 |
85 | it("should not leak when count records", async () => {
86 | await execute("posts.count");
87 | });
88 |
89 | it("should not leak when get a record", async () => {
90 | await execute("posts.get", { id: posts[5] });
91 | });
92 |
93 | it("should not leak when resolve a record", async () => {
94 | await execute("posts.resolve", { id: posts[5] });
95 | });
96 |
97 | it("should not leak when update a record", async () => {
98 | await execute("posts.update", { id: posts[5], title: "Modified title" });
99 | });
100 |
101 | it("should not leak when replace a record", async () => {
102 | await execute("posts.replace", {
103 | id: posts[5],
104 | title: "Replaced title",
105 | content: "Replaced content"
106 | });
107 | });
108 |
109 | it("should not leak when remove a record", async () => {
110 | console.log("posts", posts.length);
111 | await execute("posts.remove", () => {
112 | return { id: posts.pop() };
113 | });
114 | });
115 | });
116 |
--------------------------------------------------------------------------------
/benchmark/results/transform/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Moleculer Database benchmark - Transformation benchmark
4 | This is a transformation benchmark. It tests all service methods with and without transformation.
5 | ## Entity creation
6 |
7 | ### Result
8 |
9 |
10 | | Adapter config | Time | Diff | ops/sec |
11 | | -------------- | ----:| ----:| -------:|
12 | | Without transform | 55μs | 0% | 18,168.9 |
13 | | With transform | 52μs | 4.99% | 19,076.02 |
14 |
15 | 
16 |
17 | ## Entity listing
18 |
19 | ### Result
20 |
21 |
22 | | Adapter config | Time | Diff | ops/sec |
23 | | -------------- | ----:| ----:| -------:|
24 | | Without transform | 131μs | 0% | 7,591.59 |
25 | | With transform | 148μs | -11.38% | 6,727.93 |
26 |
27 | 
28 |
29 | ## Entity counting
30 |
31 | ### Result
32 |
33 |
34 | | Adapter config | Time | Diff | ops/sec |
35 | | -------------- | ----:| ----:| -------:|
36 | | Without transform | 726μs | 0% | 1,376.87 |
37 |
38 | 
39 |
40 | ## Entity getting
41 |
42 | ### Result
43 |
44 |
45 | | Adapter config | Time | Diff | ops/sec |
46 | | -------------- | ----:| ----:| -------:|
47 | | Without transform | 21μs | 0% | 46,501.41 |
48 | | With transform | 28μs | -25.63% | 34,581.01 |
49 | | Direct adapter access | 20μs | 7.33% | 49,908.36 |
50 |
51 | 
52 |
53 | ## Entity updating
54 |
55 | ### Result
56 |
57 |
58 | | Adapter config | Time | Diff | ops/sec |
59 | | -------------- | ----:| ----:| -------:|
60 | | Without transform | 93μs | 0% | 10,694.34 |
61 | | With transform | 99μs | -6.38% | 10,011.73 |
62 |
63 | 
64 |
65 | ## Entity replacing
66 |
67 | ### Result
68 |
69 |
70 | | Adapter config | Time | Diff | ops/sec |
71 | | -------------- | ----:| ----:| -------:|
72 | | Without transform | 93μs | 0% | 10,662.15 |
73 | | With transform | 100μs | -7.11% | 9,904.25 |
74 |
75 | 
76 |
77 | ## Entity deleting
78 |
79 | ### Result
80 |
81 |
82 | | Adapter config | Time | Diff | ops/sec |
83 | | -------------- | ----:| ----:| -------:|
84 | | Without transform | 51μs | 0% | 19,603.08 |
85 | | With transform | 183μs | -72.15% | 5,459.5 |
86 |
87 | 
88 |
89 | --------------------
90 | _Generated at 2021-05-10T16:10:04.759Z_
--------------------------------------------------------------------------------
/test/integration/transform.test.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const { ServiceBroker, Context } = require("moleculer");
4 | const DbService = require("../..").Service;
5 |
6 | module.exports = (getAdapter, adapterType) => {
7 | let expectedID;
8 | if (["Knex"].includes(adapterType)) {
9 | expectedID = expect.any(Number);
10 | } else {
11 | expectedID = expect.any(String);
12 | }
13 |
14 | describe("Test transformations", () => {
15 | const broker = new ServiceBroker({ logger: false });
16 | const svc = broker.createService({
17 | name: "users",
18 | mixins: [
19 | DbService({
20 | adapter: getAdapter()
21 | })
22 | ],
23 | settings: {
24 | fields: {
25 | myID: {
26 | type: "string",
27 | primaryKey: true,
28 | columnName: "_id",
29 | columnType: "integer"
30 | },
31 | name: { type: "string" },
32 | upperName: {
33 | type: "string",
34 | readonly: true,
35 | virtual: true,
36 | get: ({ entity }) => (entity.name ? entity.name.toUpperCase() : entity.name)
37 | },
38 | password: { type: "string", hidden: true },
39 | token: { type: "string", hidden: "byDefault" },
40 | email: { type: "string", readPermission: "admin" },
41 | phone: { type: "string", permission: "admin" }
42 | }
43 | },
44 |
45 | async started() {
46 | const adapter = await this.getAdapter();
47 |
48 | if (adapterType == "Knex") {
49 | await adapter.createTable();
50 | }
51 |
52 | await this.clearEntities();
53 | }
54 | });
55 |
56 | beforeAll(() => broker.start());
57 | afterAll(() => broker.stop());
58 |
59 | const ctx = Context.create(broker, null, {});
60 | const docs = {};
61 |
62 | describe("Set up", () => {
63 | it("should return empty array", async () => {
64 | await svc.clearEntities();
65 |
66 | const rows = await svc.findEntities(ctx);
67 | expect(rows).toEqual([]);
68 |
69 | const count = await svc.countEntities(ctx);
70 | expect(count).toEqual(0);
71 | });
72 | });
73 |
74 | describe("Test hidden fields, getter, readPermission", () => {
75 | it("create test entity", async () => {
76 | const res = await svc.createEntity(ctx, {
77 | name: "John Doe",
78 | upperName: "Nothing",
79 | email: "john.doe@moleculer.services",
80 | phone: "+1-555-1234",
81 | password: "johnDoe1234",
82 | token: "token1234"
83 | });
84 | docs.johnDoe = res;
85 |
86 | expect(res).toEqual({
87 | myID: expectedID,
88 | name: "John Doe",
89 | upperName: "JOHN DOE",
90 | email: "john.doe@moleculer.services",
91 | phone: "+1-555-1234"
92 | });
93 | });
94 |
95 | it("should hide e-mail address", async () => {
96 | svc.checkFieldAuthority = jest.fn(async () => false);
97 | const res = await svc.resolveEntities(ctx, { myID: docs.johnDoe.myID });
98 | expect(res).toEqual({
99 | myID: expectedID,
100 | name: "John Doe",
101 | upperName: "JOHN DOE"
102 | });
103 | });
104 |
105 | it("should not transform the entity", async () => {
106 | const res = await svc.resolveEntities(
107 | ctx,
108 | { myID: docs.johnDoe.myID },
109 | { transform: false }
110 | );
111 | expect(res).toEqual({
112 | _id: expect.anything(),
113 | name: "John Doe",
114 | email: "john.doe@moleculer.services",
115 | phone: "+1-555-1234",
116 | password: "johnDoe1234",
117 | token: "token1234"
118 | });
119 | });
120 |
121 | it("should filter fields", async () => {
122 | const res = await svc.resolveEntities(ctx, {
123 | myID: docs.johnDoe.myID,
124 | fields: ["upperName", "asdasdasd", "password", "email", "token"]
125 | });
126 | expect(res).toEqual({
127 | upperName: "JOHN DOE",
128 | token: "token1234"
129 | });
130 | });
131 |
132 | it("should filter all fields", async () => {
133 | const res = await svc.resolveEntities(ctx, {
134 | myID: docs.johnDoe.myID,
135 | fields: []
136 | });
137 | expect(res).toEqual({});
138 | });
139 | });
140 | });
141 | };
142 |
--------------------------------------------------------------------------------
/examples/simple/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | "use strict";
3 |
4 | /**
5 | * It's a simple example which demonstrates how to
6 | * use this services and make CRUD actions.
7 | */
8 |
9 | const { ServiceBroker } = require("moleculer");
10 | const { inspect } = require("util");
11 | const DbService = require("../../index").Service;
12 |
13 | // Create broker
14 | const broker = new ServiceBroker({
15 | logger: {
16 | type: "Console",
17 | options: {
18 | level: {
19 | POSTS: "debug",
20 | "*": "info"
21 | },
22 | objectPrinter: obj =>
23 | inspect(obj, {
24 | breakLength: 50,
25 | colors: true,
26 | depth: 3
27 | })
28 | }
29 | },
30 | metrics: {
31 | enabled: false,
32 | reporter: {
33 | type: "Console",
34 | options: {
35 | includes: ["moleculer.database.**"]
36 | }
37 | }
38 | },
39 |
40 | tracing: {
41 | enabled: false,
42 | exporter: {
43 | type: "Console"
44 | }
45 | }
46 | });
47 |
48 | // Create a service
49 | broker.createService({
50 | name: "posts",
51 | mixins: [
52 | DbService({
53 | adapter: {
54 | /*type: "Knex",
55 | options: {
56 | knex: {
57 | client: "sqlite3",
58 | connection: {
59 | filename: ":memory:"
60 | }
61 | }
62 | }*/
63 | type: "MongoDB"
64 | }
65 | })
66 | ],
67 |
68 | settings: {
69 | fields: {
70 | id: { type: "string", primaryKey: true, columnName: "_id" /*, generated: "user"*/ },
71 | title: {
72 | type: "string",
73 | max: 255,
74 | trim: true,
75 | required: true
76 | },
77 | content: { type: "string" },
78 | votes: { type: "number", integer: true, min: 0, default: 0, columnType: "int" },
79 | status: { type: "boolean", default: true },
80 | createdAt: {
81 | type: "number",
82 | readonly: true,
83 | onCreate: () => Date.now(),
84 | columnType: "double"
85 | },
86 | updatedAt: {
87 | type: "number",
88 | readonly: true,
89 | onUpdate: () => Date.now(),
90 | columnType: "double"
91 | }
92 | }
93 | },
94 |
95 | async started() {
96 | const adapter = await this.getAdapter();
97 | if (adapter.createTable) await adapter.createTable();
98 |
99 | await this.clearEntities();
100 | }
101 | });
102 |
103 | // Start server
104 | broker
105 | .start()
106 | .then(async () => {
107 | // Create a new post
108 | let post = await broker.call("posts.create", {
109 | id: "63397c8302751c0c0abd3609",
110 | title: "My first post",
111 | content: "Content of my first post..."
112 | });
113 | console.log("First post:", post);
114 |
115 | await broker.Promise.delay(500);
116 |
117 | post = await broker.call("posts.create", {
118 | title: "My second post",
119 | content: "Content of my second post...",
120 | votes: 3
121 | });
122 | console.log("Second post:", post);
123 |
124 | post = await broker.call("posts.create", {
125 | title: "Third post",
126 | content: "Content of my 3rd post...",
127 | status: false
128 | });
129 | console.log("3rd post:", post);
130 |
131 | let posts = await broker.call("posts.createMany", [
132 | {
133 | title: "Forth post",
134 | content: "Content of my 4th post...",
135 | status: true
136 | },
137 | {
138 | title: "Fifth post",
139 | content: "Content of my 5th post...",
140 | status: true
141 | }
142 | ]);
143 | console.log("4. & 5. posts:", posts);
144 |
145 | // Get all posts
146 | //let posts = await broker.call("posts.find", { limit: 2, sort: "-createdAt" });
147 | posts = await broker.call("posts.find", { query: { status: false } });
148 | console.log("Find:", posts);
149 |
150 | // List posts with pagination
151 | posts = await broker.call("posts.list", { page: 1, pageSize: 10 });
152 | console.log("List:", posts);
153 |
154 | // Get a post by ID
155 | post = await broker.call("posts.get", { id: post.id });
156 | console.log("Get:", post);
157 |
158 | // Update the post
159 | post = await broker.call("posts.update", { id: post.id, title: "Modified post" });
160 | console.log("Updated:", post);
161 |
162 | // Delete a user
163 | const res = await broker.call("posts.remove", { id: post.id });
164 | console.log("Deleted:", res);
165 | })
166 | .then(() => broker.repl())
167 | .catch(err => {
168 | broker.logger.error(err);
169 | process.exit(1);
170 | });
171 |
--------------------------------------------------------------------------------
/examples/multi-tenants/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /**
4 | * This example demonstrates the multi-tenant feature.
5 | */
6 |
7 | const { ServiceBroker } = require("moleculer");
8 | const { inspect } = require("util");
9 | const DbService = require("../../index").Service;
10 |
11 | const tenant1Meta = { meta: { tenantId: 1001 }, broadcast: () => {} };
12 | const tenant2Meta = { meta: { tenantId: 1002 }, broadcast: () => {} };
13 | const tenant3Meta = { meta: { tenantId: 1003 }, broadcast: () => {} };
14 |
15 | // Create broker
16 | const broker = new ServiceBroker({
17 | logger: {
18 | type: "Console",
19 | options: {
20 | objectPrinter: obj =>
21 | inspect(obj, {
22 | breakLength: 50,
23 | colors: true,
24 | depth: 3
25 | })
26 | }
27 | }
28 | });
29 |
30 | // Create a service
31 | broker.createService({
32 | name: "posts",
33 | mixins: [
34 | DbService({
35 | adapter: {
36 | //type: "MongoDB",
37 | type: "Knex",
38 | options: {
39 | //uri: "mongodb://127.0.0.1:27017",
40 | //dbName: "tenants",
41 | //collection: "posts",
42 | tableName: "tenant_posts",
43 | knex: {
44 | client: "mssql",
45 | connection: {
46 | host: "127.0.0.1",
47 | port: 1433,
48 | user: "sa",
49 | password: "Moleculer@Pass1234",
50 | database: "db_int_test",
51 | encrypt: false
52 | }
53 | }
54 | }
55 | }
56 | })
57 | ],
58 | settings: {
59 | fields: {
60 | id: { type: "number", increment: true, primaryKey: true, columnType: "bigint" },
61 | title: { type: "string", required: true },
62 | content: { type: "string", required: false },
63 | tenantId: {
64 | type: "number",
65 | columnType: "integer",
66 | set: ({ ctx }) => ctx.meta.tenantId,
67 | required: true
68 | }
69 | },
70 | scopes: {
71 | tenant(q, ctx) {
72 | const tenantId = ctx.meta.tenantId;
73 | if (!tenantId) throw new Error("Missing tenantId!");
74 |
75 | q.tenantId = ctx.meta.tenantId;
76 | return q;
77 | }
78 | },
79 | defaultScopes: ["tenant"]
80 | },
81 | methods: {
82 | getAdapterByContext(ctx) {
83 | const tenantId = ctx.meta.tenantId;
84 | if (!tenantId) throw new Error("Missing tenantId!");
85 |
86 | return [
87 | tenantId,
88 | {
89 | //type: "MongoDB",
90 | type: "Knex",
91 | options: {
92 | //uri: "mongodb://127.0.0.1:27017",
93 | //dbName: `tenant-posts-${tenantId}`,
94 | //collection: `posts`
95 |
96 | tableName: "tenant_posts",
97 | schema: "tenant_" + tenantId,
98 | knex: {
99 | client: "mssql",
100 | connection: {
101 | host: "127.0.0.1",
102 | port: 1433,
103 | user: "sa",
104 | password: "Moleculer@Pass1234",
105 | database: "db_int_test",
106 | encrypt: false
107 | }
108 | }
109 | }
110 | }
111 | ];
112 | }
113 | },
114 |
115 | hooks: {
116 | customs: {
117 | async adapterConnected(adapter) {
118 | await adapter.createTable();
119 | }
120 | }
121 | },
122 |
123 | async started() {
124 | await this.clearEntities(tenant1Meta);
125 | await this.clearEntities(tenant2Meta);
126 | await this.clearEntities(tenant3Meta);
127 | }
128 | });
129 |
130 | // Start server
131 | broker.start().then(async () => {
132 | await broker.call("posts.find", null, tenant1Meta);
133 |
134 | await broker.call(
135 | "posts.create",
136 | { title: "First post", content: "First content" },
137 | tenant1Meta
138 | );
139 | await broker.call(
140 | "posts.create",
141 | { title: "Second post", content: "Second content" },
142 | tenant2Meta
143 | );
144 | await broker.call(
145 | "posts.create",
146 | { title: "Third post", content: "Third content" },
147 | tenant3Meta
148 | );
149 | await broker.call(
150 | "posts.create",
151 | { title: "Fourth post", content: "Fourth content" },
152 | tenant1Meta
153 | );
154 | await broker.call(
155 | "posts.create",
156 | { title: "Fifth post", content: "Fifth content" },
157 | tenant2Meta
158 | );
159 |
160 | broker.logger.info("Find tenant1: ", await broker.call("posts.find", null, tenant1Meta));
161 | broker.logger.info("Find tenant2: ", await broker.call("posts.find", null, tenant2Meta));
162 | broker.logger.info("Find tenant3: ", await broker.call("posts.find", null, tenant3Meta));
163 | });
164 |
--------------------------------------------------------------------------------
/src/schema.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @moleculer/database
3 | * Copyright (c) 2024 MoleculerJS (https://github.com/moleculerjs/database)
4 | * MIT Licensed
5 | */
6 |
7 | "use strict";
8 |
9 | const _ = require("lodash");
10 |
11 | const Validator = require("fastest-validator");
12 | const validator = new Validator({
13 | useNewCustomCheckerFunction: true
14 | });
15 |
16 | function getPrimaryKeyFromFields(fields) {
17 | let field = null;
18 | _.forEach(fields, (f, name) => {
19 | if (!field && f.primaryKey) {
20 | field = {
21 | ...f,
22 | name
23 | };
24 | }
25 | });
26 |
27 | return field || { name: "_id", type: "string", columnName: "_id" };
28 | }
29 |
30 | function fixIDInRestPath(def, primaryKeyField) {
31 | if (def && def.rest) {
32 | if (_.isObject(def.rest)) {
33 | def.rest.path = def.rest.path
34 | ? def.rest.path.replace(/:id/, `:${primaryKeyField.name}`)
35 | : null;
36 | } else if (_.isString(def.rest)) {
37 | def.rest = def.rest.replace(/:id/, `:${primaryKeyField.name}`);
38 | }
39 | }
40 | }
41 |
42 | function fixIDInCacheKeys(def, primaryKeyField) {
43 | if (def && def.cache && def.cache.keys) {
44 | def.cache.keys = def.cache.keys.map(key => (key == "id" ? primaryKeyField.name : key));
45 | }
46 | }
47 |
48 | function isActionEnabled(mixinOpts, actionName) {
49 | if (typeof mixinOpts.createActions == "object") {
50 | return mixinOpts.createActions[actionName] !== false;
51 | }
52 | return mixinOpts.createActions !== false;
53 | };
54 |
55 | function generateValidatorSchemaFromFields(fields, opts) {
56 | const res = {};
57 |
58 | if (fields == null || Object.keys(fields).length == 0) return res;
59 |
60 | opts == opts || {};
61 | if (opts.level == null) opts.level = 0;
62 |
63 | if (opts.level == 0) res.$$strict = opts.strict;
64 |
65 | _.map(fields, (field, name) => {
66 | if (field === false) return;
67 | if (typeof field == "string") field = validator.parseShortHand(field);
68 |
69 | const schema = generateFieldValidatorSchema(field, opts);
70 | if (schema != null) res[name] = schema;
71 | });
72 |
73 | return res;
74 | }
75 |
76 | function generateFieldValidatorSchema(field, opts) {
77 | const schema = _.omit(field, [
78 | "name",
79 | "required",
80 | "optional",
81 | "columnName",
82 | "primaryKey",
83 | "optional",
84 | "hidden",
85 | "readonly",
86 | "required",
87 | "immutable",
88 | "onCreate",
89 | "onUpdate",
90 | "onReplace",
91 | "onRemove",
92 | "permission",
93 | "readPermission",
94 | "populate",
95 | "itemProperties",
96 | "set",
97 | "get",
98 | "validate",
99 | "default"
100 | ]);
101 |
102 | // Type
103 | schema.type = field.type || "any";
104 |
105 | // Readonly or virtual field -> Forbidden
106 | if (field.readonly || field.virtual) return null;
107 |
108 | // Primary key forbidden on create
109 | if (field.primaryKey && opts.type == "create" && field.generated != "user") return null;
110 |
111 | // Required
112 | // If there is `set` we can't set the required maybe the value will be set in the `set`
113 | if (!field.required || field.set) schema.optional = true;
114 |
115 | // On update, every field is optional except primaryKey
116 | if (opts.type == "update") schema.optional = !field.primaryKey;
117 |
118 | // On replace, the primaryKey is required
119 | if (opts.type == "replace" && field.primaryKey) schema.optional = false;
120 |
121 | // Type conversion (enable by default)
122 | if (opts.enableParamsConversion && ["string", "number", "date", "boolean"].includes(field.type))
123 | schema.convert = field.convert != null ? field.convert : true;
124 |
125 | // Default value (if not "update"), Function default value is not supported by FV
126 | if (field.default !== undefined && !_.isFunction(field.default) && opts.type != "update")
127 | schema.default = _.cloneDeep(field.default);
128 |
129 | // Nested object
130 | if (field.type == "object" && field.properties) {
131 | schema.strict = opts.strict;
132 | schema.properties = generateValidatorSchemaFromFields(field.properties, {
133 | ...opts,
134 | level: opts.level + 1
135 | });
136 | }
137 |
138 | // Array
139 | if (field.type == "array" && field.items) {
140 | schema.items = generateFieldValidatorSchema(field.items, opts);
141 | }
142 |
143 | return schema;
144 | }
145 |
146 | module.exports = {
147 | getPrimaryKeyFromFields,
148 | fixIDInRestPath,
149 | fixIDInCacheKeys,
150 | generateValidatorSchemaFromFields,
151 | generateFieldValidatorSchema,
152 | isActionEnabled
153 | };
154 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | 
4 | [](https://coveralls.io/github/moleculerjs/database?branch=master)
5 | [](https://snyk.io/test/github/moleculerjs/database)
6 | [](https://www.npmjs.com/package/@moleculer/database)
7 |
8 | # @moleculer/database
9 | Advanced Database Access Service for Moleculer microservices framework. Use it to persist your data in a database.
10 |
11 | >this module follows the *one database per service* pattern. To learn more about this design pattern and its implications check this [article](https://microservices.io/patterns/data/database-per-service.html). For *multiple entities/tables per service* approach check [FAQ](https://moleculer.services/docs/0.14/faq.html#How-can-I-manage-multiple-entities-tables-per-service).
12 |
13 | ## Features
14 | - multiple pluggable adapters (NeDB, MongoDB, Knex)
15 | - common CRUD actions for RESTful API with caching
16 | - pagination, field filtering support
17 | - field sanitizations, validations
18 | - read-only, immutable, virtual fields
19 | - field permissions (read/write)
20 | - ID field encoding
21 | - data transformation
22 | - populating between Moleculer services
23 | - create/update/remove hooks
24 | - soft delete mode
25 | - scopes support
26 | - entity lifecycle events
27 | - Multi-tenancy
28 |
29 | ## Install
30 | ```
31 | npm i @moleculer/database @seald-io/nedb
32 | ```
33 | > Installing `@seald-io/nedb` is optional. It can be good for prototyping.
34 |
35 | ## Usage
36 |
37 | **Define the service**
38 | ```js
39 | // posts.service.js
40 |
41 | const DbService = require("@moleculer/database").Service;
42 |
43 | module.exports = {
44 | name: "posts",
45 | mixins: [
46 | DbService({
47 | adapter: "NeDB"
48 | })
49 | ],
50 |
51 | settings: {
52 | fields: {
53 | id: { type: "string", primaryKey: true, columnName: "_id" },
54 | title: { type: "string", max: 255, trim: true, required: true },
55 | content: { type: "string" },
56 | votes: { type: "number", integer: true, min: 0, default: 0 },
57 | status: { type: "boolean", default: true },
58 | createdAt: { type: "number", readonly: true, onCreate: () => Date.now() },
59 | updatedAt: { type: "number", readonly: true, onUpdate: () => Date.now() }
60 | }
61 | }
62 | }
63 | ```
64 |
65 | **Call the actions**
66 | ```js
67 | // sample.js
68 |
69 | // Create a new post
70 | let post = await broker.call("posts.create", {
71 | title: "My first post",
72 | content: "Content of my first post..."
73 | });
74 | console.log("New post:", post);
75 | /* Results:
76 | New post: {
77 | id: 'Zrpjq8B1XTSywUgT',
78 | title: 'My first post',
79 | content: 'Content of my first post...',
80 | votes: 0,
81 | status: true,
82 | createdAt: 1618065551990
83 | }
84 | */
85 |
86 | // Get all posts
87 | let posts = await broker.call("posts.find", { sort: "-createdAt" });
88 | console.log("Find:", posts);
89 |
90 | // List posts with pagination
91 | posts = await broker.call("posts.list", { page: 1, pageSize: 10 });
92 | console.log("List:", posts);
93 |
94 | // Get a post by ID
95 | post = await broker.call("posts.get", { id: post.id });
96 | console.log("Get:", post);
97 |
98 | // Update the post
99 | post = await broker.call("posts.update", { id: post.id, title: "Modified post" });
100 | console.log("Updated:", post);
101 |
102 | // Delete a user
103 | const res = await broker.call("posts.remove", { id: post.id });
104 | console.log("Deleted:", res);
105 | ```
106 |
107 | [**Try it in your browser on repl.it**](https://replit.com/@icebob/moleculer-database-common)
108 |
109 | ## Documentation
110 | You can find [here the documentation](docs/README.md).
111 |
112 | ## Benchmark
113 | There is some benchmark with all adapters. [You can find the results here.](benchmark/results/common/README.md)
114 |
115 | ## License
116 | The project is available under the [MIT license](https://tldrlegal.com/license/mit-license).
117 |
118 | ## Contact
119 | Copyright (c) 2025 MoleculerJS
120 |
121 | [](https://github.com/moleculerjs) [](https://twitter.com/MoleculerJS)
122 |
--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------
1 | # CLAUDE.md
2 |
3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4 |
5 | ## Project Overview
6 |
7 | This is the **@moleculer/database** package - an advanced database access service for the Moleculer microservices framework. It provides a mixin that adds CRUD operations, field validation, data transformation, populating, scoping, multi-tenancy, and other database features to Moleculer services.
8 |
9 | ## Key Architecture Components
10 |
11 | ### Core Structure
12 | - **Mixin Pattern**: The service is implemented as a Moleculer mixin that other services can use
13 | - **Multi-Adapter Support**: Supports NeDB, MongoDB, and Knex (SQL databases) adapters
14 | - **Field-Based Schema**: Uses a comprehensive field definition system similar to Fastest Validator
15 | - **Transformation Pipeline**: Data flows through validation → transformation → storage → retrieval → transformation
16 |
17 | ### Main Files
18 | - `src/index.js` - Main mixin factory function with lifecycle hooks
19 | - `src/actions.js` - Auto-generated CRUD actions (find, list, get, create, update, etc.)
20 | - `src/methods.js` - Core database methods used by actions and custom implementations
21 | - `src/validation.js` - Field validation and sanitization logic
22 | - `src/transform.js` - Data transformation (encoding/decoding, populating, field filtering)
23 | - `src/adapters/` - Database adapter implementations (base, mongodb, knex, nedb)
24 | - `src/schema.js` - Field schema processing and validation schema generation
25 |
26 | ### Adapter Pattern
27 | - `src/adapters/base.js` - Abstract base adapter
28 | - `src/adapters/mongodb.js` - MongoDB adapter
29 | - `src/adapters/knex.js` - SQL databases via Knex.js
30 | - `src/adapters/nedb.js` - NeDB (in-memory/file-based) adapter
31 |
32 | ## Development Commands
33 |
34 | ### Testing
35 | - `npm test` - Run all tests (unit, integration, leak detection)
36 | - `npm run test:unit` - Run unit tests only
37 | - `npm run test:integration` - Run integration tests with coverage
38 | - `npm run test:leak` - Run memory leak detection tests
39 | - `npm run ci:unit` - Watch mode for unit tests
40 | - `npm run ci:integration` - Watch mode for integration tests
41 | - `npm run ci:leak` - Watch mode for leak detection tests
42 |
43 | ### Development
44 | - `npm run dev` - Start example service in development mode
45 | - `npm run lint` - Run ESLint on source code, examples, and tests
46 | - `npm run bench` - Run benchmarks
47 | - `npm run bench:watch` - Run benchmarks in watch mode
48 |
49 | ### Maintenance
50 | - `npm run deps` - Check and update dependencies interactively
51 | - `npm run ci-update-deps` - Auto-update minor version dependencies
52 |
53 | ## Database Adapters
54 |
55 | When working with database functionality, understand that:
56 | - **NeDB** is used for development/testing (default if no adapter specified)
57 | - **MongoDB** supports full document operations and nested field querying
58 | - **Knex** supports SQL databases (MySQL, PostgreSQL, SQLite, etc.) but converts objects/arrays to JSON strings
59 |
60 | ## Field Definitions
61 |
62 | The service uses a comprehensive field definition system. Each field can have:
63 | - Basic validation properties (type, required, min, max, etc.)
64 | - Database properties (columnName, columnType, primaryKey)
65 | - Permission properties (readPermission, permission)
66 | - Lifecycle hooks (onCreate, onUpdate, onRemove)
67 | - Transformation functions (get, set, validate)
68 | - Populate configurations for relationships
69 |
70 | ## Multi-Tenancy Support
71 |
72 | The service supports three multi-tenancy modes:
73 | 1. **Record-based**: Same table with tenant ID field + scopes
74 | 2. **Table-based**: Different tables per tenant
75 | 3. **Database-based**: Different databases per tenant
76 |
77 | Implement via the `getAdapterByContext` method to customize adapter creation per context.
78 |
79 | ## Testing Integration Services
80 |
81 | When testing services that use this mixin:
82 | - Use NeDB adapter for simple unit tests (no external dependencies)
83 | - Use real databases for integration tests (see `test/docker-compose.yml`)
84 | - Test both single operations and batch operations
85 | - Test scoping, populating, and multi-tenancy if used
86 | - Check soft delete behavior if implemented
87 |
88 | ## Common Patterns
89 |
90 | ### Service Implementation
91 | ```javascript
92 | const DbService = require("@moleculer/database").Service;
93 |
94 | module.exports = {
95 | name: "posts",
96 | mixins: [DbService({ adapter: "MongoDB" })],
97 | settings: {
98 | fields: {
99 | id: { type: "string", primaryKey: true, columnName: "_id" },
100 | title: { type: "string", required: true },
101 | // ... more fields
102 | }
103 | }
104 | }
105 | ```
106 |
107 | ### Custom Methods
108 | Services often implement custom methods that use the built-in methods like `findEntities`, `createEntity`, `updateEntity`, etc.
109 |
110 | ### Adapter Connection
111 | Adapters connect lazily on first use, supporting multi-tenancy scenarios where connection details depend on context.
--------------------------------------------------------------------------------
/src/transform.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @moleculer/database
3 | * Copyright (c) 2022 MoleculerJS (https://github.com/moleculerjs/database)
4 | * MIT Licensed
5 | */
6 |
7 | "use strict";
8 |
9 | const { ServiceSchemaError, ValidationError } = require("moleculer").Errors;
10 | const _ = require("lodash");
11 |
12 | function deepResolve(values, resolvedObj) {
13 | if (!resolvedObj) return values;
14 |
15 | return values.map(v => {
16 | if (v != null) {
17 | if (Array.isArray(v)) return deepResolve(v, resolvedObj);
18 | else {
19 | const res = resolvedObj[v];
20 | // If not found, return the original value (which can be `null`)
21 | return res != null ? res : v;
22 | }
23 | }
24 | return v;
25 | });
26 | }
27 |
28 | module.exports = function (mixinOpts) {
29 | return {
30 | /**
31 | * Transform the result rows.
32 | *
33 | * @param {Adapter?} adapter
34 | * @param {Object|Array