@nestarc/data-subject 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nestarc
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.
package/README.md ADDED
@@ -0,0 +1,351 @@
1
+ # @nestarc/data-subject
2
+
3
+ `@nestarc/data-subject` is a small NestJS-oriented toolkit for handling data-subject export and erasure requests against subject-scoped data.
4
+
5
+ Today the package ships:
6
+
7
+ - a programmatic entity registry
8
+ - a `DataSubjectService` for `export`, `erase`, and request lookup
9
+ - a `DataSubjectModule.forRoot(...)` integration for NestJS
10
+ - a lightweight Prisma adapter built on `findMany`, `deleteMany`, and `updateMany`
11
+ - in-memory request and artifact stores for tests and local development
12
+ - typed policy validation and typed runtime errors
13
+
14
+ ## Current Scope
15
+
16
+ Package version: `0.1.0`
17
+
18
+ This repository currently focuses on the execution core. It does **not** currently ship:
19
+
20
+ - decorators or automatic entity discovery
21
+ - a CLI or schema linter
22
+ - persistent request storage adapters
23
+ - persistent artifact storage adapters beyond the in-memory implementation
24
+ - schema-aware Prisma field deletion beyond `null` assignment
25
+
26
+ If you need database-specific behavior, you can plug in your own `EntityExecutor`, `RequestStorage`, or `ArtifactStorage`.
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ npm install @nestarc/data-subject
32
+ ```
33
+
34
+ Peer dependencies used by this package:
35
+
36
+ - `@nestjs/common`
37
+ - `@nestjs/core`
38
+ - `reflect-metadata`
39
+ - `rxjs`
40
+ - `@prisma/client` if you use `fromPrisma(...)`
41
+
42
+ ## Quick Start
43
+
44
+ ```ts
45
+ import { Module } from '@nestjs/common';
46
+ import { PrismaClient } from '@prisma/client';
47
+ import {
48
+ DataSubjectModule,
49
+ InMemoryArtifactStorage,
50
+ InMemoryRequestStorage,
51
+ fromPrisma,
52
+ } from '@nestarc/data-subject';
53
+
54
+ const prisma = new PrismaClient();
55
+
56
+ @Module({
57
+ imports: [
58
+ DataSubjectModule.forRoot({
59
+ requestStorage: new InMemoryRequestStorage(),
60
+ artifactStorage: new InMemoryArtifactStorage(),
61
+ slaDays: 30,
62
+ strictLegalBasis: true,
63
+ entities: [
64
+ {
65
+ policy: {
66
+ entityName: 'User',
67
+ subjectField: 'userId',
68
+ rowLevel: 'delete-row',
69
+ fields: {
70
+ email: 'delete',
71
+ name: 'delete',
72
+ },
73
+ },
74
+ executor: fromPrisma({
75
+ delegate: prisma.user,
76
+ subjectField: 'userId',
77
+ tenantField: 'tenantId',
78
+ }),
79
+ },
80
+ {
81
+ policy: {
82
+ entityName: 'Invoice',
83
+ subjectField: 'customerId',
84
+ fields: {
85
+ customerName: {
86
+ strategy: 'retain',
87
+ legalBasis: 'tax:KR-basic-law-sec85',
88
+ until: '+7y',
89
+ },
90
+ amount: {
91
+ strategy: 'retain',
92
+ legalBasis: 'tax:KR-basic-law-sec85',
93
+ },
94
+ customerEmail: {
95
+ strategy: 'anonymize',
96
+ replacement: '[REDACTED]',
97
+ },
98
+ },
99
+ },
100
+ executor: fromPrisma({
101
+ delegate: prisma.invoice,
102
+ subjectField: 'customerId',
103
+ tenantField: 'tenantId',
104
+ }),
105
+ },
106
+ ],
107
+ publishOutbox: async (type, payload) => {
108
+ // forward to your outbox publisher
109
+ },
110
+ publishAudit: async (event, data) => {
111
+ // optional hook
112
+ },
113
+ }),
114
+ ],
115
+ })
116
+ export class AppModule {}
117
+ ```
118
+
119
+ Usage:
120
+
121
+ ```ts
122
+ const exportRequest = await dataSubject.export('user_123', 'tenant_abc');
123
+ const eraseRequest = await dataSubject.erase('user_123', 'tenant_abc');
124
+
125
+ const sameRequest = await dataSubject.getRequest(exportRequest.id);
126
+ const tenantRequests = await dataSubject.listByTenant('tenant_abc');
127
+ const overdue = await dataSubject.listOverdue();
128
+ ```
129
+
130
+ ## Policy Model
131
+
132
+ Policies are registered per entity and compiled before execution.
133
+
134
+ ### `delete`
135
+
136
+ ```ts
137
+ fields: {
138
+ email: 'delete',
139
+ }
140
+ ```
141
+
142
+ - shorthand `'delete'` is normalized to `{ strategy: 'delete' }`
143
+ - entity `rowLevel` defaults to `'delete-fields'`
144
+ - with the default Prisma adapter:
145
+ - `'delete-row'` calls `deleteMany`
146
+ - `'delete-fields'` calls `updateMany` and writes `null` into the configured delete fields
147
+
148
+ ### `anonymize`
149
+
150
+ ```ts
151
+ fields: {
152
+ email: { strategy: 'anonymize', replacement: '[REDACTED]' },
153
+ }
154
+ ```
155
+
156
+ - replacements must be static
157
+ - function replacements are rejected during policy compilation
158
+
159
+ ### `retain`
160
+
161
+ ```ts
162
+ fields: {
163
+ amount: {
164
+ strategy: 'retain',
165
+ legalBasis: 'tax:KR-basic-law-sec85',
166
+ until: '+7y',
167
+ },
168
+ }
169
+ ```
170
+
171
+ - `legalBasis` is required
172
+ - `strictLegalBasis: true` enables `scheme:reference` validation
173
+ - `pseudonymize` is part of the type model, but this package does not perform pseudonymization by itself
174
+
175
+ ### Mixed Strategies
176
+
177
+ When an entity mixes `delete`, `anonymize`, and `retain`, execution is intentionally conservative:
178
+
179
+ - `retain` fields are preserved
180
+ - delete fields are downgraded to field-level updates instead of row deletion
181
+ - mixed entities are reported as `strategy: 'mixed'` in erase stats
182
+ - retained fields are recorded in `stats.retained`
183
+
184
+ This prevents `retain` fields from being dropped just because some other fields on the same row are deletable.
185
+
186
+ ## Export Behavior
187
+
188
+ `DataSubjectService.export(subjectId, tenantId)` does the following:
189
+
190
+ 1. creates a request record
191
+ 2. reads matching rows from every registered entity
192
+ 3. writes one JSON file per entity into a ZIP archive
193
+ 4. stores the ZIP through `ArtifactStorage.put(...)`
194
+ 5. records:
195
+ - `artifactHash` as a SHA-256 digest of the ZIP bytes
196
+ - `artifactUrl` returned by the artifact storage
197
+ - `stats.entities[]` with `strategy: 'export'`
198
+
199
+ Current export artifact shape:
200
+
201
+ - key: `<requestId>.zip`
202
+ - contents: `<EntityName>.json` files
203
+
204
+ ## Erase Behavior
205
+
206
+ `DataSubjectService.erase(subjectId, tenantId)` does the following:
207
+
208
+ 1. creates a request record
209
+ 2. publishes `data_subject.erasure_requested`
210
+ 3. executes each registered entity according to its compiled policy
211
+ 4. records:
212
+ - `stats.entities[]`
213
+ - `stats.retained[]`
214
+ - `stats.verificationResidual[]`
215
+ - `artifactHash` as a SHA-256 digest of the erase report JSON
216
+
217
+ Important details:
218
+
219
+ - erase uses `ArtifactStorage` for exports, but **not** for erase reports
220
+ - erase verification currently only fails on residual rows after `delete-row`
221
+ - field-level delete and anonymize operations keep rows in place by design
222
+
223
+ ## NestJS Integration
224
+
225
+ `DataSubjectModule.forRoot(...)` accepts:
226
+
227
+ - `requestStorage`
228
+ - `artifactStorage`
229
+ - `slaDays`
230
+ - `strictLegalBasis`
231
+ - `entities`
232
+ - `publishOutbox`
233
+ - `publishAudit`
234
+ - `runInTransaction`
235
+
236
+ The module exports:
237
+
238
+ - `DataSubjectService`
239
+ - `DATA_SUBJECT_REGISTRY`
240
+
241
+ ## Public API
242
+
243
+ The package currently exports:
244
+
245
+ - `DataSubjectService`
246
+ - `DataSubjectModule`
247
+ - `Registry`
248
+ - `compilePolicy`
249
+ - `validateLegalBasis`
250
+ - `fromPrisma`
251
+ - `InMemoryRequestStorage`
252
+ - `InMemoryArtifactStorage`
253
+ - all public types from `src/types.ts`
254
+ - typed errors from `src/errors.ts`
255
+
256
+ ## Events and Hooks
257
+
258
+ ### Outbox Hook
259
+
260
+ If `publishOutbox` is provided, the built-in service emits:
261
+
262
+ - `data_subject.request_created`
263
+ - `data_subject.erasure_requested`
264
+ - `data_subject.request_completed`
265
+ - `data_subject.request_failed`
266
+
267
+ `request_completed` and `request_failed` are emitted for both export and erase requests. `erasure_requested` is erase-only.
268
+
269
+ ### Audit Hook
270
+
271
+ If `publishAudit` is provided, the built-in service currently emits:
272
+
273
+ - `data_subject.request_created`
274
+
275
+ No additional audit lifecycle events are emitted by the current implementation.
276
+
277
+ ## Typed Errors
278
+
279
+ The package exposes `DataSubjectError` with stable error codes.
280
+
281
+ Currently used codes include:
282
+
283
+ - `dsr_invalid_policy`
284
+ - `dsr_anonymize_dynamic_replacement`
285
+ - `dsr_verification_failed`
286
+ - `dsr_entity_already_registered`
287
+ - `dsr_request_conflict`
288
+ - `dsr_request_not_found`
289
+
290
+ Some additional codes exist in the public enum for future or adapter-specific use.
291
+
292
+ ## Transaction Boundaries
293
+
294
+ `runInTransaction` is an integration hook, not an automatic rollback guarantee.
295
+
296
+ ```ts
297
+ new DataSubjectService({
298
+ // ...
299
+ runInTransaction: async (work) => myUnitOfWork.run(work),
300
+ });
301
+ ```
302
+
303
+ Use it when your erase flow can run inside a real unit-of-work that also covers:
304
+
305
+ - the entity executors
306
+ - request storage writes
307
+ - outbox publishing
308
+
309
+ If those components do not participate in the same transaction boundary, rollback remains best-effort.
310
+
311
+ ## Practical Limitations
312
+
313
+ The current implementation is intentionally small. A few things are important to know up front:
314
+
315
+ - `fromPrisma(...)` only depends on `findMany`, `deleteMany`, and `updateMany`
316
+ - default Prisma field deletion writes `null`; it does not inspect schema nullability
317
+ - request states include `validating`, but the built-in service currently transitions through `created -> processing -> completed|failed`
318
+ - there is no built-in subject existence check before export or erase
319
+ - only in-memory request and artifact adapters are included in this repository
320
+
321
+ ## Development
322
+
323
+ ```bash
324
+ npm test
325
+ npm run build
326
+ ```
327
+
328
+ ## CI and Release
329
+
330
+ GitHub Actions is configured with two workflows:
331
+
332
+ - `CI`: runs `npm ci`, `npm run lint`, `npm test -- --runInBand`, and `npm run build` on pushes, pull requests, and manual runs
333
+ - `Release`: runs the same validation suite and then publishes to npm when a GitHub Release is published
334
+
335
+ Release expectations:
336
+
337
+ - configure repository secret `NPM_TOKEN`
338
+ - publish a GitHub Release from a tag that matches `v<package.json version>`
339
+ - prerelease versions publish with npm dist-tag `next`
340
+ - stable versions such as `0.1.0` publish with npm dist-tag `latest`
341
+
342
+ ## Related Docs
343
+
344
+ - [docs/prd.md](docs/prd.md)
345
+ - [docs/spec.md](docs/spec.md)
346
+ - [docs/compliance.md](docs/compliance.md)
347
+ - [CHANGELOG.md](CHANGELOG.md)
348
+
349
+ ## License
350
+
351
+ MIT
@@ -0,0 +1,19 @@
1
+ import { DynamicModule } from '@nestjs/common';
2
+ import { type DataSubjectServiceDeps } from './data-subject.service';
3
+ import { type RegisterInput } from './registry';
4
+ import type { ArtifactStorage } from './storage/artifact-storage.interface';
5
+ import type { RequestStorage } from './storage/request-storage.interface';
6
+ export declare const DATA_SUBJECT_REGISTRY: unique symbol;
7
+ export interface DataSubjectModuleOptions {
8
+ requestStorage: RequestStorage;
9
+ artifactStorage: ArtifactStorage;
10
+ slaDays?: number;
11
+ strictLegalBasis?: boolean;
12
+ entities?: RegisterInput[];
13
+ publishOutbox?: DataSubjectServiceDeps['publishOutbox'];
14
+ publishAudit?: DataSubjectServiceDeps['publishAudit'];
15
+ runInTransaction?: DataSubjectServiceDeps['runInTransaction'];
16
+ }
17
+ export declare class DataSubjectModule {
18
+ static forRoot(options: DataSubjectModuleOptions): DynamicModule;
19
+ }
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var DataSubjectModule_1;
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.DataSubjectModule = exports.DATA_SUBJECT_REGISTRY = void 0;
11
+ const common_1 = require("@nestjs/common");
12
+ const data_subject_service_1 = require("./data-subject.service");
13
+ const registry_1 = require("./registry");
14
+ exports.DATA_SUBJECT_REGISTRY = Symbol('DATA_SUBJECT_REGISTRY');
15
+ let DataSubjectModule = DataSubjectModule_1 = class DataSubjectModule {
16
+ static forRoot(options) {
17
+ const providers = [
18
+ {
19
+ provide: exports.DATA_SUBJECT_REGISTRY,
20
+ useFactory: () => {
21
+ const registry = new registry_1.Registry({
22
+ strictLegalBasis: options.strictLegalBasis,
23
+ });
24
+ for (const entity of options.entities ?? []) {
25
+ registry.register(entity);
26
+ }
27
+ return registry;
28
+ },
29
+ },
30
+ {
31
+ provide: data_subject_service_1.DataSubjectService,
32
+ useFactory: (registry) => new data_subject_service_1.DataSubjectService({
33
+ registry,
34
+ requestStorage: options.requestStorage,
35
+ artifactStorage: options.artifactStorage,
36
+ slaDays: options.slaDays ?? 30,
37
+ publishOutbox: options.publishOutbox,
38
+ publishAudit: options.publishAudit,
39
+ runInTransaction: options.runInTransaction,
40
+ }),
41
+ inject: [exports.DATA_SUBJECT_REGISTRY],
42
+ },
43
+ ];
44
+ return {
45
+ module: DataSubjectModule_1,
46
+ providers,
47
+ exports: [data_subject_service_1.DataSubjectService, exports.DATA_SUBJECT_REGISTRY],
48
+ global: true,
49
+ };
50
+ }
51
+ };
52
+ exports.DataSubjectModule = DataSubjectModule;
53
+ exports.DataSubjectModule = DataSubjectModule = DataSubjectModule_1 = __decorate([
54
+ (0, common_1.Module)({})
55
+ ], DataSubjectModule);
@@ -0,0 +1,35 @@
1
+ import type { Registry } from './registry';
2
+ import type { ArtifactStorage } from './storage/artifact-storage.interface';
3
+ import type { RequestStorage } from './storage/request-storage.interface';
4
+ import type { DataSubjectRequest, RequestState } from './types';
5
+ export interface DataSubjectServiceDeps {
6
+ registry: Registry;
7
+ requestStorage: RequestStorage;
8
+ artifactStorage: ArtifactStorage;
9
+ slaDays: number;
10
+ idFactory?: () => string;
11
+ clock?: () => Date;
12
+ publishOutbox?: (type: string, payload: unknown) => Promise<void>;
13
+ publishAudit?: (event: string, data: Record<string, unknown>) => Promise<void>;
14
+ runInTransaction?: <T>(work: () => Promise<T>) => Promise<T>;
15
+ }
16
+ export declare class DataSubjectService {
17
+ private readonly deps;
18
+ private readonly idFactory;
19
+ private readonly clock;
20
+ private readonly publishOutbox;
21
+ private readonly publishAudit;
22
+ private readonly runInTransaction;
23
+ constructor(deps: DataSubjectServiceDeps);
24
+ export(subjectId: string, tenantId: string): Promise<DataSubjectRequest>;
25
+ erase(subjectId: string, tenantId: string): Promise<DataSubjectRequest>;
26
+ getRequest(id: string): Promise<DataSubjectRequest>;
27
+ listByTenant(tenantId: string, opts?: {
28
+ state?: RequestState;
29
+ }): Promise<DataSubjectRequest[]>;
30
+ listOverdue(): Promise<DataSubjectRequest[]>;
31
+ private createRequest;
32
+ private setState;
33
+ private markFailed;
34
+ private mustLoad;
35
+ }
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DataSubjectService = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const erase_runner_1 = require("./erase-runner");
6
+ const errors_1 = require("./errors");
7
+ const export_runner_1 = require("./export-runner");
8
+ class DataSubjectService {
9
+ deps;
10
+ idFactory;
11
+ clock;
12
+ publishOutbox;
13
+ publishAudit;
14
+ runInTransaction;
15
+ constructor(deps) {
16
+ this.deps = deps;
17
+ this.idFactory = deps.idFactory ?? (() => (0, node_crypto_1.randomUUID)());
18
+ this.clock = deps.clock ?? (() => new Date());
19
+ this.publishOutbox = deps.publishOutbox ?? (async () => { });
20
+ this.publishAudit = deps.publishAudit ?? (async () => { });
21
+ this.runInTransaction = deps.runInTransaction ?? (async (work) => work());
22
+ }
23
+ async export(subjectId, tenantId) {
24
+ const request = await this.createRequest('export', subjectId, tenantId);
25
+ try {
26
+ await this.setState(request.id, 'processing');
27
+ const runner = new export_runner_1.ExportRunner(this.deps.registry, this.deps.artifactStorage);
28
+ const result = await runner.run(request.id, subjectId, tenantId);
29
+ await this.deps.requestStorage.update(request.id, {
30
+ state: 'completed',
31
+ completedAt: this.clock(),
32
+ artifactHash: result.artifactHash,
33
+ artifactUrl: result.artifactUrl,
34
+ stats: result.stats,
35
+ });
36
+ await this.publishOutbox('data_subject.request_completed', {
37
+ requestId: request.id,
38
+ state: 'completed',
39
+ artifactHash: result.artifactHash,
40
+ });
41
+ }
42
+ catch (error) {
43
+ await this.markFailed(request.id, error);
44
+ await this.publishOutbox('data_subject.request_failed', {
45
+ requestId: request.id,
46
+ failureReason: messageFromError(error),
47
+ });
48
+ }
49
+ return this.mustLoad(request.id);
50
+ }
51
+ async erase(subjectId, tenantId) {
52
+ const request = await this.createRequest('erase', subjectId, tenantId);
53
+ try {
54
+ await this.runInTransaction(async () => {
55
+ await this.publishOutbox('data_subject.erasure_requested', {
56
+ requestId: request.id,
57
+ subjectId,
58
+ tenantId,
59
+ requestedAt: this.clock().toISOString(),
60
+ });
61
+ await this.setState(request.id, 'processing');
62
+ const runner = new erase_runner_1.EraseRunner(this.deps.registry);
63
+ const result = await runner.run(subjectId, tenantId);
64
+ const report = JSON.stringify({ requestId: request.id, stats: result.stats });
65
+ const artifactHash = (0, node_crypto_1.createHash)('sha256').update(report).digest('hex');
66
+ await this.deps.requestStorage.update(request.id, {
67
+ state: 'completed',
68
+ completedAt: this.clock(),
69
+ stats: result.stats,
70
+ artifactHash,
71
+ });
72
+ await this.publishOutbox('data_subject.request_completed', {
73
+ requestId: request.id,
74
+ state: 'completed',
75
+ artifactHash,
76
+ });
77
+ });
78
+ }
79
+ catch (error) {
80
+ await this.markFailed(request.id, error);
81
+ await this.publishOutbox('data_subject.request_failed', {
82
+ requestId: request.id,
83
+ failureReason: messageFromError(error),
84
+ });
85
+ }
86
+ return this.mustLoad(request.id);
87
+ }
88
+ async getRequest(id) {
89
+ return this.mustLoad(id);
90
+ }
91
+ async listByTenant(tenantId, opts = {}) {
92
+ return this.deps.requestStorage.listByTenant(tenantId, opts);
93
+ }
94
+ async listOverdue() {
95
+ return this.deps.requestStorage.listOverdue(this.clock());
96
+ }
97
+ async createRequest(type, subjectId, tenantId) {
98
+ const now = this.clock();
99
+ const dueAt = new Date(now.getTime() + this.deps.slaDays * 86_400_000);
100
+ const request = {
101
+ id: this.idFactory(),
102
+ tenantId,
103
+ subjectId,
104
+ type,
105
+ state: 'created',
106
+ createdAt: now,
107
+ dueAt,
108
+ completedAt: null,
109
+ failedAt: null,
110
+ failureReason: null,
111
+ artifactHash: null,
112
+ artifactUrl: null,
113
+ stats: null,
114
+ requestedBy: null,
115
+ };
116
+ await this.deps.requestStorage.insert(request);
117
+ await this.publishOutbox('data_subject.request_created', {
118
+ requestId: request.id,
119
+ type,
120
+ subjectId,
121
+ tenantId,
122
+ });
123
+ await this.publishAudit('data_subject.request_created', {
124
+ requestId: request.id,
125
+ type,
126
+ tenantId,
127
+ });
128
+ return request;
129
+ }
130
+ async setState(id, state) {
131
+ await this.deps.requestStorage.update(id, { state });
132
+ }
133
+ async markFailed(id, error) {
134
+ await this.deps.requestStorage.update(id, {
135
+ state: 'failed',
136
+ failedAt: this.clock(),
137
+ failureReason: messageFromError(error),
138
+ });
139
+ }
140
+ async mustLoad(id) {
141
+ const request = await this.deps.requestStorage.findById(id);
142
+ if (!request) {
143
+ throw new errors_1.DataSubjectError(errors_1.DataSubjectErrorCode.RequestNotFound, `request ${id} not found`);
144
+ }
145
+ return request;
146
+ }
147
+ }
148
+ exports.DataSubjectService = DataSubjectService;
149
+ function messageFromError(error) {
150
+ if (error instanceof Error && error.message) {
151
+ return error.message;
152
+ }
153
+ if (typeof error === 'string') {
154
+ return error;
155
+ }
156
+ try {
157
+ return JSON.stringify(error);
158
+ }
159
+ catch {
160
+ return String(error);
161
+ }
162
+ }
@@ -0,0 +1,10 @@
1
+ import type { Registry } from './registry';
2
+ import type { RequestStats } from './types';
3
+ export interface EraseResult {
4
+ stats: RequestStats;
5
+ }
6
+ export declare class EraseRunner {
7
+ private readonly registry;
8
+ constructor(registry: Registry);
9
+ run(subjectId: string, tenantId: string): Promise<EraseResult>;
10
+ }