@gobing-ai/ts-utils 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/README.md +220 -0
- package/dist/access.d.ts +12 -0
- package/dist/access.d.ts.map +1 -0
- package/dist/access.js +75 -0
- package/dist/api-response.d.ts +47 -0
- package/dist/api-response.d.ts.map +1 -0
- package/dist/api-response.js +68 -0
- package/dist/const.d.ts +3 -0
- package/dist/const.d.ts.map +1 -0
- package/dist/const.js +2 -0
- package/dist/cursor.d.ts +20 -0
- package/dist/cursor.d.ts.map +1 -0
- package/dist/cursor.js +61 -0
- package/dist/date.d.ts +4 -0
- package/dist/date.d.ts.map +1 -0
- package/dist/date.js +19 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +43 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +17 -0
- package/dist/origin.d.ts +4 -0
- package/dist/origin.d.ts.map +1 -0
- package/dist/origin.js +30 -0
- package/dist/output.d.ts +16 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +39 -0
- package/package.json +40 -0
- package/src/access.ts +77 -0
- package/src/api-response.ts +107 -0
- package/src/const.ts +3 -0
- package/src/cursor.ts +82 -0
- package/src/date.ts +18 -0
- package/src/errors.ts +53 -0
- package/src/index.ts +8 -0
- package/src/origin.ts +34 -0
- package/src/output.ts +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# @gobing-ai/ts-utils
|
|
2
|
+
|
|
3
|
+
Shared utilities — error types, date helpers, cursor-based pagination, role-based access control, API response builders, and output formatting. No platform dependencies; works in Bun, Node, and Cloudflare Workers.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`ts-utils` provides small, composable utilities used across `ts-runtime`, `ts-db`, `ts-infra`, and application code. Each module is self-contained with zero external dependencies.
|
|
8
|
+
|
|
9
|
+
| Module | Purpose |
|
|
10
|
+
|--------|---------|
|
|
11
|
+
| `errors` | Typed application errors (`AppError`, `NotFoundError`, `ValidationError`, etc.) |
|
|
12
|
+
| `date` | `nowMs()`, timestamp ↔ Date conversion |
|
|
13
|
+
| `cursor` | Base64url-encoded cursor-based pagination |
|
|
14
|
+
| `access` | Zitadel + generic role-based access control (`hasRole`, `getRoles`) |
|
|
15
|
+
| `api-response` | Standard API response envelope builders |
|
|
16
|
+
| `output` | Human-readable output helpers (tables, JSON, etc.) |
|
|
17
|
+
| `origin` | Request origin parsing |
|
|
18
|
+
| `const` | Shared constants |
|
|
19
|
+
|
|
20
|
+
## Architecture
|
|
21
|
+
|
|
22
|
+
```mermaid
|
|
23
|
+
classDiagram
|
|
24
|
+
class AppError {
|
|
25
|
+
+ErrorCode code
|
|
26
|
+
+string message
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class NotFoundError {
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class ValidationError {
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class ConflictError {
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class InternalError {
|
|
39
|
+
+unknown cause?
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class ErrorCode {
|
|
43
|
+
<<enumeration>>
|
|
44
|
+
NotFound
|
|
45
|
+
Validation
|
|
46
|
+
Conflict
|
|
47
|
+
Internal
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class CursorData {
|
|
51
|
+
<<interface>>
|
|
52
|
+
+string id
|
|
53
|
+
+number createdAt?
|
|
54
|
+
+number offset?
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class CursorHelpers {
|
|
58
|
+
+createCursor(id, createdAt?, offset?) CursorData
|
|
59
|
+
+parseCursor(data) CursorData
|
|
60
|
+
+encodeCursor(cursor) string
|
|
61
|
+
+decodeCursor(encoded) string
|
|
62
|
+
+encodeCursorFromItem(id, createdAt?, offset?) string
|
|
63
|
+
+decodeAndParseCursor(encoded) CursorData
|
|
64
|
+
+buildCursorMeta(items, limit, hasMore) object
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
class DateHelpers {
|
|
68
|
+
+nowMs() number
|
|
69
|
+
+toMs(input) number | null
|
|
70
|
+
+fromMs(ms) Date | null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class AccessControl {
|
|
74
|
+
+hasRole(profile, role) boolean
|
|
75
|
+
+getRoles(profile) string[]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
AppError <|-- NotFoundError
|
|
79
|
+
AppError <|-- ValidationError
|
|
80
|
+
AppError <|-- ConflictError
|
|
81
|
+
AppError <|-- InternalError
|
|
82
|
+
AppError --> ErrorCode
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## How It Works
|
|
86
|
+
|
|
87
|
+
### Error types
|
|
88
|
+
|
|
89
|
+
Typed error hierarchy with error codes — consumers catch by type, not by string:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
throw new NotFoundError(`User ${userId} not found`);
|
|
93
|
+
throw new ValidationError('Email is required');
|
|
94
|
+
throw new InternalError('DB connection lost', originalError);
|
|
95
|
+
|
|
96
|
+
// Callers can discriminate:
|
|
97
|
+
if (error instanceof NotFoundError) {
|
|
98
|
+
return new Response(null, { status: 404 });
|
|
99
|
+
}
|
|
100
|
+
if (isAppError(error)) {
|
|
101
|
+
// error.code is typed: 'NOT_FOUND' | 'VALIDATION' | 'CONFLICT' | 'INTERNAL'
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Cursor-based pagination
|
|
106
|
+
|
|
107
|
+
Encode a cursor from the last item in a page, return it to the client, decode on the next request:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { encodeCursorFromItem, decodeAndParseCursor } from '@gobing-ai/ts-utils';
|
|
111
|
+
|
|
112
|
+
// Page 1: encode cursor from last item
|
|
113
|
+
const cursor = encodeCursorFromItem(lastItem.id, lastItem.createdAt);
|
|
114
|
+
// → "eyJpZCI6ImFiYyIsImNyZWF0ZWRBdCI6MTcwMDAwMDAwMH0"
|
|
115
|
+
|
|
116
|
+
// Page 2: decode cursor from query param
|
|
117
|
+
const parsed = decodeAndParseCursor(requestCursor);
|
|
118
|
+
// → { id: "abc", createdAt: 1700000000 }
|
|
119
|
+
|
|
120
|
+
// Build pagination metadata
|
|
121
|
+
const meta = buildCursorMeta(items, limit, hasMore);
|
|
122
|
+
// → { nextCursor: "eyJ...", hasMore: true, limit: 20 }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Cursor flow:**
|
|
126
|
+
|
|
127
|
+
```mermaid
|
|
128
|
+
sequenceDiagram
|
|
129
|
+
Client->>API: GET /items?limit=20
|
|
130
|
+
API->>DB: SELECT ... LIMIT 20
|
|
131
|
+
DB-->>API: 20 rows
|
|
132
|
+
API->>API: encodeCursorFromItem(lastItem)
|
|
133
|
+
API-->>Client: { items: [...], nextCursor: "eyJ..." }
|
|
134
|
+
|
|
135
|
+
Client->>API: GET /items?limit=20&cursor=eyJ...
|
|
136
|
+
API->>API: decodeAndParseCursor("eyJ...")
|
|
137
|
+
API->>DB: SELECT ... WHERE id > "abc" LIMIT 20
|
|
138
|
+
DB-->>API: 20 rows
|
|
139
|
+
API-->>Client: { items: [...], nextCursor: "..." }
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Role-based access control
|
|
143
|
+
|
|
144
|
+
Supports Zitadel IAM roles and generic role arrays/objects:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { hasRole, getRoles } from '@gobing-ai/ts-utils';
|
|
148
|
+
|
|
149
|
+
// Zitadel profile
|
|
150
|
+
const profile = {
|
|
151
|
+
'urn:zitadel:iam:org:project:roles': { admin: 'project-1', viewer: 'project-1' },
|
|
152
|
+
};
|
|
153
|
+
hasRole(profile, 'admin'); // → true
|
|
154
|
+
getRoles(profile); // → ['admin', 'viewer']
|
|
155
|
+
|
|
156
|
+
// Generic roles array
|
|
157
|
+
hasRole({ roles: ['editor', 'viewer'] }, 'editor'); // → true
|
|
158
|
+
|
|
159
|
+
// Object-based roles
|
|
160
|
+
hasRole({ roles: { admin: true } }, 'admin'); // → true
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Date utilities
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { nowMs, toMs, fromMs } from '@gobing-ai/ts-utils';
|
|
167
|
+
|
|
168
|
+
const ts = nowMs(); // → 1700000000000
|
|
169
|
+
|
|
170
|
+
toMs(new Date('2024-01-01')); // → 1704067200000
|
|
171
|
+
toMs('2024-01-01'); // → 1704067200000
|
|
172
|
+
toMs(null); // → null
|
|
173
|
+
|
|
174
|
+
fromMs(1704067200000); // → Date('2024-01-01')
|
|
175
|
+
fromMs(null); // → null
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Usage
|
|
179
|
+
|
|
180
|
+
### Install
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
bun add @gobing-ai/ts-utils
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Common patterns
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
import {
|
|
190
|
+
AppError, NotFoundError, ValidationError, ConflictError, InternalError,
|
|
191
|
+
nowMs, toMs, fromMs,
|
|
192
|
+
encodeCursorFromItem, decodeAndParseCursor, buildCursorMeta,
|
|
193
|
+
hasRole, getRoles,
|
|
194
|
+
} from '@gobing-ai/ts-utils';
|
|
195
|
+
|
|
196
|
+
// Errors
|
|
197
|
+
function getUser(id: string) {
|
|
198
|
+
const user = db.find(id);
|
|
199
|
+
if (!user) throw new NotFoundError(`User ${id} not found`);
|
|
200
|
+
return user;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Pagination
|
|
204
|
+
async function listUsers(limit: number, cursor?: string) {
|
|
205
|
+
const parsed = cursor ? decodeAndParseCursor(cursor) : undefined;
|
|
206
|
+
const users = await db.query(limit, parsed?.id);
|
|
207
|
+
const hasMore = users.length === limit;
|
|
208
|
+
return {
|
|
209
|
+
items: users,
|
|
210
|
+
...buildCursorMeta(users, limit, hasMore),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Auth guard
|
|
215
|
+
function requireAdmin(profile: unknown) {
|
|
216
|
+
if (!hasRole(profile as Record<string, unknown>, 'admin')) {
|
|
217
|
+
throw new ForbiddenError('Admin access required');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|
package/dist/access.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zitadel + generic role-based access control helpers.
|
|
3
|
+
*
|
|
4
|
+
* Supports Zitadel IAM role claims (`urn:zitadel:iam:org:project:roles`),
|
|
5
|
+
* generic `roles` arrays, and object-based role maps.
|
|
6
|
+
*
|
|
7
|
+
* If auth provider uses a different claim format, extend `hasRole`
|
|
8
|
+
* rather than forking this module.
|
|
9
|
+
*/
|
|
10
|
+
export declare function hasRole(profile: Record<string, unknown> | null | undefined, role: string): boolean;
|
|
11
|
+
export declare function getRoles(profile: Record<string, unknown> | null | undefined): string[];
|
|
12
|
+
//# sourceMappingURL=access.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../src/access.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CA+BlG;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,EAAE,CAkCtF"}
|
package/dist/access.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zitadel + generic role-based access control helpers.
|
|
3
|
+
*
|
|
4
|
+
* Supports Zitadel IAM role claims (`urn:zitadel:iam:org:project:roles`),
|
|
5
|
+
* generic `roles` arrays, and object-based role maps.
|
|
6
|
+
*
|
|
7
|
+
* If auth provider uses a different claim format, extend `hasRole`
|
|
8
|
+
* rather than forking this module.
|
|
9
|
+
*/
|
|
10
|
+
export function hasRole(profile, role) {
|
|
11
|
+
if (!profile)
|
|
12
|
+
return false;
|
|
13
|
+
if (!role || typeof role !== 'string')
|
|
14
|
+
return false;
|
|
15
|
+
const zitadelRoles = profile['urn:zitadel:iam:org:project:roles'];
|
|
16
|
+
if (zitadelRoles && typeof zitadelRoles === 'object') {
|
|
17
|
+
try {
|
|
18
|
+
if (zitadelRoles !== null && !Array.isArray(zitadelRoles) && Object.hasOwn(zitadelRoles, role)) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Continue to other role formats when host objects reject inspection.
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const rolesArray = profile.roles;
|
|
27
|
+
if (Array.isArray(rolesArray)) {
|
|
28
|
+
return rolesArray.includes(role);
|
|
29
|
+
}
|
|
30
|
+
if (rolesArray && typeof rolesArray === 'object' && !Array.isArray(rolesArray)) {
|
|
31
|
+
try {
|
|
32
|
+
if (rolesArray !== null) {
|
|
33
|
+
return Object.hasOwn(rolesArray, role);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Fall through.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
export function getRoles(profile) {
|
|
43
|
+
if (!profile)
|
|
44
|
+
return [];
|
|
45
|
+
const roles = new Set();
|
|
46
|
+
const zitadelRoles = profile['urn:zitadel:iam:org:project:roles'];
|
|
47
|
+
if (zitadelRoles && typeof zitadelRoles === 'object' && zitadelRoles !== null && !Array.isArray(zitadelRoles)) {
|
|
48
|
+
try {
|
|
49
|
+
for (const key of Object.keys(zitadelRoles)) {
|
|
50
|
+
roles.add(key);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Ignore host objects that reject key enumeration.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const rolesArray = profile.roles;
|
|
58
|
+
if (Array.isArray(rolesArray)) {
|
|
59
|
+
rolesArray.forEach((role) => {
|
|
60
|
+
if (typeof role === 'string')
|
|
61
|
+
roles.add(role);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (rolesArray && typeof rolesArray === 'object' && rolesArray !== null && !Array.isArray(rolesArray)) {
|
|
65
|
+
try {
|
|
66
|
+
for (const key of Object.keys(rolesArray)) {
|
|
67
|
+
roles.add(key);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Ignore host objects that reject key enumeration.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return Array.from(roles);
|
|
75
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export declare const API_ERROR_CODES: {
|
|
2
|
+
readonly SUCCESS: 0;
|
|
3
|
+
readonly NOT_FOUND: 404;
|
|
4
|
+
readonly VALIDATION_ERROR: 422;
|
|
5
|
+
readonly BAD_REQUEST: 400;
|
|
6
|
+
readonly UNAUTHORIZED: 401;
|
|
7
|
+
readonly FORBIDDEN: 403;
|
|
8
|
+
readonly CONFLICT: 409;
|
|
9
|
+
readonly INTERNAL_ERROR: 500;
|
|
10
|
+
};
|
|
11
|
+
export type ApiErrorCode = (typeof API_ERROR_CODES)[keyof typeof API_ERROR_CODES];
|
|
12
|
+
export type ApiEnvelopeResult = 'success' | 'info' | 'warn' | 'error';
|
|
13
|
+
export interface ApiSuccessEnvelope<T> {
|
|
14
|
+
code: 0;
|
|
15
|
+
message: string;
|
|
16
|
+
result: 'success' | 'info';
|
|
17
|
+
data: T;
|
|
18
|
+
meta?: {
|
|
19
|
+
total?: number;
|
|
20
|
+
limit?: number;
|
|
21
|
+
offset?: number;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export interface ApiErrorEnvelope {
|
|
25
|
+
result: 'warn' | 'error';
|
|
26
|
+
code: number;
|
|
27
|
+
message: string;
|
|
28
|
+
data: null;
|
|
29
|
+
details?: unknown;
|
|
30
|
+
}
|
|
31
|
+
export type ApiEnvelope<T> = ApiSuccessEnvelope<T> | ApiErrorEnvelope;
|
|
32
|
+
export declare function successResponse<T>(data: T, message?: string): ApiSuccessEnvelope<T>;
|
|
33
|
+
export declare function infoResponse<T>(data: T, message?: string): ApiSuccessEnvelope<T>;
|
|
34
|
+
export declare function paginatedResponse<T>(data: T[], meta: {
|
|
35
|
+
total?: number;
|
|
36
|
+
limit?: number;
|
|
37
|
+
offset?: number;
|
|
38
|
+
}, message?: string): ApiSuccessEnvelope<T[]>;
|
|
39
|
+
export declare function errorResponse(code: number, message: string, details?: unknown): ApiErrorEnvelope;
|
|
40
|
+
export declare function notFoundResponse(message?: string, details?: unknown): ApiErrorEnvelope;
|
|
41
|
+
export declare function validationErrorResponse(details: unknown, message?: string): ApiErrorEnvelope;
|
|
42
|
+
export declare function badRequestResponse(message: string, details?: unknown): ApiErrorEnvelope;
|
|
43
|
+
export declare function unauthorizedResponse(message?: string, details?: unknown): ApiErrorEnvelope;
|
|
44
|
+
export declare function forbiddenResponse(message?: string, details?: unknown): ApiErrorEnvelope;
|
|
45
|
+
export declare function conflictResponse(message?: string, details?: unknown): ApiErrorEnvelope;
|
|
46
|
+
export declare function internalErrorResponse(message?: string, details?: unknown): ApiErrorEnvelope;
|
|
47
|
+
//# sourceMappingURL=api-response.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-response.d.ts","sourceRoot":"","sources":["../src/api-response.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe;;;;;;;;;CASlB,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,OAAO,eAAe,CAAC,CAAC;AAElF,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEtE,MAAM,WAAW,kBAAkB,CAAC,CAAC;IACjC,IAAI,EAAE,CAAC,CAAC;IACR,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,GAAG,MAAM,CAAC;IAC3B,IAAI,EAAE,CAAC,CAAC;IACR,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9D;AAED,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,IAAI,CAAC;IACX,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI,kBAAkB,CAAC,CAAC,CAAC,GAAG,gBAAgB,CAAC;AAEtE,wBAAgB,eAAe,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,SAAY,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAOtF;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,SAAgC,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAOvG;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EAC/B,IAAI,EAAE,CAAC,EAAE,EACT,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,EACzD,OAAO,SAAgC,GACxC,kBAAkB,CAAC,CAAC,EAAE,CAAC,CAQzB;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAahG;AAED,wBAAgB,gBAAgB,CAAC,OAAO,SAAuB,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAEpG;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,SAAsB,GAAG,gBAAgB,CAEzG;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAEvF;AAED,wBAAgB,oBAAoB,CAAC,OAAO,SAA4B,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAE7G;AAED,wBAAgB,iBAAiB,CAAC,OAAO,SAAqB,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAEnG;AAED,wBAAgB,gBAAgB,CAAC,OAAO,SAAsB,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAEnG;AAED,wBAAgB,qBAAqB,CAAC,OAAO,SAA0B,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAE5G"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export const API_ERROR_CODES = {
|
|
2
|
+
SUCCESS: 0,
|
|
3
|
+
NOT_FOUND: 404,
|
|
4
|
+
VALIDATION_ERROR: 422,
|
|
5
|
+
BAD_REQUEST: 400,
|
|
6
|
+
UNAUTHORIZED: 401,
|
|
7
|
+
FORBIDDEN: 403,
|
|
8
|
+
CONFLICT: 409,
|
|
9
|
+
INTERNAL_ERROR: 500,
|
|
10
|
+
};
|
|
11
|
+
export function successResponse(data, message = 'Success') {
|
|
12
|
+
return {
|
|
13
|
+
code: API_ERROR_CODES.SUCCESS,
|
|
14
|
+
message,
|
|
15
|
+
result: 'success',
|
|
16
|
+
data,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function infoResponse(data, message = 'Data retrieved successfully') {
|
|
20
|
+
return {
|
|
21
|
+
code: API_ERROR_CODES.SUCCESS,
|
|
22
|
+
message,
|
|
23
|
+
result: 'info',
|
|
24
|
+
data,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function paginatedResponse(data, meta, message = 'Data retrieved successfully') {
|
|
28
|
+
return {
|
|
29
|
+
code: API_ERROR_CODES.SUCCESS,
|
|
30
|
+
message,
|
|
31
|
+
result: 'info',
|
|
32
|
+
data,
|
|
33
|
+
meta,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function errorResponse(code, message, details) {
|
|
37
|
+
const response = {
|
|
38
|
+
code,
|
|
39
|
+
message,
|
|
40
|
+
result: code >= 500 ? 'error' : 'warn',
|
|
41
|
+
data: null,
|
|
42
|
+
};
|
|
43
|
+
if (details !== undefined) {
|
|
44
|
+
response.details = details;
|
|
45
|
+
}
|
|
46
|
+
return response;
|
|
47
|
+
}
|
|
48
|
+
export function notFoundResponse(message = 'Resource not found', details) {
|
|
49
|
+
return errorResponse(API_ERROR_CODES.NOT_FOUND, message, details);
|
|
50
|
+
}
|
|
51
|
+
export function validationErrorResponse(details, message = 'Validation failed') {
|
|
52
|
+
return errorResponse(API_ERROR_CODES.VALIDATION_ERROR, message, details);
|
|
53
|
+
}
|
|
54
|
+
export function badRequestResponse(message, details) {
|
|
55
|
+
return errorResponse(API_ERROR_CODES.BAD_REQUEST, message, details);
|
|
56
|
+
}
|
|
57
|
+
export function unauthorizedResponse(message = 'Authentication required', details) {
|
|
58
|
+
return errorResponse(API_ERROR_CODES.UNAUTHORIZED, message, details);
|
|
59
|
+
}
|
|
60
|
+
export function forbiddenResponse(message = 'Access forbidden', details) {
|
|
61
|
+
return errorResponse(API_ERROR_CODES.FORBIDDEN, message, details);
|
|
62
|
+
}
|
|
63
|
+
export function conflictResponse(message = 'Resource conflict', details) {
|
|
64
|
+
return errorResponse(API_ERROR_CODES.CONFLICT, message, details);
|
|
65
|
+
}
|
|
66
|
+
export function internalErrorResponse(message = 'Internal server error', details) {
|
|
67
|
+
return errorResponse(API_ERROR_CODES.INTERNAL_ERROR, message, details);
|
|
68
|
+
}
|
package/dist/const.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"const.d.ts","sourceRoot":"","sources":["../src/const.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,QAAQ,CAAC;AAEtC,eAAO,MAAM,gBAAgB,QAAQ,CAAC"}
|
package/dist/const.js
ADDED
package/dist/cursor.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface CursorData {
|
|
2
|
+
id: string;
|
|
3
|
+
createdAt?: number;
|
|
4
|
+
offset?: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function createCursor(id: string, createdAt?: Date | number, offset?: number): CursorData;
|
|
7
|
+
export declare function parseCursor(data: string | Record<string, unknown>): CursorData;
|
|
8
|
+
export declare function encodeCursor(cursor: CursorData): string;
|
|
9
|
+
export declare function decodeCursor(encoded: string): string;
|
|
10
|
+
export declare function encodeCursorFromItem(id: string, createdAt?: Date | number, offset?: number): string;
|
|
11
|
+
export declare function decodeAndParseCursor(encoded: string): CursorData;
|
|
12
|
+
export declare function buildCursorMeta<T extends {
|
|
13
|
+
id: string;
|
|
14
|
+
createdAt?: number | Date;
|
|
15
|
+
}>(items: T[], limit: number, hasMore: boolean): {
|
|
16
|
+
nextCursor?: string;
|
|
17
|
+
hasMore: boolean;
|
|
18
|
+
limit: number;
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=cursor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor.d.ts","sourceRoot":"","sources":["../src/cursor.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,IAAI,GAAG,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,UAAU,CAY/F;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,UAAU,CAmB9E;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAEvD;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAMpD;AAED,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,IAAI,GAAG,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAEnG;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAEhE;AAED,wBAAgB,eAAe,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EAC/E,KAAK,EAAE,CAAC,EAAE,EACV,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,OAAO,GACjB;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAc1D"}
|
package/dist/cursor.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { toMs } from './date.js';
|
|
2
|
+
export function createCursor(id, createdAt, offset) {
|
|
3
|
+
const cursor = { id };
|
|
4
|
+
if (createdAt !== undefined) {
|
|
5
|
+
const ms = toMs(createdAt);
|
|
6
|
+
if (ms !== null) {
|
|
7
|
+
cursor.createdAt = ms;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
if (offset !== undefined) {
|
|
11
|
+
cursor.offset = offset;
|
|
12
|
+
}
|
|
13
|
+
return cursor;
|
|
14
|
+
}
|
|
15
|
+
export function parseCursor(data) {
|
|
16
|
+
const parsed = typeof data === 'string' ? JSON.parse(data) : data;
|
|
17
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
18
|
+
throw new Error('Invalid cursor: must be an object');
|
|
19
|
+
}
|
|
20
|
+
if (!parsed.id || typeof parsed.id !== 'string') {
|
|
21
|
+
throw new Error('Invalid cursor: missing or invalid id');
|
|
22
|
+
}
|
|
23
|
+
const result = { id: parsed.id };
|
|
24
|
+
if (typeof parsed.createdAt === 'number') {
|
|
25
|
+
result.createdAt = parsed.createdAt;
|
|
26
|
+
}
|
|
27
|
+
if (typeof parsed.offset === 'number') {
|
|
28
|
+
result.offset = parsed.offset;
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
export function encodeCursor(cursor) {
|
|
33
|
+
return Buffer.from(JSON.stringify(cursor)).toString('base64url');
|
|
34
|
+
}
|
|
35
|
+
export function decodeCursor(encoded) {
|
|
36
|
+
try {
|
|
37
|
+
return Buffer.from(encoded, 'base64url').toString('utf-8');
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new Error(`Invalid cursor encoding: ${String(error)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function encodeCursorFromItem(id, createdAt, offset) {
|
|
44
|
+
return encodeCursor(createCursor(id, createdAt, offset));
|
|
45
|
+
}
|
|
46
|
+
export function decodeAndParseCursor(encoded) {
|
|
47
|
+
return parseCursor(decodeCursor(encoded));
|
|
48
|
+
}
|
|
49
|
+
export function buildCursorMeta(items, limit, hasMore) {
|
|
50
|
+
const meta = {
|
|
51
|
+
hasMore,
|
|
52
|
+
limit,
|
|
53
|
+
};
|
|
54
|
+
if (hasMore) {
|
|
55
|
+
const lastItem = items.at(-1);
|
|
56
|
+
if (lastItem) {
|
|
57
|
+
meta.nextCursor = encodeCursorFromItem(lastItem.id, lastItem.createdAt);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return meta;
|
|
61
|
+
}
|
package/dist/date.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"date.d.ts","sourceRoot":"","sources":["../src/date.ts"],"names":[],"mappings":"AAAA,wBAAgB,KAAK,IAAI,MAAM,CAE9B;AAED,wBAAgB,IAAI,CAAC,KAAK,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAQpF;AAED,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,GAAG,IAAI,CAGjE"}
|
package/dist/date.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function nowMs() {
|
|
2
|
+
return Date.now();
|
|
3
|
+
}
|
|
4
|
+
export function toMs(input) {
|
|
5
|
+
if (input === null || input === undefined)
|
|
6
|
+
return null;
|
|
7
|
+
if (input instanceof Date)
|
|
8
|
+
return input.getTime();
|
|
9
|
+
if (typeof input === 'string') {
|
|
10
|
+
const parsed = new Date(input).getTime();
|
|
11
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
12
|
+
}
|
|
13
|
+
return Math.floor(input);
|
|
14
|
+
}
|
|
15
|
+
export function fromMs(ms) {
|
|
16
|
+
if (ms === null || ms === undefined || Number.isNaN(ms))
|
|
17
|
+
return null;
|
|
18
|
+
return new Date(ms);
|
|
19
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare const ErrorCode: {
|
|
2
|
+
readonly NotFound: "NOT_FOUND";
|
|
3
|
+
readonly Validation: "VALIDATION";
|
|
4
|
+
readonly Conflict: "CONFLICT";
|
|
5
|
+
readonly Internal: "INTERNAL";
|
|
6
|
+
};
|
|
7
|
+
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
8
|
+
export declare class AppError extends Error {
|
|
9
|
+
readonly code: ErrorCode;
|
|
10
|
+
constructor(code: ErrorCode, message: string);
|
|
11
|
+
}
|
|
12
|
+
export declare class NotFoundError extends AppError {
|
|
13
|
+
constructor(message: string);
|
|
14
|
+
}
|
|
15
|
+
export declare class ValidationError extends AppError {
|
|
16
|
+
constructor(message: string);
|
|
17
|
+
}
|
|
18
|
+
export declare class ConflictError extends AppError {
|
|
19
|
+
constructor(message: string);
|
|
20
|
+
}
|
|
21
|
+
export declare class InternalError extends AppError {
|
|
22
|
+
readonly cause?: unknown | undefined;
|
|
23
|
+
constructor(message: string, cause?: unknown | undefined);
|
|
24
|
+
}
|
|
25
|
+
export declare function isAppError(error: unknown): error is AppError;
|
|
26
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,SAAS;;;;;CAKZ,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,OAAO,SAAS,CAAC,CAAC;AAEnE,qBAAa,QAAS,SAAQ,KAAK;IAC/B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;gBAEb,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM;CAK/C;AAED,qBAAa,aAAc,SAAQ,QAAQ;gBAC3B,OAAO,EAAE,MAAM;CAI9B;AAED,qBAAa,eAAgB,SAAQ,QAAQ;gBAC7B,OAAO,EAAE,MAAM;CAI9B;AAED,qBAAa,aAAc,SAAQ,QAAQ;gBAC3B,OAAO,EAAE,MAAM;CAI9B;AAED,qBAAa,aAAc,SAAQ,QAAQ;aAGjB,KAAK,CAAC,EAAE,OAAO;gBADjC,OAAO,EAAE,MAAM,EACG,KAAK,CAAC,EAAE,OAAO,YAAA;CAKxC;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,QAAQ,CAE5D"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const ErrorCode = {
|
|
2
|
+
NotFound: 'NOT_FOUND',
|
|
3
|
+
Validation: 'VALIDATION',
|
|
4
|
+
Conflict: 'CONFLICT',
|
|
5
|
+
Internal: 'INTERNAL',
|
|
6
|
+
};
|
|
7
|
+
export class AppError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(code, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'AppError';
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class NotFoundError extends AppError {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(ErrorCode.NotFound, message);
|
|
18
|
+
this.name = 'NotFoundError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class ValidationError extends AppError {
|
|
22
|
+
constructor(message) {
|
|
23
|
+
super(ErrorCode.Validation, message);
|
|
24
|
+
this.name = 'ValidationError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class ConflictError extends AppError {
|
|
28
|
+
constructor(message) {
|
|
29
|
+
super(ErrorCode.Conflict, message);
|
|
30
|
+
this.name = 'ConflictError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class InternalError extends AppError {
|
|
34
|
+
cause;
|
|
35
|
+
constructor(message, cause) {
|
|
36
|
+
super(ErrorCode.Internal, message);
|
|
37
|
+
this.cause = cause;
|
|
38
|
+
this.name = 'InternalError';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function isAppError(error) {
|
|
42
|
+
return error instanceof AppError;
|
|
43
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC"}
|
package/dist/index.js
ADDED