@vsaas/loopback-softdelete-mixin4 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # `@vsaas/loopback-softdelete-mixin4`
2
+
3
+ Fork of `loopback-softdelete-mixin4` for `@xompass` LoopBack 3 projects.
4
+
5
+ This fork keeps the public mixin behavior compatible for existing apps, but trims the old Babel/Gulp toolchain and modernizes the package around:
6
+
7
+ - TypeScript source in `src/`
8
+ - `tsdown` builds to `dist/`
9
+ - `vitest` tests
10
+ - English-only runtime and docs
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install @vsaas/loopback-softdelete-mixin4
16
+ ```
17
+
18
+ ## Server Configuration
19
+
20
+ Add the package directory to `mixins` in your `server/model-config.json`:
21
+
22
+ ```json
23
+ {
24
+ "_meta": {
25
+ "mixins": ["../node_modules/@vsaas/loopback-softdelete-mixin4"]
26
+ }
27
+ }
28
+ ```
29
+
30
+ `@vsaas/loopback-boot` resolves this mixin from the package manifest, so no root wrapper file is required.
31
+
32
+ ## Model Configuration
33
+
34
+ ```json
35
+ {
36
+ "name": "Widget",
37
+ "properties": {
38
+ "name": {
39
+ "type": "string"
40
+ }
41
+ },
42
+ "mixins": {
43
+ "SoftDelete": true
44
+ }
45
+ }
46
+ ```
47
+
48
+ ## Options
49
+
50
+ ```json
51
+ {
52
+ "mixins": {
53
+ "SoftDelete": {
54
+ "deletedAt": "deletedAt",
55
+ "scrub": true,
56
+ "index": true,
57
+ "deletedById": true,
58
+ "deleteOp": true
59
+ }
60
+ }
61
+ }
62
+ ```
63
+
64
+ Supported options:
65
+
66
+ - `deletedAt`: rename the timestamp field used for soft deletes
67
+ - `scrub`: set deleted model fields to `null`; use `true` for all non-key fields or an array for a subset
68
+ - `index`: adds `deleteIndex` so soft-deleted rows can coexist with uniqueness constraints
69
+ - `deletedById`: adds a `deletedById` field
70
+ - `deleteOp`: adds a `deleteOp` field
71
+
72
+ ## Query Behavior
73
+
74
+ By default, queries exclude soft-deleted rows. To include them, pass:
75
+
76
+ ```js
77
+ {
78
+ deleted: true;
79
+ }
80
+ ```
81
+
82
+ at the same level as `where`, `include`, or `order`.
83
+
84
+ ## Notes
85
+
86
+ - This package overrides `destroy`, `destroyById`, and `destroyAll` to perform soft deletes.
87
+ - `count`, `find`, `findOrCreate`, `update`, and `updateAll` are scoped to non-deleted rows unless you explicitly opt into deleted results where supported.
88
+ - The `index` option only adds the `deleteIndex` property. It does not create the database index for you.
@@ -0,0 +1,4 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
3
+ //#endregion
4
+ exports.__commonJSMin = __commonJSMin;
package/dist/debug.cjs ADDED
@@ -0,0 +1,30 @@
1
+ const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
2
+ let node_util = require("node:util");
3
+ //#region src/debug.ts
4
+ var require_debug = /* @__PURE__ */ require_runtime.__commonJSMin(((exports, module) => {
5
+ function isEnabled(namespace) {
6
+ const debug = process.env.DEBUG;
7
+ if (!debug) return false;
8
+ return debug.split(",").map((part) => part.trim()).filter(Boolean).some((pattern) => matchesPattern(pattern, namespace));
9
+ }
10
+ function matchesPattern(pattern, value) {
11
+ if (pattern === "*" || pattern === value) return true;
12
+ if (!pattern.endsWith("*")) return false;
13
+ return value.startsWith(pattern.slice(0, -1));
14
+ }
15
+ function createDebug(name = "soft-delete") {
16
+ const namespace = `loopback:mixins:${name}`;
17
+ if (!isEnabled(namespace)) return () => {};
18
+ return (...args) => {
19
+ console.error(`${namespace} ${(0, node_util.format)(...args)}`);
20
+ };
21
+ }
22
+ module.exports = createDebug;
23
+ }));
24
+ //#endregion
25
+ Object.defineProperty(exports, "default", {
26
+ enumerable: true,
27
+ get: function() {
28
+ return require_debug();
29
+ }
30
+ });
package/dist/index.cjs ADDED
@@ -0,0 +1,7 @@
1
+ //#region src/index.ts
2
+ const softDelete = require("./soft-delete.cjs");
3
+ function registerSoftDeleteMixin(app) {
4
+ app.loopback.modelBuilder.mixins.define("SoftDelete", softDelete);
5
+ }
6
+ module.exports = registerSoftDeleteMixin;
7
+ //#endregion
@@ -0,0 +1,139 @@
1
+ const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
2
+ const require_debug$1 = require("./debug.cjs");
3
+ let node_crypto = require("node:crypto");
4
+ //#region src/soft-delete.ts
5
+ var require_soft_delete = /* @__PURE__ */ require_runtime.__commonJSMin(((exports, module) => {
6
+ const debug = require_debug$1.default();
7
+ function withOptionalCallback(promise, callback) {
8
+ if (typeof callback !== "function") return promise;
9
+ promise.then((result) => callback(null, result), (error) => callback(error));
10
+ return promise;
11
+ }
12
+ function buildWhere(where, deletedAt) {
13
+ const queryNonDeleted = { [deletedAt]: null };
14
+ if (!where || Object.keys(where).length === 0) return queryNonDeleted;
15
+ return { and: [where, queryNonDeleted] };
16
+ }
17
+ function createDeletePayload(scrubbed, deletedAt, index) {
18
+ const payload = {
19
+ ...scrubbed,
20
+ [deletedAt]: /* @__PURE__ */ new Date()
21
+ };
22
+ if (index) payload.deleteIndex = generateDeleteIndex();
23
+ return payload;
24
+ }
25
+ function softDeleteMixin(Model, { deletedAt = "deletedAt", scrub = false, index = false, deletedById = false, deleteOp = false } = {}) {
26
+ debug("SoftDelete mixin for Model %s", Model.modelName);
27
+ debug("options %j", {
28
+ deletedAt,
29
+ scrub,
30
+ index,
31
+ deletedById,
32
+ deleteOp
33
+ });
34
+ const properties = Model.definition.properties;
35
+ const idName = Model.dataSource.idName(Model.modelName);
36
+ let scrubbed = {};
37
+ if (scrub !== false) {
38
+ let propertiesToScrub = scrub;
39
+ if (!Array.isArray(propertiesToScrub)) propertiesToScrub = Object.keys(properties).filter((propertyName) => {
40
+ return !(properties[propertyName] ?? {}).id && propertyName !== idName && propertyName !== deletedAt;
41
+ });
42
+ scrubbed = propertiesToScrub.reduce((accumulator, propertyName) => {
43
+ accumulator[propertyName] = null;
44
+ return accumulator;
45
+ }, {});
46
+ }
47
+ Model.defineProperty(deletedAt, {
48
+ type: Date,
49
+ required: false,
50
+ default: null
51
+ });
52
+ if (index) Model.defineProperty("deleteIndex", {
53
+ type: String,
54
+ required: true,
55
+ default: "null"
56
+ });
57
+ if (deletedById) Model.defineProperty("deletedById", {
58
+ type: Number,
59
+ required: false,
60
+ default: null
61
+ });
62
+ if (deleteOp) Model.defineProperty("deleteOp", {
63
+ type: String,
64
+ required: false,
65
+ default: null
66
+ });
67
+ Model.destroyAll = function softDestroyAll(where, callback) {
68
+ return withOptionalCallback(Model.updateAll(where, createDeletePayload(scrubbed, deletedAt, index)), callback);
69
+ };
70
+ Model.remove = Model.destroyAll;
71
+ Model.deleteAll = Model.destroyAll;
72
+ Model.destroyById = function softDestroyById(id, callback) {
73
+ return withOptionalCallback(Model.updateAll({ [idName]: id }, createDeletePayload(scrubbed, deletedAt, index)), callback);
74
+ };
75
+ Model.removeById = Model.destroyById;
76
+ Model.deleteById = Model.destroyById;
77
+ Model.prototype.destroy = function softDestroy(options, callback) {
78
+ const normalizedCallback = typeof options === "function" ? options : callback;
79
+ const normalizedOptions = typeof options === "function" || options == null ? {} : { ...options };
80
+ const payload = createDeletePayload(scrubbed, deletedAt, index);
81
+ normalizedOptions.delete = true;
82
+ if (deletedById && normalizedOptions.deletedById) payload.deletedById = normalizedOptions.deletedById;
83
+ if (deleteOp && normalizedOptions.deletedById) payload.deleteOp = "user";
84
+ return withOptionalCallback(this.updateAttributes(payload, normalizedOptions), normalizedCallback);
85
+ };
86
+ Model.prototype.remove = Model.prototype.destroy;
87
+ Model.prototype.delete = Model.prototype.destroy;
88
+ const originalFindOrCreate = Model.findOrCreate;
89
+ Model.findOrCreate = function findOrCreateDeleted(query = {}, ...rest) {
90
+ if (!query.deleted) query.where = buildWhere(query.where, deletedAt);
91
+ return originalFindOrCreate.call(Model, query, ...rest);
92
+ };
93
+ const originalFind = Model.find;
94
+ Model.find = function findDeleted(query = {}, ...rest) {
95
+ if (!query.deleted) query.where = buildWhere(query.where, deletedAt);
96
+ return originalFind.call(Model, query, ...rest);
97
+ };
98
+ const originalCount = Model.count;
99
+ Model.count = function countDeleted(where = {}, ...rest) {
100
+ return originalCount.call(Model, buildWhere(where, deletedAt), ...rest);
101
+ };
102
+ const originalUpdate = Model.update;
103
+ Model.update = Model.updateAll = function updateDeleted(where = {}, ...rest) {
104
+ return originalUpdate.call(Model, buildWhere(where, deletedAt), ...rest);
105
+ };
106
+ if (Model.settings.remoting && Model.settings.remoting.sharedMethods.deleteById !== false && (deletedById || deleteOp)) {
107
+ Model.disableRemoteMethodByName("deleteById");
108
+ Model.remoteMethod("deleteById", {
109
+ accessType: "WRITE",
110
+ isStatic: false,
111
+ accepts: [{
112
+ arg: "options",
113
+ type: "object",
114
+ http: "optionsFromRequest"
115
+ }],
116
+ returns: {
117
+ arg: "data",
118
+ type: "object",
119
+ root: true
120
+ },
121
+ http: {
122
+ verb: "delete",
123
+ path: "/"
124
+ }
125
+ });
126
+ Model.prototype.deleteById = function deleteByIdWithMetadata(options = {}) {
127
+ if (deletedById) options.deletedById = options.accessToken ? options.accessToken.userId : null;
128
+ if (deleteOp && options.deletedById) options.deleteOp = "user";
129
+ return this.destroy(options).then(() => ({ count: 1 }));
130
+ };
131
+ }
132
+ }
133
+ function generateDeleteIndex() {
134
+ return (0, node_crypto.randomBytes)(4).toString("hex");
135
+ }
136
+ module.exports = softDeleteMixin;
137
+ }));
138
+ //#endregion
139
+ module.exports = require_soft_delete();
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@vsaas/loopback-softdelete-mixin4",
3
+ "version": "10.0.0",
4
+ "description": "Fork of loopback-softdelete-mixin4 for @xompass LoopBack 3 projects",
5
+ "keywords": [
6
+ "delete",
7
+ "loopback",
8
+ "mixin",
9
+ "soft",
10
+ "strongloop"
11
+ ],
12
+ "homepage": "https://github.com/xompass/loopback-softdelete-mixin4",
13
+ "bugs": {
14
+ "url": "https://github.com/xompass/loopback-softdelete-mixin4/issues"
15
+ },
16
+ "author": "Xompass",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/xompass/loopback-softdelete-mixin4"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "README.md"
24
+ ],
25
+ "main": "dist/index.cjs",
26
+ "exports": {
27
+ ".": "./dist/index.cjs",
28
+ "./soft-delete": "./dist/soft-delete.cjs",
29
+ "./package.json": "./package.json"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public",
33
+ "registry": "https://registry.npmjs.org/"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "25.5.0",
37
+ "oxfmt": "0.42.0",
38
+ "oxlint": "1.57.0",
39
+ "tsdown": "0.21.5",
40
+ "typescript": "6.0.2",
41
+ "vitest": "4.1.1",
42
+ "@vsaas/error-handler": "^10.0.0",
43
+ "@vsaas/loopback": "^10.0.0",
44
+ "@vsaas/remoting": "^10.0.0",
45
+ "@vsaas/loopback-datasource-juggler": "^10.0.0"
46
+ },
47
+ "peerDependencies": {
48
+ "@vsaas/loopback-datasource-juggler": "^10.0.0"
49
+ },
50
+ "engines": {
51
+ "node": ">=20"
52
+ },
53
+ "loopback": {
54
+ "mixins": {
55
+ "SoftDelete": "./dist/soft-delete.cjs"
56
+ }
57
+ },
58
+ "scripts": {
59
+ "build": "tsdown",
60
+ "fmt": "oxfmt -c ../.oxfmtrc.json .",
61
+ "lint": "oxlint -c ../.oxlintrc.json .",
62
+ "lint:fix": "oxlint -c ../.oxlintrc.json . --fix",
63
+ "test:run": "vitest run",
64
+ "test": "pnpm run build && vitest run",
65
+ "typecheck": "tsc --noEmit -p tsconfig.json"
66
+ }
67
+ }