@nage-api/contracts 1.0.0-beta.2
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 +202 -0
- package/README.md +102 -0
- package/dist/auth.types.d.ts +70 -0
- package/dist/auth.types.js +9 -0
- package/dist/common.types.d.ts +41 -0
- package/dist/common.types.js +9 -0
- package/dist/config.types.d.ts +314 -0
- package/dist/config.types.js +16 -0
- package/dist/context.types.d.ts +34 -0
- package/dist/context.types.js +10 -0
- package/dist/entity.types.d.ts +43 -0
- package/dist/entity.types.js +10 -0
- package/dist/error.types.d.ts +55 -0
- package/dist/error.types.js +10 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +9 -0
- package/dist/job.types.d.ts +64 -0
- package/dist/job.types.js +10 -0
- package/dist/logger.types.d.ts +25 -0
- package/dist/logger.types.js +10 -0
- package/dist/pagination.types.d.ts +41 -0
- package/dist/pagination.types.js +9 -0
- package/dist/query.types.d.ts +92 -0
- package/dist/query.types.js +13 -0
- package/dist/repository.types.d.ts +70 -0
- package/dist/repository.types.js +11 -0
- package/dist/response.types.d.ts +43 -0
- package/dist/response.types.js +10 -0
- package/dist/security.types.d.ts +47 -0
- package/dist/security.types.js +10 -0
- package/package.json +41 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The typed configuration surface (PLAN.md §11).
|
|
3
|
+
*
|
|
4
|
+
* These live in `@nage-api/contracts`, not in `@nage-api/config`, for a layering reason:
|
|
5
|
+
* a feature package needs to read the shape of *its own* config block, and
|
|
6
|
+
* feature packages may not import each other. Types at the bottom of the graph
|
|
7
|
+
* are readable by everyone; `@nage-api/config` owns the runtime that produces and
|
|
8
|
+
* validates a value of this shape.
|
|
9
|
+
*
|
|
10
|
+
* Every default implied here is the safe one (§12): CORS is off until an
|
|
11
|
+
* allow-list is given, TLS verification is full, and features are disabled until
|
|
12
|
+
* switched on.
|
|
13
|
+
*/
|
|
14
|
+
export type NodeEnvironment = 'development' | 'test' | 'staging' | 'production';
|
|
15
|
+
/**
|
|
16
|
+
* Cross-origin policy. `origins` is an explicit allow-list; the literal `'*'` is
|
|
17
|
+
* accepted but must be written out, and is refused in production.
|
|
18
|
+
*/
|
|
19
|
+
export interface CorsConfig {
|
|
20
|
+
readonly origins: readonly string[] | '*';
|
|
21
|
+
readonly credentials?: boolean;
|
|
22
|
+
readonly methods?: readonly string[];
|
|
23
|
+
readonly allowedHeaders?: readonly string[];
|
|
24
|
+
readonly exposedHeaders?: readonly string[];
|
|
25
|
+
readonly maxAge?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface HelmetConfig {
|
|
28
|
+
readonly enabled?: boolean;
|
|
29
|
+
/** Content-Security-Policy. Disable only for routes serving Swagger UI. */
|
|
30
|
+
readonly contentSecurityPolicy?: boolean;
|
|
31
|
+
/** HSTS is forced on in production regardless of this flag. */
|
|
32
|
+
readonly hsts?: boolean;
|
|
33
|
+
readonly crossOriginResourcePolicy?: 'same-origin' | 'same-site' | 'cross-origin';
|
|
34
|
+
}
|
|
35
|
+
export interface HttpConfig {
|
|
36
|
+
/** Omit to disable CORS entirely — the safe default. */
|
|
37
|
+
readonly cors?: CorsConfig;
|
|
38
|
+
readonly helmet?: HelmetConfig;
|
|
39
|
+
readonly compression?: boolean;
|
|
40
|
+
readonly bodyLimit?: string;
|
|
41
|
+
/** Number of reverse proxies to trust, or `false`. Affects the client IP. */
|
|
42
|
+
readonly trustProxy?: number | boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Ceiling on how long a handler may take before the caller is answered 504
|
|
45
|
+
* (§21). Defaults to 30 seconds; `0` removes the ceiling, which means one
|
|
46
|
+
* wedged dependency can hold connections until the clients give up.
|
|
47
|
+
*/
|
|
48
|
+
readonly requestTimeoutMs?: number;
|
|
49
|
+
}
|
|
50
|
+
export interface VersioningConfig {
|
|
51
|
+
readonly enabled?: boolean;
|
|
52
|
+
/** Defaults to `x-application-version`, retained from the legacy framework. */
|
|
53
|
+
readonly header?: string;
|
|
54
|
+
readonly defaultVersion?: number;
|
|
55
|
+
/** Lowest version the API still serves; older requests are refused. */
|
|
56
|
+
readonly minVersion?: number;
|
|
57
|
+
}
|
|
58
|
+
export interface ValidationConfig {
|
|
59
|
+
readonly enabled?: boolean;
|
|
60
|
+
readonly whitelist?: boolean;
|
|
61
|
+
readonly forbidNonWhitelisted?: boolean;
|
|
62
|
+
readonly transform?: boolean;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Global request throttling (§12). The legacy framework imported a throttler but
|
|
66
|
+
* never registered a guard, so nothing was actually limited — here it is on by
|
|
67
|
+
* default and switching it off is a `doctor` finding.
|
|
68
|
+
*/
|
|
69
|
+
export interface RateLimitConfig extends FeatureToggle {
|
|
70
|
+
/** Requests permitted per window, per caller. */
|
|
71
|
+
readonly limit?: number;
|
|
72
|
+
/** Window length in milliseconds. */
|
|
73
|
+
readonly windowMs?: number;
|
|
74
|
+
/** Paths excluded from the global limit, e.g. `/health/live`. */
|
|
75
|
+
readonly excludePaths?: readonly string[];
|
|
76
|
+
/** Count authenticated callers per user rather than per IP. */
|
|
77
|
+
readonly perUser?: boolean;
|
|
78
|
+
}
|
|
79
|
+
export interface SecurityConfig {
|
|
80
|
+
readonly rateLimit?: RateLimitConfig;
|
|
81
|
+
/**
|
|
82
|
+
* Refuse to boot when the configuration contains a critical finding. On by
|
|
83
|
+
* default in production and staging (§21).
|
|
84
|
+
*/
|
|
85
|
+
readonly assertOnBoot?: boolean;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Graceful shutdown (§21).
|
|
89
|
+
*
|
|
90
|
+
* The three deadlines exist because each stage can be held up by a different
|
|
91
|
+
* party: the load balancer (`readinessDelayMs`), a slow handler
|
|
92
|
+
* (`drainTimeoutMs`), and a module whose `close()` never returns
|
|
93
|
+
* (`forceExitAfterMs`). A single timeout would let the slowest one consume the
|
|
94
|
+
* whole budget and leave the container to be `SIGKILL`ed mid-write.
|
|
95
|
+
*/
|
|
96
|
+
export interface ShutdownConfig {
|
|
97
|
+
/** Registers `SIGTERM`/`SIGINT` hooks so pools drain before exit (§21). */
|
|
98
|
+
readonly enabled?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Time to keep serving after readiness starts failing, before the listener
|
|
101
|
+
* closes. Orchestrator endpoint removal is asynchronous, so a pod that stops
|
|
102
|
+
* listening the instant it is signalled refuses requests that were routed to
|
|
103
|
+
* it a moment earlier. Defaults to 0 — set it to your readiness period plus a
|
|
104
|
+
* margin (typically 5000) behind a load balancer.
|
|
105
|
+
*/
|
|
106
|
+
readonly readinessDelayMs?: number;
|
|
107
|
+
/** How long in-flight requests may take to finish. Defaults to 10 seconds. */
|
|
108
|
+
readonly drainTimeoutMs?: number;
|
|
109
|
+
/**
|
|
110
|
+
* Hard deadline for the whole sequence. On expiry the process exits non-zero
|
|
111
|
+
* rather than waiting for a hook that will never return. Defaults to 30
|
|
112
|
+
* seconds — keep it below the orchestrator's `terminationGracePeriodSeconds`,
|
|
113
|
+
* or the `SIGKILL` arrives first and the deadline never applies.
|
|
114
|
+
*/
|
|
115
|
+
readonly forceExitAfterMs?: number;
|
|
116
|
+
}
|
|
117
|
+
export interface AppConfig {
|
|
118
|
+
readonly name: string;
|
|
119
|
+
readonly environment: NodeEnvironment;
|
|
120
|
+
readonly port?: number;
|
|
121
|
+
readonly host?: string;
|
|
122
|
+
readonly globalPrefix?: string;
|
|
123
|
+
/** Current API version served by this deployment. */
|
|
124
|
+
readonly version?: number;
|
|
125
|
+
}
|
|
126
|
+
export interface LoggingConfig {
|
|
127
|
+
readonly level?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
|
128
|
+
/** Field names redacted from every log line (§18). */
|
|
129
|
+
readonly redact?: readonly string[];
|
|
130
|
+
/** Emit newline-delimited JSON. Off means human-readable lines for dev. */
|
|
131
|
+
readonly json?: boolean;
|
|
132
|
+
}
|
|
133
|
+
/** What `@nage-api/core` needs in order to boot an application. */
|
|
134
|
+
export interface NageCoreConfig {
|
|
135
|
+
readonly app: AppConfig;
|
|
136
|
+
readonly http?: HttpConfig;
|
|
137
|
+
readonly versioning?: VersioningConfig;
|
|
138
|
+
readonly validation?: ValidationConfig;
|
|
139
|
+
readonly shutdown?: ShutdownConfig;
|
|
140
|
+
readonly logging?: LoggingConfig;
|
|
141
|
+
readonly security?: SecurityConfig;
|
|
142
|
+
}
|
|
143
|
+
/** Every optional feature is off until it is switched on. */
|
|
144
|
+
export interface FeatureToggle {
|
|
145
|
+
readonly enabled?: boolean;
|
|
146
|
+
}
|
|
147
|
+
export type SqlDialect = 'postgres' | 'mysql' | 'mariadb' | 'sqlite';
|
|
148
|
+
export type DatabaseDriver = SqlDialect | 'mongodb';
|
|
149
|
+
/**
|
|
150
|
+
* TLS mode for the database connection. `no-verify` exists so that disabling
|
|
151
|
+
* certificate validation is a deliberate, greppable choice — the legacy
|
|
152
|
+
* `rejectUnauthorized: false` was neither (§12).
|
|
153
|
+
*/
|
|
154
|
+
export type TlsMode = 'disable' | 'require' | 'verify-full' | 'no-verify';
|
|
155
|
+
export interface PoolConfig {
|
|
156
|
+
readonly min?: number;
|
|
157
|
+
readonly max?: number;
|
|
158
|
+
readonly idleTimeoutMs?: number;
|
|
159
|
+
readonly acquireTimeoutMs?: number;
|
|
160
|
+
}
|
|
161
|
+
export interface DatabaseConfig extends FeatureToggle {
|
|
162
|
+
/** One driver per workspace; the other is never installed (§14.1). */
|
|
163
|
+
readonly driver: DatabaseDriver;
|
|
164
|
+
readonly url?: string;
|
|
165
|
+
readonly host?: string;
|
|
166
|
+
readonly port?: number;
|
|
167
|
+
readonly database?: string;
|
|
168
|
+
readonly username?: string;
|
|
169
|
+
readonly schema?: string;
|
|
170
|
+
readonly pool?: PoolConfig;
|
|
171
|
+
readonly ssl?: TlsMode;
|
|
172
|
+
readonly migrations?: {
|
|
173
|
+
readonly directory?: string;
|
|
174
|
+
readonly table?: string;
|
|
175
|
+
/** Run pending migrations on boot. Off in production by default (§21). */
|
|
176
|
+
readonly runOnBoot?: boolean;
|
|
177
|
+
};
|
|
178
|
+
readonly seeds?: {
|
|
179
|
+
readonly directory?: string;
|
|
180
|
+
};
|
|
181
|
+
/** Hard ceiling applied to every model's query policy (§12). */
|
|
182
|
+
readonly maxQueryLimit?: number;
|
|
183
|
+
}
|
|
184
|
+
export interface JwtConfig {
|
|
185
|
+
/** RS256 by default — asymmetric, so verifiers never hold a signing key. */
|
|
186
|
+
readonly algorithm?: 'RS256' | 'RS512' | 'ES256' | 'HS256';
|
|
187
|
+
readonly accessTtl?: string;
|
|
188
|
+
readonly issuer: string;
|
|
189
|
+
readonly audience?: string | readonly string[];
|
|
190
|
+
/** Key id published in the JWT header, so keys can be rotated. */
|
|
191
|
+
readonly keyId?: string;
|
|
192
|
+
}
|
|
193
|
+
export interface RefreshTokenConfig {
|
|
194
|
+
readonly ttl?: string;
|
|
195
|
+
readonly rotate?: boolean;
|
|
196
|
+
readonly reuseDetection?: boolean;
|
|
197
|
+
}
|
|
198
|
+
export interface PasswordPolicyConfig {
|
|
199
|
+
readonly algorithm?: 'argon2id' | 'bcrypt';
|
|
200
|
+
readonly minLength?: number;
|
|
201
|
+
readonly requireMixedCase?: boolean;
|
|
202
|
+
readonly requireNumber?: boolean;
|
|
203
|
+
readonly requireSymbol?: boolean;
|
|
204
|
+
/** Check candidates against a breached-password list. */
|
|
205
|
+
readonly breachCheck?: boolean;
|
|
206
|
+
}
|
|
207
|
+
export interface OtpConfig {
|
|
208
|
+
readonly length?: number;
|
|
209
|
+
readonly ttl?: string;
|
|
210
|
+
readonly maxAttempts?: number;
|
|
211
|
+
readonly resendCooldown?: string;
|
|
212
|
+
}
|
|
213
|
+
export interface LockoutConfig {
|
|
214
|
+
readonly maxAttempts?: number;
|
|
215
|
+
readonly window?: string;
|
|
216
|
+
readonly duration?: string;
|
|
217
|
+
}
|
|
218
|
+
export interface AuthConfig extends FeatureToggle {
|
|
219
|
+
readonly strategy?: 'jwt';
|
|
220
|
+
readonly jwt?: JwtConfig;
|
|
221
|
+
readonly refresh?: RefreshTokenConfig;
|
|
222
|
+
readonly password?: PasswordPolicyConfig;
|
|
223
|
+
readonly otp?: OtpConfig;
|
|
224
|
+
readonly lockout?: LockoutConfig;
|
|
225
|
+
readonly apiKeys?: FeatureToggle;
|
|
226
|
+
}
|
|
227
|
+
export interface CacheConfig extends FeatureToggle {
|
|
228
|
+
readonly driver?: 'memory' | 'redis' | 'two-tier';
|
|
229
|
+
readonly url?: string;
|
|
230
|
+
readonly ttl?: string;
|
|
231
|
+
readonly namespace?: string;
|
|
232
|
+
}
|
|
233
|
+
export interface QueueConfig extends FeatureToggle {
|
|
234
|
+
readonly driver?: 'bullmq';
|
|
235
|
+
readonly url?: string;
|
|
236
|
+
readonly prefix?: string;
|
|
237
|
+
readonly concurrency?: number;
|
|
238
|
+
/** Keep a durable record of each job run (the legacy `JobLog`). */
|
|
239
|
+
readonly jobLogs?: boolean;
|
|
240
|
+
}
|
|
241
|
+
export interface StorageConfig extends FeatureToggle {
|
|
242
|
+
readonly cdn?: 'local' | 's3' | 'azure';
|
|
243
|
+
readonly bucket?: string;
|
|
244
|
+
readonly region?: string;
|
|
245
|
+
readonly publicBaseUrl?: string;
|
|
246
|
+
readonly validation?: {
|
|
247
|
+
readonly maxSize?: string;
|
|
248
|
+
readonly mime?: readonly string[];
|
|
249
|
+
/** Verify the declared MIME type against the file's magic bytes (§12). */
|
|
250
|
+
readonly magicBytes?: boolean;
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
export interface RealtimeConfig extends FeatureToggle {
|
|
254
|
+
readonly adapter?: 'memory' | 'redis';
|
|
255
|
+
readonly url?: string;
|
|
256
|
+
readonly path?: string;
|
|
257
|
+
}
|
|
258
|
+
export interface NotifyConfig extends FeatureToggle {
|
|
259
|
+
readonly channels?: readonly ('email' | 'sms' | 'push')[];
|
|
260
|
+
readonly from?: string;
|
|
261
|
+
readonly templateDirectory?: string;
|
|
262
|
+
}
|
|
263
|
+
export interface ObservabilityConfig {
|
|
264
|
+
readonly logging?: 'json' | 'pino' | 'pretty';
|
|
265
|
+
readonly tracing?: FeatureToggle & {
|
|
266
|
+
readonly endpoint?: string;
|
|
267
|
+
readonly sampleRatio?: number;
|
|
268
|
+
};
|
|
269
|
+
readonly metrics?: FeatureToggle & {
|
|
270
|
+
readonly path?: string;
|
|
271
|
+
};
|
|
272
|
+
readonly sentry?: FeatureToggle & {
|
|
273
|
+
readonly dsn?: string;
|
|
274
|
+
};
|
|
275
|
+
readonly redact?: readonly string[];
|
|
276
|
+
}
|
|
277
|
+
/** Where secrets come from (§11.1 item 5). Never `nage.config.ts`. */
|
|
278
|
+
export interface SecretsConfig {
|
|
279
|
+
readonly provider?: 'env' | 'aws' | 'vault';
|
|
280
|
+
/** Prefix applied to every lookup, e.g. `prod/my-app/`. */
|
|
281
|
+
readonly prefix?: string;
|
|
282
|
+
readonly region?: string;
|
|
283
|
+
/** Cache resolved secrets for this long; `0` disables caching. */
|
|
284
|
+
readonly cacheTtlSeconds?: number;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* The complete configuration of an application: what core needs, plus one block
|
|
288
|
+
* per optional feature. `@nage-api/config`'s `defineConfig()` produces it.
|
|
289
|
+
*/
|
|
290
|
+
export interface NageConfig extends NageCoreConfig {
|
|
291
|
+
readonly database?: DatabaseConfig;
|
|
292
|
+
readonly auth?: AuthConfig;
|
|
293
|
+
readonly cache?: CacheConfig;
|
|
294
|
+
readonly queue?: QueueConfig;
|
|
295
|
+
readonly storage?: StorageConfig;
|
|
296
|
+
readonly realtime?: RealtimeConfig;
|
|
297
|
+
readonly notify?: NotifyConfig;
|
|
298
|
+
readonly observability?: ObservabilityConfig;
|
|
299
|
+
readonly secrets?: SecretsConfig;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Pluggable secret source (§11.1, §12). Implementations live in `@nage-api/config`;
|
|
303
|
+
* consumers depend on this port, so moving from env vars to AWS Secrets Manager
|
|
304
|
+
* or Vault is a configuration change, not a code change.
|
|
305
|
+
*/
|
|
306
|
+
export interface SecretProviderPort {
|
|
307
|
+
/** Identifier used in diagnostics, e.g. `env` or `aws-secrets-manager`. */
|
|
308
|
+
readonly provider: string;
|
|
309
|
+
/** Resolve a secret, or `null` when it is not set. */
|
|
310
|
+
get(name: string): Promise<string | null>;
|
|
311
|
+
/** Resolve a secret, or reject when it is missing. */
|
|
312
|
+
require(name: string): Promise<string>;
|
|
313
|
+
}
|
|
314
|
+
//# sourceMappingURL=config.types.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The typed configuration surface (PLAN.md §11).
|
|
4
|
+
*
|
|
5
|
+
* These live in `@nage-api/contracts`, not in `@nage-api/config`, for a layering reason:
|
|
6
|
+
* a feature package needs to read the shape of *its own* config block, and
|
|
7
|
+
* feature packages may not import each other. Types at the bottom of the graph
|
|
8
|
+
* are readable by everyone; `@nage-api/config` owns the runtime that produces and
|
|
9
|
+
* validates a value of this shape.
|
|
10
|
+
*
|
|
11
|
+
* Every default implied here is the safe one (§12): CORS is off until an
|
|
12
|
+
* allow-list is given, TLS verification is full, and features are disabled until
|
|
13
|
+
* switched on.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
//# sourceMappingURL=config.types.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-request context (PLAN.md §18).
|
|
3
|
+
*
|
|
4
|
+
* The legacy `ClsStore` was declared but never populated. Here the context is
|
|
5
|
+
* the carrier for the correlation id that must appear in every log line, every
|
|
6
|
+
* response envelope, and every queue job spawned from a request.
|
|
7
|
+
*/
|
|
8
|
+
import type { Id } from './common.types.js';
|
|
9
|
+
import type { AuthUser, RoleName } from './auth.types.js';
|
|
10
|
+
/** Correlation id accepted from `X-Request-Id` or generated with a CSPRNG. */
|
|
11
|
+
export type RequestId = string;
|
|
12
|
+
export interface RequestContext<TRole extends string = RoleName> {
|
|
13
|
+
readonly requestId: RequestId;
|
|
14
|
+
/** Upstream trace id when the caller is another service. */
|
|
15
|
+
readonly correlationId?: RequestId;
|
|
16
|
+
readonly user?: AuthUser<TRole>;
|
|
17
|
+
readonly tenantId?: Id;
|
|
18
|
+
readonly ip?: string;
|
|
19
|
+
readonly userAgent?: string;
|
|
20
|
+
/** Value of the `x-application-version` header (PLAN.md §16.2). */
|
|
21
|
+
readonly apiVersion?: number;
|
|
22
|
+
readonly locale?: string;
|
|
23
|
+
/** `Date.now()` at the start of the request; used for duration logging. */
|
|
24
|
+
readonly startedAt: number;
|
|
25
|
+
}
|
|
26
|
+
/** Read/write accessor over the ambient context, implemented in `@nage-api/core`. */
|
|
27
|
+
export interface ContextStore<TRole extends string = RoleName> {
|
|
28
|
+
get(): RequestContext<TRole> | undefined;
|
|
29
|
+
/** Throws `INTERNAL_ERROR` when called outside a request scope. */
|
|
30
|
+
require(): RequestContext<TRole>;
|
|
31
|
+
run<TResult>(context: RequestContext<TRole>, fn: () => TResult): TResult;
|
|
32
|
+
set<TKey extends keyof RequestContext<TRole>>(key: TKey, value: RequestContext<TRole>[TKey]): void;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=context.types.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Per-request context (PLAN.md §18).
|
|
4
|
+
*
|
|
5
|
+
* The legacy `ClsStore` was declared but never populated. Here the context is
|
|
6
|
+
* the carrier for the correlation id that must appear in every log line, every
|
|
7
|
+
* response envelope, and every queue job spawned from a request.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
//# sourceMappingURL=context.types.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base entity shape (PLAN.md §14.2).
|
|
3
|
+
*
|
|
4
|
+
* Audit columns are populated from the request context (CLS) automatically, so
|
|
5
|
+
* services never set them by hand. Soft-delete semantics are unified across
|
|
6
|
+
* drivers: `deleted_at` is the single source of truth, `deleted` is derived.
|
|
7
|
+
*/
|
|
8
|
+
import type { Id, IsoDateString } from './common.types.js';
|
|
9
|
+
/** Who/when a record was created and last changed. */
|
|
10
|
+
export interface AuditFields {
|
|
11
|
+
readonly created_at: IsoDateString;
|
|
12
|
+
readonly updated_at: IsoDateString;
|
|
13
|
+
readonly created_by?: Id | null;
|
|
14
|
+
readonly updated_by?: Id | null;
|
|
15
|
+
}
|
|
16
|
+
/** Soft-delete marker. A driver must exclude these rows unless `withDeleted`. */
|
|
17
|
+
export interface SoftDeleteFields {
|
|
18
|
+
readonly deleted_at?: IsoDateString | null;
|
|
19
|
+
readonly deleted_by?: Id | null;
|
|
20
|
+
}
|
|
21
|
+
/** Optimistic locking (opt-in per model). */
|
|
22
|
+
export interface VersionFields {
|
|
23
|
+
readonly version: number;
|
|
24
|
+
}
|
|
25
|
+
/** Every framework-managed entity carries an id plus audit and delete markers. */
|
|
26
|
+
export interface BaseEntity extends AuditFields, SoftDeleteFields {
|
|
27
|
+
readonly id: Id;
|
|
28
|
+
}
|
|
29
|
+
/** Fields the framework owns; project code must never write them directly. */
|
|
30
|
+
export type ManagedFields = keyof AuditFields | keyof SoftDeleteFields | 'id' | 'version';
|
|
31
|
+
/** The writable surface of an entity — what a create/update DTO may contain. */
|
|
32
|
+
export type Writable<TEntity> = Omit<TEntity, ManagedFields & keyof TEntity>;
|
|
33
|
+
/** Soft vs hard delete (PLAN.md §14.1). */
|
|
34
|
+
export type DeleteMode = 'soft' | 'hard';
|
|
35
|
+
/** Model-level metadata a repository needs at runtime. */
|
|
36
|
+
export interface ModelDescriptor<TEntity> {
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly primaryKey: Extract<keyof TEntity, string>;
|
|
39
|
+
readonly softDelete: boolean;
|
|
40
|
+
readonly timestamps: boolean;
|
|
41
|
+
readonly versioned: boolean;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=entity.types.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Base entity shape (PLAN.md §14.2).
|
|
4
|
+
*
|
|
5
|
+
* Audit columns are populated from the request context (CLS) automatically, so
|
|
6
|
+
* services never set them by hand. Soft-delete semantics are unified across
|
|
7
|
+
* drivers: `deleted_at` is the single source of truth, `deleted` is derived.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
//# sourceMappingURL=entity.types.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable, machine-readable error codes (PLAN.md §17).
|
|
3
|
+
*
|
|
4
|
+
* Clients program against the **code**, never the message. Messages are
|
|
5
|
+
* client-safe strings; internal detail (stack, SQL, upstream body) is logged
|
|
6
|
+
* against the request id and never serialized to the client.
|
|
7
|
+
*/
|
|
8
|
+
import type { UnknownRecord } from './common.types.js';
|
|
9
|
+
/** Input the caller sent is malformed or fails validation. */
|
|
10
|
+
export type ValidationErrorCode = 'VALIDATION_FAILED' | 'INVALID_QUERY' | 'QUERY_LIMIT_EXCEEDED'
|
|
11
|
+
/** The request body exceeded `http.bodyLimit`. */
|
|
12
|
+
| 'PAYLOAD_TOO_LARGE';
|
|
13
|
+
/** The caller could not be identified. */
|
|
14
|
+
export type AuthenticationErrorCode = 'AUTH_INVALID_CREDENTIALS' | 'AUTH_TOKEN_EXPIRED' | 'AUTH_TOKEN_INVALID' | 'AUTH_TOKEN_REUSED' | 'AUTH_SESSION_REVOKED' | 'AUTH_ACCOUNT_LOCKED' | 'AUTH_OTP_INVALID' | 'AUTH_OTP_EXPIRED' | 'AUTH_REQUIRED';
|
|
15
|
+
/** The caller is known but not allowed. */
|
|
16
|
+
export type AuthorizationErrorCode = 'FORBIDDEN' | 'INSUFFICIENT_ROLE' | 'INSUFFICIENT_PERMISSION' | 'POLICY_DENIED';
|
|
17
|
+
/** Resource lifecycle problems. */
|
|
18
|
+
export type ResourceErrorCode = 'RESOURCE_NOT_FOUND' | 'RESOURCE_CONFLICT' | 'RESOURCE_GONE' | 'OPTIMISTIC_LOCK_CONFLICT';
|
|
19
|
+
/** Infrastructure and upstream failures. */
|
|
20
|
+
export type InfrastructureErrorCode = 'RATE_LIMIT_EXCEEDED' | 'REQUEST_TIMEOUT' | 'EXTERNAL_SERVICE_ERROR' | 'DATABASE_ERROR' | 'TRANSACTION_FAILED' | 'CONFIGURATION_INVALID' | 'UNSUPPORTED_OPERATION' | 'INTERNAL_ERROR';
|
|
21
|
+
/** Business-rule violations raised by application code. */
|
|
22
|
+
export type DomainErrorCode = 'DOMAIN_RULE_VIOLATED' | 'PRECONDITION_FAILED';
|
|
23
|
+
/** The complete, closed catalog of error codes the framework can emit. */
|
|
24
|
+
export type ErrorCode = ValidationErrorCode | AuthenticationErrorCode | AuthorizationErrorCode | ResourceErrorCode | InfrastructureErrorCode | DomainErrorCode;
|
|
25
|
+
/** Per-field validation feedback, mirroring `class-validator` constraint maps. */
|
|
26
|
+
export interface ErrorDetail {
|
|
27
|
+
/** Dot-path of the offending field, e.g. `address.postcode`. */
|
|
28
|
+
readonly field?: string;
|
|
29
|
+
/** Constraint name → human-readable message. */
|
|
30
|
+
readonly constraints?: Readonly<Record<string, string>>;
|
|
31
|
+
/** Free-form message when the problem is not field-scoped. */
|
|
32
|
+
readonly message?: string;
|
|
33
|
+
}
|
|
34
|
+
/** The `error` member of an error envelope (PLAN.md §16.1). */
|
|
35
|
+
export interface ErrorPayload {
|
|
36
|
+
readonly code: ErrorCode;
|
|
37
|
+
/** Client-safe message. Never contains stack traces, SQL or upstream bodies. */
|
|
38
|
+
readonly message: string;
|
|
39
|
+
readonly details?: readonly ErrorDetail[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Server-side-only error metadata. Logged with the correlation id;
|
|
43
|
+
* **never** serialized into a response.
|
|
44
|
+
*/
|
|
45
|
+
export interface ErrorMetadata extends UnknownRecord {
|
|
46
|
+
readonly cause?: unknown;
|
|
47
|
+
}
|
|
48
|
+
/** Shape every framework error class satisfies (implemented in `@nage-api/core`). */
|
|
49
|
+
export interface NageErrorLike {
|
|
50
|
+
readonly code: ErrorCode;
|
|
51
|
+
readonly httpStatus: number;
|
|
52
|
+
readonly safeMessage: string;
|
|
53
|
+
readonly meta?: ErrorMetadata;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=error.types.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Stable, machine-readable error codes (PLAN.md §17).
|
|
4
|
+
*
|
|
5
|
+
* Clients program against the **code**, never the message. Messages are
|
|
6
|
+
* client-safe strings; internal detail (stack, SQL, upstream body) is logged
|
|
7
|
+
* against the request id and never serialized to the client.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
//# sourceMappingURL=error.types.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nage-api/contracts` — the bottom of the dependency graph (PLAN.md §7.2).
|
|
3
|
+
*
|
|
4
|
+
* Types only: no runtime code, no NestJS, no validation libraries. Every other
|
|
5
|
+
* `@nage-api/*` package and every generated app may import this; it imports nothing.
|
|
6
|
+
*/
|
|
7
|
+
export type * from './common.types.js';
|
|
8
|
+
export type * from './config.types.js';
|
|
9
|
+
export type * from './error.types.js';
|
|
10
|
+
export type * from './pagination.types.js';
|
|
11
|
+
export type * from './query.types.js';
|
|
12
|
+
export type * from './entity.types.js';
|
|
13
|
+
export type * from './auth.types.js';
|
|
14
|
+
export type * from './context.types.js';
|
|
15
|
+
export type * from './logger.types.js';
|
|
16
|
+
export type * from './response.types.js';
|
|
17
|
+
export type * from './security.types.js';
|
|
18
|
+
export type * from './job.types.js';
|
|
19
|
+
export type * from './repository.types.js';
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `@nage-api/contracts` — the bottom of the dependency graph (PLAN.md §7.2).
|
|
4
|
+
*
|
|
5
|
+
* Types only: no runtime code, no NestJS, no validation libraries. Every other
|
|
6
|
+
* `@nage-api/*` package and every generated app may import this; it imports nothing.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `Job` work unit (PLAN.md §13, §14.1).
|
|
3
|
+
*
|
|
4
|
+
* The legacy `Job` was the framework's best idea and its worst type: `body: any`,
|
|
5
|
+
* `records: any[]`, `payload: any` defeated the generic `ModelService<M>`.
|
|
6
|
+
* Here every member is typed against the entity it operates on.
|
|
7
|
+
*/
|
|
8
|
+
import type { DeepPartial, Id, UnknownRecord } from './common.types.js';
|
|
9
|
+
import type { AuthUser, RoleName } from './auth.types.js';
|
|
10
|
+
import type { Query } from './query.types.js';
|
|
11
|
+
import type { RequestContext } from './context.types.js';
|
|
12
|
+
import type { DeleteMode } from './entity.types.js';
|
|
13
|
+
/** The lifecycle operation a job represents. */
|
|
14
|
+
export type JobAction = 'findAll' | 'findOne' | 'findById' | 'count' | 'create' | 'update' | 'delete' | 'restore' | 'bulkCreate' | 'bulkUpdate' | 'bulkDelete';
|
|
15
|
+
/**
|
|
16
|
+
* A unit of work handed to `ModelService` lifecycle hooks
|
|
17
|
+
* (`doBeforeCreate`, `doAfterFindAll`, …).
|
|
18
|
+
*
|
|
19
|
+
* @typeParam TEntity the model the job operates on
|
|
20
|
+
* @typeParam TBody the write payload (defaults to a partial entity)
|
|
21
|
+
* @typeParam TParams route/DSL parameters supplied by the caller
|
|
22
|
+
*/
|
|
23
|
+
export interface Job<TEntity, TBody = DeepPartial<TEntity>, TParams extends UnknownRecord = UnknownRecord, TRole extends string = RoleName> {
|
|
24
|
+
readonly action: JobAction;
|
|
25
|
+
/** Route/path parameters and any non-DSL inputs. */
|
|
26
|
+
readonly params: TParams;
|
|
27
|
+
/** Parsed, validated and allow-listed query DSL. */
|
|
28
|
+
query: Query<TEntity>;
|
|
29
|
+
/** Validated request body for write actions. */
|
|
30
|
+
body: TBody;
|
|
31
|
+
/** Target record id for single-record actions. */
|
|
32
|
+
readonly id?: Id;
|
|
33
|
+
/** The authenticated caller; used by ownership-scoping hooks. */
|
|
34
|
+
readonly owner?: AuthUser<TRole>;
|
|
35
|
+
readonly context: RequestContext<TRole>;
|
|
36
|
+
readonly deleteMode?: DeleteMode;
|
|
37
|
+
/** Populated by the service after the data layer runs. */
|
|
38
|
+
record?: TEntity;
|
|
39
|
+
records?: TEntity[];
|
|
40
|
+
/** Total matching rows for list actions. */
|
|
41
|
+
count?: number;
|
|
42
|
+
}
|
|
43
|
+
/** A job whose result set is known to be loaded (post-hook narrowing). */
|
|
44
|
+
export interface CompletedJob<TEntity, TBody = DeepPartial<TEntity>> extends Job<TEntity, TBody> {
|
|
45
|
+
readonly records: TEntity[];
|
|
46
|
+
readonly count: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Typed background-job contract for `@nage-api/queue` (PLAN.md §8).
|
|
50
|
+
* Publisher and consumer share this type, so payloads cannot drift.
|
|
51
|
+
*/
|
|
52
|
+
export interface QueueJob<TName extends string, TPayload> {
|
|
53
|
+
readonly name: TName;
|
|
54
|
+
readonly payload: TPayload;
|
|
55
|
+
/** Correlation id propagated from the request that enqueued the job. */
|
|
56
|
+
readonly requestId?: string;
|
|
57
|
+
readonly attempts?: number;
|
|
58
|
+
readonly scheduledAt?: number;
|
|
59
|
+
}
|
|
60
|
+
/** Registry mapping job names to payload types; consumers index into it. */
|
|
61
|
+
export type QueueJobMap = Record<string, unknown>;
|
|
62
|
+
/** Handler for one entry of a `QueueJobMap`. */
|
|
63
|
+
export type QueueJobHandler<TMap extends QueueJobMap, TName extends keyof TMap & string> = (job: QueueJob<TName, TMap[TName]>) => Promise<void>;
|
|
64
|
+
//# sourceMappingURL=job.types.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The `Job` work unit (PLAN.md §13, §14.1).
|
|
4
|
+
*
|
|
5
|
+
* The legacy `Job` was the framework's best idea and its worst type: `body: any`,
|
|
6
|
+
* `records: any[]`, `payload: any` defeated the generic `ModelService<M>`.
|
|
7
|
+
* Here every member is typed against the entity it operates on.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
//# sourceMappingURL=job.types.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured-logging port (PLAN.md §18).
|
|
3
|
+
*
|
|
4
|
+
* `@nage-api/core` ships a minimal JSON implementation so correlation ids reach
|
|
5
|
+
* stdout from day one; `@nage-api/observability` swaps in pino/OTel behind the same
|
|
6
|
+
* port without any caller changing.
|
|
7
|
+
*/
|
|
8
|
+
import type { UnknownRecord } from './common.types.js';
|
|
9
|
+
export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
|
10
|
+
/**
|
|
11
|
+
* Structured fields attached to a log line. The request id is added by the
|
|
12
|
+
* logger itself, so callers never have to remember it.
|
|
13
|
+
*/
|
|
14
|
+
export type LogFields = UnknownRecord;
|
|
15
|
+
export interface LoggerPort {
|
|
16
|
+
trace(message: string, fields?: LogFields): void;
|
|
17
|
+
debug(message: string, fields?: LogFields): void;
|
|
18
|
+
info(message: string, fields?: LogFields): void;
|
|
19
|
+
warn(message: string, fields?: LogFields): void;
|
|
20
|
+
error(message: string, fields?: LogFields): void;
|
|
21
|
+
fatal(message: string, fields?: LogFields): void;
|
|
22
|
+
/** Derive a logger that adds `bindings` to every line (e.g. a module name). */
|
|
23
|
+
child(bindings: LogFields): LoggerPort;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=logger.types.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Structured-logging port (PLAN.md §18).
|
|
4
|
+
*
|
|
5
|
+
* `@nage-api/core` ships a minimal JSON implementation so correlation ids reach
|
|
6
|
+
* stdout from day one; `@nage-api/observability` swaps in pino/OTel behind the same
|
|
7
|
+
* port without any caller changing.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
//# sourceMappingURL=logger.types.js.map
|