@carlos-tzin/tzin 0.1.6 → 0.1.8
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/dist/auth.d.ts +76 -0
- package/dist/auth.js +177 -0
- package/dist/db.d.ts +96 -0
- package/dist/db.js +212 -0
- package/package.json +9 -1
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Middleware } from './middleware.js';
|
|
2
|
+
export interface AuthUser {
|
|
3
|
+
id: string;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
export interface AuthConfig {
|
|
7
|
+
/** Secret key for JWT signing/verification */
|
|
8
|
+
secret: string;
|
|
9
|
+
/** Token issuer (optional) */
|
|
10
|
+
issuer?: string;
|
|
11
|
+
/** Token audience (optional) */
|
|
12
|
+
audience?: string;
|
|
13
|
+
/** Token expiration (default: '1h') */
|
|
14
|
+
expiresIn?: string;
|
|
15
|
+
/** Custom token extractor (default: Bearer header) */
|
|
16
|
+
extractToken?: (req: Request) => string | null;
|
|
17
|
+
/** Called when auth fails */
|
|
18
|
+
onUnauthorized?: (req: Request, reason: string) => Response | Promise<Response>;
|
|
19
|
+
}
|
|
20
|
+
export interface JwtPayload {
|
|
21
|
+
sub: string;
|
|
22
|
+
iat: number;
|
|
23
|
+
exp: number;
|
|
24
|
+
iss?: string;
|
|
25
|
+
aud?: string;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
/** Typed key for the authenticated user */
|
|
29
|
+
export declare const AUTH_USER: import("./context.js").ContextKey<AuthUser>;
|
|
30
|
+
/**
|
|
31
|
+
* Sign a JWT token.
|
|
32
|
+
*/
|
|
33
|
+
export declare function signJwt(payload: Record<string, unknown>, secret: string, options?: {
|
|
34
|
+
expiresIn?: string;
|
|
35
|
+
issuer?: string;
|
|
36
|
+
audience?: string;
|
|
37
|
+
}): string;
|
|
38
|
+
/**
|
|
39
|
+
* Verify a JWT token.
|
|
40
|
+
*/
|
|
41
|
+
export declare function verifyJwt(token: string, secret: string, options?: {
|
|
42
|
+
issuer?: string;
|
|
43
|
+
audience?: string;
|
|
44
|
+
}): JwtPayload;
|
|
45
|
+
/**
|
|
46
|
+
* Bearer token authentication middleware.
|
|
47
|
+
*
|
|
48
|
+
* Validates the Authorization: Bearer <token> header and attaches
|
|
49
|
+
* the decoded user to ctx.set(AUTH_USER, user).
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* import { bearerAuth, AUTH_USER } from '@carlos-tzin/tzin/auth'
|
|
54
|
+
*
|
|
55
|
+
* const app = createApp(routes, {
|
|
56
|
+
* middleware: [bearerAuth({ secret: process.env.JWT_SECRET! })],
|
|
57
|
+
* })
|
|
58
|
+
*
|
|
59
|
+
* // In handler:
|
|
60
|
+
* const user = ctx.require(AUTH_USER)
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function bearerAuth(config: AuthConfig): Middleware;
|
|
64
|
+
/**
|
|
65
|
+
* Optional auth middleware - doesn't fail if no token,
|
|
66
|
+
* but validates if present.
|
|
67
|
+
*/
|
|
68
|
+
export declare function optionalAuth(config: AuthConfig): Middleware;
|
|
69
|
+
/**
|
|
70
|
+
* API key authentication middleware.
|
|
71
|
+
* Checks for a header like X-API-Key.
|
|
72
|
+
*/
|
|
73
|
+
export declare function apiKeyAuth(config: {
|
|
74
|
+
key: string;
|
|
75
|
+
header?: string;
|
|
76
|
+
}): Middleware;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { defineContext } from './context.js';
|
|
2
|
+
// ── Context Key ──────────────────────────────────────────────────────
|
|
3
|
+
/** Typed key for the authenticated user */
|
|
4
|
+
export const AUTH_USER = defineContext('auth_user');
|
|
5
|
+
// ── JWT Utilities (minimal, no dependencies) ────────────────────────
|
|
6
|
+
function base64UrlEncode(data) {
|
|
7
|
+
return Buffer.from(data).toString('base64url');
|
|
8
|
+
}
|
|
9
|
+
function base64UrlDecode(data) {
|
|
10
|
+
return Buffer.from(data, 'base64url').toString();
|
|
11
|
+
}
|
|
12
|
+
function hmacSign(data, secret) {
|
|
13
|
+
const { createHmac } = require('node:crypto');
|
|
14
|
+
return createHmac('sha256', secret).update(data).digest('base64url');
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Sign a JWT token.
|
|
18
|
+
*/
|
|
19
|
+
export function signJwt(payload, secret, options) {
|
|
20
|
+
const header = { alg: 'HS256', typ: 'JWT' };
|
|
21
|
+
const now = Math.floor(Date.now() / 1000);
|
|
22
|
+
const exp = options?.expiresIn ? parseDuration(options.expiresIn) : 3600;
|
|
23
|
+
const fullPayload = {
|
|
24
|
+
...payload,
|
|
25
|
+
iat: now,
|
|
26
|
+
exp: now + exp,
|
|
27
|
+
...(options?.issuer && { iss: options.issuer }),
|
|
28
|
+
...(options?.audience && { aud: options.audience }),
|
|
29
|
+
};
|
|
30
|
+
const headerEncoded = base64UrlEncode(JSON.stringify(header));
|
|
31
|
+
const payloadEncoded = base64UrlEncode(JSON.stringify(fullPayload));
|
|
32
|
+
const signature = hmacSign(`${headerEncoded}.${payloadEncoded}`, secret);
|
|
33
|
+
return `${headerEncoded}.${payloadEncoded}.${signature}`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Verify a JWT token.
|
|
37
|
+
*/
|
|
38
|
+
export function verifyJwt(token, secret, options) {
|
|
39
|
+
const parts = token.split('.');
|
|
40
|
+
if (parts.length !== 3)
|
|
41
|
+
throw new Error('Invalid JWT format');
|
|
42
|
+
const [headerEncoded, payloadEncoded, signature] = parts;
|
|
43
|
+
const expectedSig = hmacSign(`${headerEncoded}.${payloadEncoded}`, secret);
|
|
44
|
+
if (signature !== expectedSig)
|
|
45
|
+
throw new Error('Invalid JWT signature');
|
|
46
|
+
const payload = JSON.parse(base64UrlDecode(payloadEncoded));
|
|
47
|
+
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
|
|
48
|
+
throw new Error('Token expired');
|
|
49
|
+
}
|
|
50
|
+
if (options?.issuer && payload.iss !== options.issuer) {
|
|
51
|
+
throw new Error('Invalid issuer');
|
|
52
|
+
}
|
|
53
|
+
if (options?.audience && payload.aud !== options.audience) {
|
|
54
|
+
throw new Error('Invalid audience');
|
|
55
|
+
}
|
|
56
|
+
return payload;
|
|
57
|
+
}
|
|
58
|
+
function parseDuration(str) {
|
|
59
|
+
const match = str.match(/^(\d+)([smhd])$/);
|
|
60
|
+
if (!match)
|
|
61
|
+
return 3600;
|
|
62
|
+
const [, num, unit] = match;
|
|
63
|
+
const n = parseInt(num, 10);
|
|
64
|
+
switch (unit) {
|
|
65
|
+
case 's': return n;
|
|
66
|
+
case 'm': return n * 60;
|
|
67
|
+
case 'h': return n * 3600;
|
|
68
|
+
case 'd': return n * 86400;
|
|
69
|
+
default: return 3600;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// ── Middleware ────────────────────────────────────────────────────────
|
|
73
|
+
/**
|
|
74
|
+
* Bearer token authentication middleware.
|
|
75
|
+
*
|
|
76
|
+
* Validates the Authorization: Bearer <token> header and attaches
|
|
77
|
+
* the decoded user to ctx.set(AUTH_USER, user).
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* import { bearerAuth, AUTH_USER } from '@carlos-tzin/tzin/auth'
|
|
82
|
+
*
|
|
83
|
+
* const app = createApp(routes, {
|
|
84
|
+
* middleware: [bearerAuth({ secret: process.env.JWT_SECRET! })],
|
|
85
|
+
* })
|
|
86
|
+
*
|
|
87
|
+
* // In handler:
|
|
88
|
+
* const user = ctx.require(AUTH_USER)
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export function bearerAuth(config) {
|
|
92
|
+
const extract = config.extractToken ?? defaultExtractToken;
|
|
93
|
+
const onUnauthorized = config.onUnauthorized ?? defaultUnauthorized;
|
|
94
|
+
return async ({ req, ctx, next }) => {
|
|
95
|
+
const token = extract(req);
|
|
96
|
+
if (!token) {
|
|
97
|
+
return onUnauthorized(req, 'Missing token');
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const payload = verifyJwt(token, config.secret, {
|
|
101
|
+
issuer: config.issuer,
|
|
102
|
+
audience: config.audience,
|
|
103
|
+
});
|
|
104
|
+
const user = {
|
|
105
|
+
id: payload.sub,
|
|
106
|
+
...payload,
|
|
107
|
+
};
|
|
108
|
+
ctx.set(AUTH_USER, user);
|
|
109
|
+
return next();
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
const reason = err instanceof Error ? err.message : 'Invalid token';
|
|
113
|
+
return onUnauthorized(req, reason);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Optional auth middleware - doesn't fail if no token,
|
|
119
|
+
* but validates if present.
|
|
120
|
+
*/
|
|
121
|
+
export function optionalAuth(config) {
|
|
122
|
+
const extract = config.extractToken ?? defaultExtractToken;
|
|
123
|
+
return async ({ req, ctx, next }) => {
|
|
124
|
+
const token = extract(req);
|
|
125
|
+
if (!token) {
|
|
126
|
+
return next();
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const payload = verifyJwt(token, config.secret, {
|
|
130
|
+
issuer: config.issuer,
|
|
131
|
+
audience: config.audience,
|
|
132
|
+
});
|
|
133
|
+
const user = {
|
|
134
|
+
id: payload.sub,
|
|
135
|
+
...payload,
|
|
136
|
+
};
|
|
137
|
+
ctx.set(AUTH_USER, user);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Ignore invalid tokens in optional mode
|
|
141
|
+
}
|
|
142
|
+
return next();
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* API key authentication middleware.
|
|
147
|
+
* Checks for a header like X-API-Key.
|
|
148
|
+
*/
|
|
149
|
+
export function apiKeyAuth(config) {
|
|
150
|
+
const headerName = config.header ?? 'x-api-key';
|
|
151
|
+
return async ({ req, next }) => {
|
|
152
|
+
const apiKey = req.headers.get(headerName);
|
|
153
|
+
if (!apiKey || apiKey !== config.key) {
|
|
154
|
+
return new Response(JSON.stringify({ error: 'Invalid API key' }), {
|
|
155
|
+
status: 401,
|
|
156
|
+
headers: { 'content-type': 'application/json' },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return next();
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
163
|
+
function defaultExtractToken(req) {
|
|
164
|
+
const auth = req.headers.get('authorization');
|
|
165
|
+
if (!auth)
|
|
166
|
+
return null;
|
|
167
|
+
const parts = auth.split(' ');
|
|
168
|
+
if (parts.length !== 2 || parts[0] !== 'Bearer')
|
|
169
|
+
return null;
|
|
170
|
+
return parts[1];
|
|
171
|
+
}
|
|
172
|
+
function defaultUnauthorized(_req, reason) {
|
|
173
|
+
return new Response(JSON.stringify({ error: reason }), {
|
|
174
|
+
status: 401,
|
|
175
|
+
headers: { 'content-type': 'application/json' },
|
|
176
|
+
});
|
|
177
|
+
}
|
package/dist/db.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { TSchema } from '@sinclair/typebox';
|
|
2
|
+
export type SchemaToType<S> = S extends TSchema ? ReturnType<S['Decode']> : never;
|
|
3
|
+
export interface ModelConfig {
|
|
4
|
+
/** Primary key field (default: 'id') */
|
|
5
|
+
primaryKey?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface QueryBuilder<T> {
|
|
8
|
+
/** Filter by field equality */
|
|
9
|
+
where(field: keyof T, value: T[keyof T]): QueryBuilder<T>;
|
|
10
|
+
/** Filter by field inequality */
|
|
11
|
+
whereNot(field: keyof T, value: T[keyof T]): QueryBuilder<T>;
|
|
12
|
+
/** Filter with greater than */
|
|
13
|
+
whereGt(field: keyof T, value: number): QueryBuilder<T>;
|
|
14
|
+
/** Filter with less than */
|
|
15
|
+
whereLt(field: keyof T, value: number): QueryBuilder<T>;
|
|
16
|
+
/** Limit results */
|
|
17
|
+
limit(n: number): QueryBuilder<T>;
|
|
18
|
+
/** Offset results */
|
|
19
|
+
offset(n: number): QueryBuilder<T>;
|
|
20
|
+
/** Order by field */
|
|
21
|
+
orderBy(field: keyof T, direction?: 'asc' | 'desc'): QueryBuilder<T>;
|
|
22
|
+
/** Select specific fields */
|
|
23
|
+
select<K extends keyof T>(...fields: K[]): QueryBuilder<Pick<T, K>>;
|
|
24
|
+
/** Execute query and return results */
|
|
25
|
+
exec(): Promise<T[]>;
|
|
26
|
+
/** Execute query and return first result */
|
|
27
|
+
first(): Promise<T | null>;
|
|
28
|
+
/** Count results */
|
|
29
|
+
count(): Promise<number>;
|
|
30
|
+
}
|
|
31
|
+
export interface Model<T, S extends TSchema> {
|
|
32
|
+
/** Find by primary key */
|
|
33
|
+
findById(id: string): Promise<T | null>;
|
|
34
|
+
/** Find first matching record */
|
|
35
|
+
findFirst(where: Partial<T>): Promise<T | null>;
|
|
36
|
+
/** Find many matching records */
|
|
37
|
+
findMany(where?: Partial<T>): QueryBuilder<T>;
|
|
38
|
+
/** Create a new record */
|
|
39
|
+
create(data: T): Promise<T>;
|
|
40
|
+
/** Create many records */
|
|
41
|
+
createMany(data: T[]): Promise<T[]>;
|
|
42
|
+
/** Update by primary key */
|
|
43
|
+
update(id: string, data: Partial<T>): Promise<T | null>;
|
|
44
|
+
/** Delete by primary key */
|
|
45
|
+
delete(id: string): Promise<boolean>;
|
|
46
|
+
/** Delete many matching records */
|
|
47
|
+
deleteMany(where: Partial<T>): Promise<number>;
|
|
48
|
+
/** Count records */
|
|
49
|
+
count(where?: Partial<T>): Promise<number>;
|
|
50
|
+
/** Execute raw query */
|
|
51
|
+
raw(sql: string, ...args: unknown[]): Promise<T[]>;
|
|
52
|
+
/** Schema validation */
|
|
53
|
+
readonly schema: S;
|
|
54
|
+
/** Table name */
|
|
55
|
+
readonly tableName: string;
|
|
56
|
+
}
|
|
57
|
+
export interface Store {
|
|
58
|
+
findAll(table: string): Record<string, unknown>[];
|
|
59
|
+
findById(table: string, id: string, pk: string): Record<string, unknown> | null;
|
|
60
|
+
insert(table: string, data: Record<string, unknown>): Record<string, unknown>;
|
|
61
|
+
update(table: string, id: string, data: Record<string, unknown>, pk: string): Record<string, unknown> | null;
|
|
62
|
+
delete(table: string, id: string, pk: string): boolean;
|
|
63
|
+
deleteWhere(table: string, where: Record<string, unknown>, pk: string): number;
|
|
64
|
+
count(table: string, where?: Record<string, unknown>): number;
|
|
65
|
+
query(table: string, filters: Filter[]): Record<string, unknown>[];
|
|
66
|
+
}
|
|
67
|
+
interface Filter {
|
|
68
|
+
field: string;
|
|
69
|
+
op: '=' | '!=' | '>' | '<';
|
|
70
|
+
value: unknown;
|
|
71
|
+
}
|
|
72
|
+
/** Set a custom store adapter (e.g., for SQL databases) */
|
|
73
|
+
export declare function setStore(store: Store): void;
|
|
74
|
+
/** Get the current store (useful for testing) */
|
|
75
|
+
export declare function getStore(): Store;
|
|
76
|
+
/** Reset the store (useful for testing) */
|
|
77
|
+
export declare function resetStore(): void;
|
|
78
|
+
/**
|
|
79
|
+
* Define a typed database model.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* import { defineModel } from '@carlos-tzin/tzin/db'
|
|
84
|
+
* import { t } from '@carlos-tzin/tzin'
|
|
85
|
+
*
|
|
86
|
+
* const User = defineModel('users', t.Object({
|
|
87
|
+
* id: t.String(),
|
|
88
|
+
* name: t.String(),
|
|
89
|
+
* email: t.String(),
|
|
90
|
+
* }))
|
|
91
|
+
*
|
|
92
|
+
* const user = await User.findById('1')
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export declare function defineModel<S extends TSchema>(tableName: string, schema: S, config?: ModelConfig): Model<SchemaToType<S>, S>;
|
|
96
|
+
export {};
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// ── Memory store (default) ──────────────────────────────────────────
|
|
2
|
+
class MemoryStore {
|
|
3
|
+
data = new Map();
|
|
4
|
+
table(table) {
|
|
5
|
+
if (!this.data.has(table))
|
|
6
|
+
this.data.set(table, []);
|
|
7
|
+
return this.data.get(table);
|
|
8
|
+
}
|
|
9
|
+
findAll(table) {
|
|
10
|
+
return [...this.table(table)];
|
|
11
|
+
}
|
|
12
|
+
findById(table, id, pk) {
|
|
13
|
+
return this.table(table).find((r) => r[pk] === id) ?? null;
|
|
14
|
+
}
|
|
15
|
+
insert(table, data) {
|
|
16
|
+
this.table(table).push({ ...data });
|
|
17
|
+
return data;
|
|
18
|
+
}
|
|
19
|
+
update(table, id, data, pk) {
|
|
20
|
+
const row = this.table(table).find((r) => r[pk] === id);
|
|
21
|
+
if (!row)
|
|
22
|
+
return null;
|
|
23
|
+
Object.assign(row, data);
|
|
24
|
+
return { ...row };
|
|
25
|
+
}
|
|
26
|
+
delete(table, id, pk) {
|
|
27
|
+
const rows = this.table(table);
|
|
28
|
+
const idx = rows.findIndex((r) => r[pk] === id);
|
|
29
|
+
if (idx === -1)
|
|
30
|
+
return false;
|
|
31
|
+
rows.splice(idx, 1);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
deleteWhere(table, where, pk) {
|
|
35
|
+
const rows = this.table(table);
|
|
36
|
+
const before = rows.length;
|
|
37
|
+
this.data.set(table, rows.filter((r) => !Object.entries(where).every(([k, v]) => r[k] === v)));
|
|
38
|
+
return before - this.data.get(table).length;
|
|
39
|
+
}
|
|
40
|
+
count(table, where) {
|
|
41
|
+
if (!where)
|
|
42
|
+
return this.table(table).length;
|
|
43
|
+
return this.table(table).filter((r) => Object.entries(where).every(([k, v]) => r[k] === v)).length;
|
|
44
|
+
}
|
|
45
|
+
query(table, filters) {
|
|
46
|
+
return this.table(table).filter((row) => filters.every((f) => {
|
|
47
|
+
const val = row[f.field];
|
|
48
|
+
if (f.op === '=')
|
|
49
|
+
return val === f.value;
|
|
50
|
+
if (f.op === '!=')
|
|
51
|
+
return val !== f.value;
|
|
52
|
+
if (f.op === '>')
|
|
53
|
+
return val > f.value;
|
|
54
|
+
if (f.op === '<')
|
|
55
|
+
return val < f.value;
|
|
56
|
+
return true;
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// ── Global store ─────────────────────────────────────────────────────
|
|
61
|
+
let globalStore = new MemoryStore();
|
|
62
|
+
/** Set a custom store adapter (e.g., for SQL databases) */
|
|
63
|
+
export function setStore(store) {
|
|
64
|
+
globalStore = store;
|
|
65
|
+
}
|
|
66
|
+
/** Get the current store (useful for testing) */
|
|
67
|
+
export function getStore() {
|
|
68
|
+
return globalStore;
|
|
69
|
+
}
|
|
70
|
+
/** Reset the store (useful for testing) */
|
|
71
|
+
export function resetStore() {
|
|
72
|
+
globalStore = new MemoryStore();
|
|
73
|
+
}
|
|
74
|
+
// ── defineModel ──────────────────────────────────────────────────────
|
|
75
|
+
/**
|
|
76
|
+
* Define a typed database model.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* import { defineModel } from '@carlos-tzin/tzin/db'
|
|
81
|
+
* import { t } from '@carlos-tzin/tzin'
|
|
82
|
+
*
|
|
83
|
+
* const User = defineModel('users', t.Object({
|
|
84
|
+
* id: t.String(),
|
|
85
|
+
* name: t.String(),
|
|
86
|
+
* email: t.String(),
|
|
87
|
+
* }))
|
|
88
|
+
*
|
|
89
|
+
* const user = await User.findById('1')
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export function defineModel(tableName, schema, config) {
|
|
93
|
+
const pk = config?.primaryKey ?? 'id';
|
|
94
|
+
function applyFilters(rows, filters) {
|
|
95
|
+
return globalStore.query(tableName, filters);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
schema,
|
|
99
|
+
tableName,
|
|
100
|
+
async findById(id) {
|
|
101
|
+
const row = globalStore.findById(tableName, id, pk);
|
|
102
|
+
return row ? schema.Decode(row) : null;
|
|
103
|
+
},
|
|
104
|
+
async findFirst(where) {
|
|
105
|
+
const rows = globalStore.findAll(tableName).filter((r) => Object.entries(where).every(([k, v]) => r[k] === v));
|
|
106
|
+
return rows[0] ? schema.Decode(rows[0]) : null;
|
|
107
|
+
},
|
|
108
|
+
findMany(where) {
|
|
109
|
+
const filters = where
|
|
110
|
+
? Object.entries(where).map(([field, value]) => ({ field, op: '=', value }))
|
|
111
|
+
: [];
|
|
112
|
+
const builder = {
|
|
113
|
+
where(field, value) {
|
|
114
|
+
filters.push({ field: field, op: '=', value });
|
|
115
|
+
return builder;
|
|
116
|
+
},
|
|
117
|
+
whereNot(field, value) {
|
|
118
|
+
filters.push({ field: field, op: '!=', value });
|
|
119
|
+
return builder;
|
|
120
|
+
},
|
|
121
|
+
whereGt(field, value) {
|
|
122
|
+
filters.push({ field: field, op: '>', value });
|
|
123
|
+
return builder;
|
|
124
|
+
},
|
|
125
|
+
whereLt(field, value) {
|
|
126
|
+
filters.push({ field: field, op: '<', value });
|
|
127
|
+
return builder;
|
|
128
|
+
},
|
|
129
|
+
limit: (n) => {
|
|
130
|
+
// Store limit for later
|
|
131
|
+
const origExec = builder.exec;
|
|
132
|
+
builder.exec = async () => {
|
|
133
|
+
const rows = await origExec();
|
|
134
|
+
return rows.slice(0, n);
|
|
135
|
+
};
|
|
136
|
+
return builder;
|
|
137
|
+
},
|
|
138
|
+
offset: (n) => {
|
|
139
|
+
const origExec = builder.exec;
|
|
140
|
+
builder.exec = async () => {
|
|
141
|
+
const rows = await origExec();
|
|
142
|
+
return rows.slice(n);
|
|
143
|
+
};
|
|
144
|
+
return builder;
|
|
145
|
+
},
|
|
146
|
+
orderBy: (field, direction = 'asc') => {
|
|
147
|
+
const origExec = builder.exec;
|
|
148
|
+
builder.exec = async () => {
|
|
149
|
+
const rows = await origExec();
|
|
150
|
+
return rows.sort((a, b) => {
|
|
151
|
+
const av = a[field];
|
|
152
|
+
const bv = b[field];
|
|
153
|
+
return direction === 'asc' ? (av > bv ? 1 : -1) : (av < bv ? 1 : -1);
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
return builder;
|
|
157
|
+
},
|
|
158
|
+
select: (...fields) => {
|
|
159
|
+
const origExec = builder.exec;
|
|
160
|
+
builder.exec = async () => {
|
|
161
|
+
const rows = await origExec();
|
|
162
|
+
return rows.map((r) => {
|
|
163
|
+
const obj = {};
|
|
164
|
+
for (const f of fields)
|
|
165
|
+
obj[f] = r[f];
|
|
166
|
+
return obj;
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
return builder;
|
|
170
|
+
},
|
|
171
|
+
exec: async () => {
|
|
172
|
+
const rows = applyFilters([], filters);
|
|
173
|
+
return rows.map((r) => schema.Decode(r));
|
|
174
|
+
},
|
|
175
|
+
first: async () => {
|
|
176
|
+
const rows = await builder.exec();
|
|
177
|
+
return rows[0] ?? null;
|
|
178
|
+
},
|
|
179
|
+
count: async () => {
|
|
180
|
+
return globalStore.count(tableName, where);
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
return builder;
|
|
184
|
+
},
|
|
185
|
+
async create(data) {
|
|
186
|
+
const row = globalStore.insert(tableName, data);
|
|
187
|
+
return schema.Decode(row);
|
|
188
|
+
},
|
|
189
|
+
async createMany(data) {
|
|
190
|
+
return Promise.all(data.map((d) => this.create(d)));
|
|
191
|
+
},
|
|
192
|
+
async update(id, data) {
|
|
193
|
+
const row = globalStore.update(tableName, id, data, pk);
|
|
194
|
+
return row ? schema.Decode(row) : null;
|
|
195
|
+
},
|
|
196
|
+
async delete(id) {
|
|
197
|
+
return globalStore.delete(tableName, id, pk);
|
|
198
|
+
},
|
|
199
|
+
async deleteMany(where) {
|
|
200
|
+
return globalStore.deleteWhere(tableName, where, pk);
|
|
201
|
+
},
|
|
202
|
+
async count(where) {
|
|
203
|
+
return globalStore.count(tableName, where);
|
|
204
|
+
},
|
|
205
|
+
async raw(sql, ...args) {
|
|
206
|
+
// For in-memory store, this is a no-op
|
|
207
|
+
// Real DB adapters would implement this
|
|
208
|
+
console.warn('raw() not supported with in-memory store');
|
|
209
|
+
return [];
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carlos-tzin/tzin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Contract-first TypeScript framework. Types that scale, realtime channels with presence, and an MCP server for every API.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "The tzin authors",
|
|
@@ -46,6 +46,14 @@
|
|
|
46
46
|
"./test": {
|
|
47
47
|
"types": "./dist/test.d.ts",
|
|
48
48
|
"default": "./dist/test.js"
|
|
49
|
+
},
|
|
50
|
+
"./db": {
|
|
51
|
+
"types": "./dist/db.d.ts",
|
|
52
|
+
"default": "./dist/db.js"
|
|
53
|
+
},
|
|
54
|
+
"./auth": {
|
|
55
|
+
"types": "./dist/auth.d.ts",
|
|
56
|
+
"default": "./dist/auth.js"
|
|
49
57
|
}
|
|
50
58
|
},
|
|
51
59
|
"files": [
|