@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.
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Pagination contracts (PLAN.md §16.1).
3
+ *
4
+ * The legacy `offset` / `limit` / `count` field names are preserved for
5
+ * migration familiarity, but nested under `meta.pagination` in the envelope.
6
+ */
7
+ /** Offset pagination requested by a client. */
8
+ export interface PaginationInput {
9
+ readonly offset?: number;
10
+ readonly limit?: number;
11
+ }
12
+ /** Offset pagination reported back to a client. */
13
+ export interface PaginationMeta {
14
+ readonly offset: number;
15
+ readonly limit: number;
16
+ /** Total number of records matching the query, ignoring offset/limit. */
17
+ readonly count: number;
18
+ }
19
+ /** A page of records plus its pagination metadata. */
20
+ export interface Paginated<T> {
21
+ readonly records: readonly T[];
22
+ readonly pagination: PaginationMeta;
23
+ }
24
+ /**
25
+ * Cursor pagination for large result sets. Replaces the legacy unbounded
26
+ * `limit: -1` (PLAN.md §12), which is rejected by the query policy.
27
+ */
28
+ export interface CursorPaginationInput {
29
+ readonly cursor?: string;
30
+ readonly limit?: number;
31
+ }
32
+ export interface CursorPaginationMeta {
33
+ readonly nextCursor: string | null;
34
+ readonly limit: number;
35
+ readonly hasMore: boolean;
36
+ }
37
+ export interface CursorPaginated<T> {
38
+ readonly records: readonly T[];
39
+ readonly pagination: CursorPaginationMeta;
40
+ }
41
+ //# sourceMappingURL=pagination.types.d.ts.map
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ /**
3
+ * Pagination contracts (PLAN.md §16.1).
4
+ *
5
+ * The legacy `offset` / `limit` / `count` field names are preserved for
6
+ * migration familiarity, but nested under `meta.pagination` in the envelope.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ //# sourceMappingURL=pagination.types.js.map
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The typed query DSL (PLAN.md §12, §14.2, §16.2).
3
+ *
4
+ * The legacy framework forwarded client-supplied `$`-operators straight to the
5
+ * ORM and accepted `limit: -1`. Here the DSL is:
6
+ * - **typed** — `where`/`sort`/`select` keys are constrained to `keyof TEntity`
7
+ * - **allow-listed** — a per-model `QueryPolicy` declares which fields and
8
+ * operators a client may use
9
+ * - **bounded** — `maxLimit` is enforced by the driver, never by the client
10
+ */
11
+ import type { FieldName, Id } from './common.types.js';
12
+ /** Comparison operators a driver must support. Named, not `$`-prefixed. */
13
+ export type ComparisonOperator = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'like' | 'ilike' | 'between' | 'isNull';
14
+ /** Operator object for a single field of type `V`. */
15
+ export interface FieldCondition<V> {
16
+ readonly eq?: V;
17
+ readonly ne?: V;
18
+ readonly gt?: V;
19
+ readonly gte?: V;
20
+ readonly lt?: V;
21
+ readonly lte?: V;
22
+ readonly in?: readonly V[];
23
+ readonly nin?: readonly V[];
24
+ readonly like?: string;
25
+ readonly ilike?: string;
26
+ readonly between?: readonly [V, V];
27
+ readonly isNull?: boolean;
28
+ }
29
+ /** A bare value is shorthand for `{ eq: value }`. */
30
+ export type WhereValue<V> = V | FieldCondition<V>;
31
+ /** Typed `where` clause with logical composition. */
32
+ export type Where<TEntity> = {
33
+ readonly [K in keyof TEntity]?: WhereValue<TEntity[K]>;
34
+ } & {
35
+ readonly and?: readonly Where<TEntity>[];
36
+ readonly or?: readonly Where<TEntity>[];
37
+ readonly not?: Where<TEntity>;
38
+ };
39
+ export type SortDirection = 'asc' | 'desc';
40
+ /** Ordered sort instructions; array order is significant. */
41
+ export type Sort<TEntity> = readonly (readonly [
42
+ field: FieldName<TEntity>,
43
+ direction: SortDirection
44
+ ])[];
45
+ /** Free-text search across the model's configured `searchFields`. */
46
+ export interface SearchInput {
47
+ readonly term: string;
48
+ /** Restrict the search to a subset of the model's searchable fields. */
49
+ readonly fields?: readonly string[];
50
+ }
51
+ /** Relation to eager-load. Nested relations use dot paths (max depth is policed). */
52
+ export type Populate = string;
53
+ /** A named, server-defined query fragment (replaces client-supplied scopes). */
54
+ export type ScopeName = string;
55
+ /** The full query a repository accepts. Every member is optional. */
56
+ export interface Query<TEntity> {
57
+ readonly where?: Where<TEntity>;
58
+ readonly select?: readonly FieldName<TEntity>[];
59
+ readonly sort?: Sort<TEntity>;
60
+ readonly populate?: readonly Populate[];
61
+ readonly scope?: readonly ScopeName[];
62
+ readonly search?: SearchInput;
63
+ readonly offset?: number;
64
+ readonly limit?: number;
65
+ /** Include soft-deleted rows. Requires an explicit permission (PLAN.md §14.2). */
66
+ readonly withDeleted?: boolean;
67
+ /** Cursor pagination; mutually exclusive with `offset`. */
68
+ readonly cursor?: string;
69
+ }
70
+ /**
71
+ * Per-model allow-list enforced before a query reaches the driver.
72
+ * Anything not listed is rejected with `INVALID_QUERY`.
73
+ */
74
+ export interface QueryPolicy<TEntity> {
75
+ readonly filterable: readonly FieldName<TEntity>[];
76
+ readonly sortable: readonly FieldName<TEntity>[];
77
+ readonly selectable: readonly FieldName<TEntity>[];
78
+ readonly searchable: readonly FieldName<TEntity>[];
79
+ readonly populatable: readonly Populate[];
80
+ readonly scopes: readonly ScopeName[];
81
+ readonly operators: readonly ComparisonOperator[];
82
+ /** Hard ceiling on `limit`. There is no unbounded query. */
83
+ readonly maxLimit: number;
84
+ readonly defaultLimit: number;
85
+ /** Maximum depth allowed in dotted `populate` paths. */
86
+ readonly maxPopulateDepth: number;
87
+ }
88
+ /** Locator for a single record. */
89
+ export interface ByIdQuery<TEntity> extends Omit<Query<TEntity>, 'offset' | 'limit' | 'cursor'> {
90
+ readonly id: Id;
91
+ }
92
+ //# sourceMappingURL=query.types.d.ts.map
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ /**
3
+ * The typed query DSL (PLAN.md §12, §14.2, §16.2).
4
+ *
5
+ * The legacy framework forwarded client-supplied `$`-operators straight to the
6
+ * ORM and accepted `limit: -1`. Here the DSL is:
7
+ * - **typed** — `where`/`sort`/`select` keys are constrained to `keyof TEntity`
8
+ * - **allow-listed** — a per-model `QueryPolicy` declares which fields and
9
+ * operators a client may use
10
+ * - **bounded** — `maxLimit` is enforced by the driver, never by the client
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ //# sourceMappingURL=query.types.js.map
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Engine-agnostic data ports (PLAN.md §14.1).
3
+ *
4
+ * `@nage-api/data-sql` and `@nage-api/data-mongo` implement these and are validated by a
5
+ * single shared conformance suite. Application code depends on the port, never
6
+ * on a driver — which is what makes the engine choice a scaffold-time decision
7
+ * instead of a runtime `if (engine === …)` branch.
8
+ */
9
+ import type { DeepPartial, Id } from './common.types.js';
10
+ import type { Paginated, CursorPaginated } from './pagination.types.js';
11
+ import type { ByIdQuery, Query } from './query.types.js';
12
+ import type { DeleteMode, ModelDescriptor } from './entity.types.js';
13
+ /** Opaque handle to an open transaction; its shape is driver-specific. */
14
+ export interface TxContext {
15
+ readonly id: string;
16
+ readonly driver: string;
17
+ }
18
+ /** Options accepted by every write operation. */
19
+ export interface WriteOptions {
20
+ readonly tx?: TxContext;
21
+ }
22
+ export interface DeleteOptions extends WriteOptions {
23
+ readonly mode?: DeleteMode;
24
+ }
25
+ /** Outcome of a bulk write. */
26
+ export interface BulkResult<TEntity> {
27
+ readonly affected: number;
28
+ readonly records: readonly TEntity[];
29
+ }
30
+ /**
31
+ * The contract every driver implements for a single model.
32
+ *
33
+ * Reads take a typed `Query`; writes take `DeepPartial<TEntity>` and an optional
34
+ * transaction context so a `UnitOfWork` can make multi-model work atomic.
35
+ */
36
+ export interface RepositoryPort<TEntity> {
37
+ readonly descriptor: ModelDescriptor<TEntity>;
38
+ findAll(query?: Query<TEntity>): Promise<Paginated<TEntity>>;
39
+ findByCursor(query?: Query<TEntity>): Promise<CursorPaginated<TEntity>>;
40
+ findOne(query?: Query<TEntity>): Promise<TEntity | null>;
41
+ findById(id: Id, query?: Omit<ByIdQuery<TEntity>, 'id'>): Promise<TEntity | null>;
42
+ exists(query?: Query<TEntity>): Promise<boolean>;
43
+ count(query?: Query<TEntity>): Promise<number>;
44
+ create(data: DeepPartial<TEntity>, options?: WriteOptions): Promise<TEntity>;
45
+ update(id: Id, data: DeepPartial<TEntity>, options?: WriteOptions): Promise<TEntity>;
46
+ delete(id: Id, options?: DeleteOptions): Promise<void>;
47
+ restore(id: Id, options?: WriteOptions): Promise<TEntity>;
48
+ bulkCreate(data: readonly DeepPartial<TEntity>[], options?: WriteOptions): Promise<BulkResult<TEntity>>;
49
+ bulkUpdate(query: Query<TEntity>, data: DeepPartial<TEntity>, options?: WriteOptions): Promise<BulkResult<TEntity>>;
50
+ bulkDelete(query: Query<TEntity>, options?: DeleteOptions): Promise<number>;
51
+ }
52
+ /**
53
+ * Transaction boundary. Lifecycle hooks run **inside** the transaction, so a
54
+ * failing `doAfterCreate` rolls the write back (PLAN.md §14.2).
55
+ */
56
+ export interface UnitOfWork {
57
+ run<TResult>(fn: (tx: TxContext) => Promise<TResult>): Promise<TResult>;
58
+ }
59
+ /** Health probe feeding `/health/ready` (PLAN.md §21). */
60
+ export interface DataSourceHealth {
61
+ ping(): Promise<boolean>;
62
+ readonly driver: string;
63
+ }
64
+ /** Pluggable session storage so auth is not hard-wired to one engine (§15.1). */
65
+ export interface KeyValueStore<TValue> {
66
+ get(key: string): Promise<TValue | null>;
67
+ set(key: string, value: TValue, ttlSeconds?: number): Promise<void>;
68
+ delete(key: string): Promise<void>;
69
+ }
70
+ //# sourceMappingURL=repository.types.d.ts.map
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ /**
3
+ * Engine-agnostic data ports (PLAN.md §14.1).
4
+ *
5
+ * `@nage-api/data-sql` and `@nage-api/data-mongo` implement these and are validated by a
6
+ * single shared conformance suite. Application code depends on the port, never
7
+ * on a driver — which is what makes the engine choice a scaffold-time decision
8
+ * instead of a runtime `if (engine === …)` branch.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ //# sourceMappingURL=repository.types.js.map
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The single API response envelope (PLAN.md §16.1).
3
+ *
4
+ * Controllers `return data`; the global `ResponseInterceptor` and
5
+ * `AllExceptionsFilter` in `@nage-api/core` build these shapes. No controller ever
6
+ * touches `res` — that was the legacy `@Res()` escape hatch this replaces.
7
+ */
8
+ import type { ErrorPayload } from './error.types.js';
9
+ import type { PaginationMeta, CursorPaginationMeta } from './pagination.types.js';
10
+ /** Metadata attached to every response, successful or not. */
11
+ export interface ResponseMeta {
12
+ readonly requestId: string;
13
+ readonly timestamp: string;
14
+ /** API version that served the request (`x-application-version`). */
15
+ readonly version?: number;
16
+ readonly pagination?: PaginationMeta | CursorPaginationMeta;
17
+ }
18
+ /** Successful response. Historically produced by `core.responses.ts#Result`. */
19
+ export interface SuccessResponse<TData> {
20
+ readonly success: true;
21
+ readonly data: TData;
22
+ readonly meta: ResponseMeta;
23
+ }
24
+ /** Failed response. `error.message` is always client-safe. */
25
+ export interface ErrorResponse {
26
+ readonly success: false;
27
+ readonly error: ErrorPayload;
28
+ readonly meta: ResponseMeta;
29
+ }
30
+ /** Discriminated union a typed HTTP client can narrow on `success`. */
31
+ export type ApiResponse<TData> = SuccessResponse<TData> | ErrorResponse;
32
+ /**
33
+ * Successful envelope, kept under the legacy name for migration familiarity
34
+ * (PLAN.md §13 — "typed `Result<TData>` replaces `any`").
35
+ */
36
+ export type Result<TData> = SuccessResponse<TData>;
37
+ /** A list response: `data` is the page, `meta.pagination` carries the counters. */
38
+ export interface PaginatedResponse<TData> extends SuccessResponse<readonly TData[]> {
39
+ readonly meta: ResponseMeta & {
40
+ readonly pagination: PaginationMeta | CursorPaginationMeta;
41
+ };
42
+ }
43
+ //# sourceMappingURL=response.types.d.ts.map
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ /**
3
+ * The single API response envelope (PLAN.md §16.1).
4
+ *
5
+ * Controllers `return data`; the global `ResponseInterceptor` and
6
+ * `AllExceptionsFilter` in `@nage-api/core` build these shapes. No controller ever
7
+ * touches `res` — that was the legacy `@Res()` escape hatch this replaces.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ //# sourceMappingURL=response.types.js.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Security ports and findings (PLAN.md §12).
3
+ *
4
+ * `@nage-api/core` implements the in-process versions; `@nage-api/cache` can back the
5
+ * rate-limit store with Redis, and `@nage-api/cli`'s `doctor` renders the findings —
6
+ * none of them importing each other.
7
+ */
8
+ /** Outcome of consuming one unit of a rate-limit budget. */
9
+ export interface RateLimitResult {
10
+ readonly allowed: boolean;
11
+ /** Requests permitted in the current window. */
12
+ readonly limit: number;
13
+ /** Requests still available; `0` once the budget is spent. */
14
+ readonly remaining: number;
15
+ /** Epoch milliseconds at which the window resets. */
16
+ readonly resetAt: number;
17
+ /** Seconds a rejected caller should wait; the `Retry-After` value. */
18
+ readonly retryAfterSeconds: number;
19
+ }
20
+ /**
21
+ * Counter store behind the rate-limit guard. The default implementation is
22
+ * per-process; a multi-instance deployment supplies a shared one — through
23
+ * `NageCoreModule.forRoot(config, { rateLimitStore })` — so a limit of 100 means
24
+ * 100 across the fleet rather than 100 per pod. No shared implementation ships.
25
+ */
26
+ export interface RateLimitStore {
27
+ readonly name: string;
28
+ /** Record one request against `key` and report the resulting budget. */
29
+ consume(key: string, limit: number, windowMs: number): Promise<RateLimitResult>;
30
+ /** Clear a key — used after a successful login resets a failure counter. */
31
+ reset(key: string): Promise<void>;
32
+ }
33
+ /** How much a security finding matters. */
34
+ export type SecuritySeverity = 'critical' | 'high' | 'medium' | 'low' | 'info';
35
+ /** Stable identifiers so a finding can be suppressed or tracked over time. */
36
+ export type SecurityFindingCode = 'SEC_CORS_WILDCARD' | 'SEC_CORS_WILDCARD_WITH_CREDENTIALS' | 'SEC_HELMET_DISABLED' | 'SEC_CSP_DISABLED' | 'SEC_HSTS_DISABLED' | 'SEC_TLS_NO_VERIFY' | 'SEC_TLS_DISABLED' | 'SEC_VALIDATION_DISABLED' | 'SEC_VALIDATION_PERMISSIVE' | 'SEC_RATE_LIMIT_DISABLED' | 'SEC_UNBOUNDED_QUERY_LIMIT' | 'SEC_MIGRATIONS_ON_BOOT' | 'SEC_DEBUG_LOGGING' | 'SEC_WEAK_JWT_ALGORITHM' | 'SEC_LONG_ACCESS_TTL' | 'SEC_REFRESH_ROTATION_DISABLED' | 'SEC_REUSE_DETECTION_DISABLED' | 'SEC_WEAK_PASSWORD_POLICY' | 'SEC_WEAK_SECRET' | 'SEC_MISSING_SECRET' | 'SEC_SHUTDOWN_HOOKS_DISABLED' | 'SEC_NO_REQUEST_TIMEOUT' | 'SEC_LEGACY_PATTERN';
37
+ export interface SecurityFinding {
38
+ readonly code: SecurityFindingCode;
39
+ readonly severity: SecuritySeverity;
40
+ /** Config path or file location the finding refers to, e.g. `http.cors.origins`. */
41
+ readonly location: string;
42
+ /** What is wrong. Never contains a secret value. */
43
+ readonly message: string;
44
+ /** What to do about it. */
45
+ readonly remediation: string;
46
+ }
47
+ //# sourceMappingURL=security.types.d.ts.map
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ /**
3
+ * Security ports and findings (PLAN.md §12).
4
+ *
5
+ * `@nage-api/core` implements the in-process versions; `@nage-api/cache` can back the
6
+ * rate-limit store with Redis, and `@nage-api/cli`'s `doctor` renders the findings —
7
+ * none of them importing each other.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ //# sourceMappingURL=security.types.js.map
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@nage-api/contracts",
3
+ "version": "1.0.0-beta.2",
4
+ "description": "Pure TypeScript contracts for the @nage-api framework — types only, no runtime, no Nest",
5
+ "license": "Apache-2.0",
6
+ "type": "commonjs",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./package.json": "./package.json"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "!dist/.tsbuildinfo",
20
+ "!dist/**/*.map",
21
+ "README.md"
22
+ ],
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "22.20.1",
28
+ "rimraf": "6.1.3",
29
+ "typescript": "5.9.3",
30
+ "vitest": "4.1.10"
31
+ },
32
+ "engines": {
33
+ "node": ">=22.0.0"
34
+ },
35
+ "scripts": {
36
+ "build": "tsc -b tsconfig.build.json",
37
+ "clean": "rimraf dist .turbo",
38
+ "typecheck": "tsc -p tsconfig.json --noEmit",
39
+ "test": "vitest run"
40
+ }
41
+ }