• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rogerpadilla / uql / 28907893047

08 Jul 2026 12:15AM UTC coverage: 94.906% (-0.002%) from 94.908%
28907893047

push

github

rogerpadilla
chore(package): update gitHead to latest commit hash

3228 of 3575 branches covered (90.29%)

Branch coverage included in aggregate %.

5603 of 5730 relevant lines covered (97.78%)

385.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.0
/packages/uql-orm/src/http/handler.ts
1
import { withContext } from '../context/context.js';
2
import { getEntities, getMeta } from '../entity/index.js';
3
import { getQuerier } from '../options.js';
4
import type {
5
  EntityMeta,
6
  IdValue,
7
  Querier,
8
  Query,
9
  QuerySearch,
10
  Type,
11
  UpdatePayload,
12
  UqlContext,
13
} from '../type/index.js';
14
import {
15
  type CrudOperation,
16
  entityPath,
17
  type HttpMethod,
18
  matchRoute,
19
  type RequestSuccessResponse,
20
  type RouteMatch,
21
} from './contract.js';
22
import { parseQueryParams } from './query.js';
23

24
/**
25
 * Framework-normalized request: adapters (express, fetch, ...) reduce their native
26
 * request to this shape and get back a status + JSON body.
27
 */
28
export type HandlerRequest<Ctx = unknown> = {
29
  readonly method: string;
30
  readonly entityPath: string;
31
  readonly subPath?: string;
32
  /**
33
   * raw query-string entries; JSON values may still be stringified.
34
   */
35
  readonly query?: Record<string, unknown>;
36
  /**
37
   * parsed JSON body.
38
   */
39
  readonly body?: unknown;
40
  /**
41
   * adapter-supplied request context (express `req`, fetch `Request`, ...), passed through to hooks.
42
   */
43
  readonly context: Ctx;
44
};
45

46
export type HandlerResponse = {
47
  readonly status: number;
48
  readonly body: unknown;
49
};
50

51
/**
52
 * Wire flags ride inside the query object (as strings on GET, booleans on QUERY),
53
 * so hooks can set/override them, e.g. force `hardDelete: true` in `preFilter`.
54
 */
55
type WireFlags = {
56
  readonly hardDelete?: unknown;
57
  readonly count?: unknown;
58
};
59

60
export type HookContext<E extends object, Ctx = unknown> = {
61
  readonly meta: EntityMeta<E>;
62
  readonly op: CrudOperation;
63
  readonly method: HttpMethod;
64
  /**
65
   * parsed query - mutate in place or reassign to enforce filters (tenant scoping, row-level rules).
66
   */
67
  query: Query<E>;
68
  /**
69
   * request payload - reassignable for sanitization or field injection.
70
   */
71
  body?: unknown;
72
  /**
73
   * adapter-supplied request context - the auth/tenant source (e.g. `req.user`).
74
   */
75
  readonly context: Ctx;
76
};
77

78
export type Hook<Ctx = unknown> = <E extends object>(ctx: HookContext<E, Ctx>) => void | Promise<void>;
79

80
export type ResponseHook<Ctx = unknown> = <E extends object>(
81
  ctx: HookContext<E, Ctx>,
82
  envelope: RequestSuccessResponse<unknown>,
83
) => void | Promise<void>;
84

85
export type RequestHandlerOptions<Ctx = unknown> = {
86
  // biome-ignore lint/suspicious/noExplicitAny: accepts any entity constructor
87
  include?: Type<any>[];
88
  // biome-ignore lint/suspicious/noExplicitAny: accepts any entity constructor
89
  exclude?: Type<any>[];
90
  /**
91
   * Allow augment any kind of request before it runs. Hooks may be async
92
   * and abort the request by throwing (a numeric `status` on the error is honored).
93
   */
94
  pre?: Hook<Ctx>;
95
  /**
96
   * Allow augment a save request (POST | PUT | PATCH) before it runs.
97
   */
98
  preSave?: Hook<Ctx>;
99
  /**
100
   * Allow augment a filter request (GET | DELETE) before it runs.
101
   */
102
  preFilter?: Hook<Ctx>;
103
  /**
104
   * Shape the successful response before it is sent: strip sensitive fields,
105
   * derive presentation fields, or coerce null data. Mutate `envelope.data` in place
106
   * or reassign it. Runs after the operation (and after commit for writes).
107
   */
108
  post?: ResponseHook<Ctx>;
109
  /**
110
   * Derive the ambient {@link UqlContext} (e.g. `{ tenantId, userId }`) from the adapter request.
111
   * The whole request runs inside `withContext`, so parameterized/`security` filters are scoped
112
   * automatically. Derive tenant/auth from a verified source (session, JWT) - never trust the client.
113
   */
114
  getContext?: (context: Ctx) => UqlContext | undefined | Promise<UqlContext | undefined>;
115
};
116

117
/**
118
 * Returns `undefined` synchronously for an unknown entity or route so adapters can fall through
119
 * (e.g. express `next()`); rejects with the original error on failure so adapters map it
120
 * (e.g. `toErrorResponse`).
121
 */
122
export type RequestHandler<Ctx = unknown> = (req: HandlerRequest<Ctx>) => Promise<HandlerResponse> | undefined;
123

124
export function createRequestHandler<Ctx = unknown>(opts: RequestHandlerOptions<Ctx> = {}): RequestHandler<Ctx> {
67✔
125
  const { include, exclude, pre, preSave, preFilter, post, getContext } = opts;
67✔
126

127
  let entities = include ?? getEntities();
67✔
128
  if (exclude) {
67✔
129
    entities = entities.filter((entity) => !exclude.includes(entity));
4✔
130
  }
131
  if (!entities.length) {
67✔
132
    throw new TypeError('no entities for the uql middleware');
2✔
133
  }
134

135
  // biome-ignore lint/suspicious/noExplicitAny: heterogeneous entity map
136
  const entityByPath = new Map<string, Type<any>>(entities.map((entity) => [entityPath(entity), entity]));
80✔
137

138
  return (req) => {
65✔
139
    const entity = entityByPath.get(req.entityPath);
62✔
140
    if (!entity) {
62✔
141
      return undefined;
5✔
142
    }
143
    const match = matchRoute(req.method, req.subPath);
57✔
144
    if (!match) {
57✔
145
      return undefined;
3✔
146
    }
147
    return run(entity, match, req);
54✔
148
  };
149

150
  async function run<E extends object>(
151
    entity: Type<E>,
152
    { op, method, id }: RouteMatch,
153
    req: HandlerRequest<Ctx>,
154
  ): Promise<HandlerResponse> {
155
    const meta = getMeta(entity);
54✔
156
    // QUERY (RFC 10008) carries the JSON query in the body instead of the query string
157
    const rawQuery = method === 'QUERY' ? (req.body as Record<string, unknown> | undefined) : req.query;
54✔
158

159
    const hookCtx: HookContext<E, Ctx> = {
54✔
160
      meta,
161
      op,
162
      method,
163
      query: parseQueryParams(rawQuery) as Query<E>,
164
      body: req.body,
165
      context: req.context,
166
    };
167
    const appContext = (await getContext?.(req.context)) ?? {};
54✔
168
    // Scope the whole request (hooks + querier + relation/cascade queries) to the resolved context.
169
    return withContext(appContext, async () => {
54✔
170
      await pre?.(hookCtx);
53✔
171
      if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
51✔
172
        await preSave?.(hookCtx);
16✔
173
      } else {
174
        await preFilter?.(hookCtx);
35✔
175
      }
176

177
      const resp = await dispatch();
51✔
178
      if (post) {
45✔
179
        await post(hookCtx, resp.body as RequestSuccessResponse<unknown>);
3✔
180
      }
181
      return resp;
45✔
182
    });
183

184
    function dispatch(): Promise<HandlerResponse> {
185
      // read post-hooks so both in-place mutation and reassignment of hookCtx.query apply
186
      const query = hookCtx.query;
51✔
187
      const flags = query as WireFlags;
51✔
188
      const hardDelete = flags.hardDelete === 'true' || flags.hardDelete === true;
51✔
189
      switch (op) {
51✔
190
        case 'findOne':
191
          return withQuerier(async (querier) => {
10✔
192
            const data = await querier.findOne(entity, query);
10✔
193
            return ok({ data, count: data ? 1 : 0 });
6✔
194
          });
195
        case 'count':
196
          return withQuerier(async (querier) => {
1✔
197
            const count = await querier.count(entity, query);
1✔
198
            return ok({ data: count, count });
1✔
199
          });
200
        case 'findOneById':
201
          return withQuerier(async (querier) => {
4✔
202
            const data = await querier.findOne(entity, buildIdQuery(meta, id, query));
4✔
203
            return ok({ data, count: data ? 1 : 0 });
4!
204
          });
205
        case 'findMany':
206
          return withQuerier(async (querier) => {
14✔
207
            const findManyPromise = querier.findMany(entity, query);
14✔
208
            const countPromise = flags.count ? querier.count(entity, query) : undefined;
14✔
209
            const [data, count] = await Promise.all([findManyPromise, countPromise]);
14✔
210
            return ok({ data, count });
14✔
211
          });
212
        case 'insertOne':
213
          return withTransaction(async (querier) => {
8✔
214
            const data = await querier.insertOne(entity, hookCtx.body as E);
8✔
215
            return ok({ data, count: 1 });
6✔
216
          });
217
        case 'insertMany':
218
          return withTransaction(async (querier) => {
1✔
219
            const data = await querier.insertMany(entity, hookCtx.body as E[]);
1✔
220
            return ok({ data, count: data.length });
1✔
221
          });
222
        case 'saveOne':
223
          return withTransaction(async (querier) => {
2✔
224
            const data = await querier.saveOne(entity, hookCtx.body as E);
2✔
225
            return ok({ data, count: 1 });
2✔
226
          });
227
        case 'saveMany':
228
          return withTransaction(async (querier) => {
1✔
229
            const data = await querier.saveMany(entity, hookCtx.body as E[]);
1✔
230
            return ok({ data, count: data.length });
1✔
231
          });
232
        case 'updateOneById':
233
          return withTransaction(async (querier) => {
3✔
234
            const count = await querier.updateMany(
3✔
235
              entity,
236
              buildIdQuery(meta, id, query),
237
              hookCtx.body as UpdatePayload<E>,
238
            );
239
            return ok({ data: id, count });
3✔
240
          });
241
        case 'updateMany':
242
          return withTransaction(async (querier) => {
1✔
243
            const count = await querier.updateMany(entity, query as QuerySearch<E>, hookCtx.body as UpdatePayload<E>);
1✔
244
            return ok({ data: count, count });
1✔
245
          });
246
        case 'deleteOneById':
247
          return withTransaction(async (querier) => {
4✔
248
            const count = await querier.deleteMany(entity, buildIdQuery(meta, id, query), { hardDelete });
4✔
249
            return ok({ data: id, count });
4✔
250
          });
251
        case 'deleteMany':
252
          return withTransaction(async (querier) => {
2✔
253
            const founds = await querier.findMany(entity, query);
2✔
254
            let ids: IdValue<E>[] = [];
2✔
255
            let count = 0;
2✔
256
            if (founds.length && meta.id) {
2✔
257
              const idKey = meta.id;
1✔
258
              ids = founds.map((found) => found[idKey]);
2✔
259
              count = await querier.deleteMany(entity, { $where: ids }, { hardDelete });
1✔
260
            }
261
            return ok({ data: ids, count });
2✔
262
          });
263
      }
264
    }
265
  }
266
}
267

268
function ok(body: unknown): HandlerResponse {
269
  return { status: 200, body };
45✔
270
}
271

272
async function withQuerier(fn: (querier: Querier) => Promise<HandlerResponse>): Promise<HandlerResponse> {
273
  const querier = await getQuerier();
29✔
274
  try {
29✔
275
    return await fn(querier);
29✔
276
  } finally {
277
    await querier.release();
29✔
278
  }
279
}
280

281
async function withTransaction(fn: (querier: Querier) => Promise<HandlerResponse>): Promise<HandlerResponse> {
282
  const querier = await getQuerier();
22✔
283
  try {
22✔
284
    await querier.beginTransaction();
22✔
285
    const resp = await fn(querier);
22✔
286
    await querier.commitTransaction();
20✔
287
    return resp;
20✔
288
  } catch (err) {
289
    await querier.rollbackTransaction().catch(() => {});
2✔
290
    throw err;
2✔
291
  } finally {
292
    await querier.release();
22✔
293
  }
294
}
295

296
function buildIdQuery<E extends object>(meta: EntityMeta<E>, id: string | undefined, query: Query<E>): Query<E> {
297
  const idKey = meta.id as string;
11✔
298
  const where = query.$where;
11✔
299
  if (Array.isArray(where)) {
11✔
300
    query.$where = { $and: [{ [idKey]: { $in: where } }, { [idKey]: id }] } as Query<E>['$where'];
4✔
301
  } else if (typeof where === 'object' && where !== null) {
7!
302
    query.$where = { ...where, [idKey]: id } as Query<E>['$where'];
7✔
303
  } else {
304
    query.$where = { [idKey]: id } as Query<E>['$where'];
×
305
  }
306
  return query;
11✔
307
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc