@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 +88 -0
- package/dist/_virtual/_rolldown/runtime.cjs +4 -0
- package/dist/debug.cjs +30 -0
- package/dist/index.cjs +7 -0
- package/dist/soft-delete.cjs +139 -0
- package/package.json +67 -0
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.
|
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,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
|
+
}
|