@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 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
+ ```
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export declare const LOG_CATEGORY_APP = "app";
2
+ export declare const LOG_CATEGORY_CLI = "cli";
3
+ //# sourceMappingURL=const.d.ts.map
@@ -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
@@ -0,0 +1,2 @@
1
+ export const LOG_CATEGORY_APP = 'app';
2
+ export const LOG_CATEGORY_CLI = 'cli';
@@ -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,4 @@
1
+ export declare function nowMs(): number;
2
+ export declare function toMs(input: Date | number | string | null | undefined): number | null;
3
+ export declare function fromMs(ms: number | null | undefined): Date | null;
4
+ //# sourceMappingURL=date.d.ts.map
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ export * from './access';
2
+ export * from './api-response';
3
+ export * from './const';
4
+ export * from './cursor';
5
+ export * from './date';
6
+ export * from './errors';
7
+ export * from './origin';
8
+ export * from './output';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -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
@@ -0,0 +1,8 @@
1
+ export * from './access.js';
2
+ export * from './api-response.js';
3
+ export * from './const.js';
4
+ export * from './cursor.js';
5
+ export * from './date.js';
6
+ export * from './errors.js';
7
+ export * from './origin.js';
8
+ export * from './output.js';