@assemora/data 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.
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Slow query logging (SPEC.md §88).
3
+ *
4
+ * The data layer owns the query, so it owns the timing. What it writes down is the
5
+ * *shape* of a query that took too long — which table, which operation, which columns
6
+ * were filtered on, which relations were loaded — and never one value the caller
7
+ * passed in.
8
+ *
9
+ * The logger arrives the way the adapter does, through a registration made once when
10
+ * the application boots (SPEC.md §33). A model declaration takes no logger, a query
11
+ * takes no logger, and nothing in the data layer's public API grew a parameter for
12
+ * something only the composition root can know.
13
+ */
14
+ import { type Logger } from '@assemora/core';
15
+ import type { QueryAst } from '@assemora/database';
16
+ export type SlowQueryLogOptions = {
17
+ /**
18
+ * A query that takes at least this long is written down. 200ms.
19
+ *
20
+ * SPEC.md §89 budgets a whole REST read at 100ms and a whole mutation at 150ms, so a
21
+ * single statement above 200ms is already outside what the application promised —
22
+ * which is what makes this quiet in a healthy process and loud in a sick one. The
23
+ * threshold *is* the throttle: a line is only ever written for a query that was
24
+ * slow, and when that stops being rare, that is the thing worth knowing.
25
+ *
26
+ * `0` logs every query, which is occasionally what you want while looking at one
27
+ * endpoint and never what you want in production.
28
+ */
29
+ readonly slowerThanMs?: number;
30
+ };
31
+ /** Above the §89 budget for a whole request, so a healthy application writes nothing. */
32
+ export declare const DEFAULT_SLOW_QUERY_MS = 200;
33
+ /**
34
+ * Starts writing down the queries that take too long.
35
+ *
36
+ * ```ts
37
+ * useSlowQueryLog(app.logger)
38
+ * useSlowQueryLog(app.logger, { slowerThanMs: 50 })
39
+ * ```
40
+ *
41
+ * Registering is the switch, and `assemora()` registers it for every application it
42
+ * builds — so a project that says nothing still gets the log, and one built by hand
43
+ * with `createApplication()` opts in with this one line.
44
+ */
45
+ export declare const useSlowQueryLog: (logger: Logger, options?: SlowQueryLogOptions) => void;
46
+ export declare const clearSlowQueryLog: () => void;
47
+ /**
48
+ * Notes that a query ran, and writes it down if it was slow.
49
+ *
50
+ * `answer` is what the adapter replied with, and it is absent when the query failed.
51
+ * Only an array is counted: a `count` replies with the number it was asked for, which
52
+ * is an answer rather than a row count, and this line carries no answers.
53
+ */
54
+ export declare const recordQuery: (query: QueryAst, durationMs: number, answer?: unknown) => void;
55
+ //# sourceMappingURL=slow-queries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slow-queries.d.ts","sourceRoot":"","sources":["../src/slow-queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAsB,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAChE,OAAO,KAAK,EAAa,QAAQ,EAAgB,MAAM,oBAAoB,CAAA;AAE3E,MAAM,MAAM,mBAAmB,GAAG;IAChC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAC/B,CAAA;AAED,yFAAyF;AACzF,eAAO,MAAM,qBAAqB,MAAM,CAAA;AAIxC;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,eAAe,WAAY,MAAM,YAAW,mBAAmB,KAAQ,IAYnF,CAAA;AAED,eAAO,MAAM,iBAAiB,QAAO,IAEpC,CAAA;AAuCD;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,UAAW,QAAQ,cAAc,MAAM,WAAW,OAAO,KAAG,IAcnF,CAAA"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Slow query logging (SPEC.md §88).
3
+ *
4
+ * The data layer owns the query, so it owns the timing. What it writes down is the
5
+ * *shape* of a query that took too long — which table, which operation, which columns
6
+ * were filtered on, which relations were loaded — and never one value the caller
7
+ * passed in.
8
+ *
9
+ * The logger arrives the way the adapter does, through a registration made once when
10
+ * the application boots (SPEC.md §33). A model declaration takes no logger, a query
11
+ * takes no logger, and nothing in the data layer's public API grew a parameter for
12
+ * something only the composition root can know.
13
+ */
14
+ import { ConfigurationError } from '@assemora/core';
15
+ /** Above the §89 budget for a whole request, so a healthy application writes nothing. */
16
+ export const DEFAULT_SLOW_QUERY_MS = 200;
17
+ let log;
18
+ /**
19
+ * Starts writing down the queries that take too long.
20
+ *
21
+ * ```ts
22
+ * useSlowQueryLog(app.logger)
23
+ * useSlowQueryLog(app.logger, { slowerThanMs: 50 })
24
+ * ```
25
+ *
26
+ * Registering is the switch, and `assemora()` registers it for every application it
27
+ * builds — so a project that says nothing still gets the log, and one built by hand
28
+ * with `createApplication()` opts in with this one line.
29
+ */
30
+ export const useSlowQueryLog = (logger, options = {}) => {
31
+ const slowerThanMs = options.slowerThanMs ?? DEFAULT_SLOW_QUERY_MS;
32
+ // A NaN threshold compares false against everything, so an unread environment
33
+ // variable would switch the log off in silence rather than fail where it was written.
34
+ if (!Number.isFinite(slowerThanMs) || slowerThanMs < 0) {
35
+ throw new ConfigurationError('A slow query threshold is a number of milliseconds that is not negative. Pass 0 to log every query.');
36
+ }
37
+ log = { logger, slowerThanMs };
38
+ };
39
+ export const clearSlowQueryLog = () => {
40
+ log = undefined;
41
+ };
42
+ /**
43
+ * What a condition was, with everything the caller passed taken out of it.
44
+ *
45
+ * A column name is schema. It is already in the OpenAPI document, in the generated SDK
46
+ * and in the MCP tool list, and knowing which columns a slow query filtered on is the
47
+ * whole difference between a line that names the missing index and a line that names
48
+ * only the table.
49
+ *
50
+ * A *value* is the caller's, and this is the one place in the framework where that
51
+ * distinction has teeth: a `where` carries whatever was handed to it — an email
52
+ * address, a session token digest, a password on its way to be compared — and a slow
53
+ * query log is exactly the file that gets attached to a ticket and pasted into a chat.
54
+ * So nothing that was passed in is written here. That includes a JSON path, which
55
+ * names a key inside a document that has no schema for it to belong to.
56
+ */
57
+ const shapeOf = (conditions) => conditions.flatMap((condition) => {
58
+ if (condition.kind === 'group')
59
+ return shapeOf(condition.conditions);
60
+ if (condition.kind === 'json')
61
+ return [`${condition.field} json ${condition.operator}`];
62
+ return [`${condition.field} ${condition.operator}`];
63
+ });
64
+ /**
65
+ * The relation paths the query loaded, as the caller wrote them.
66
+ *
67
+ * Here because SPEC.md §89 asks for N+1 queries to be caught by logs as well as by
68
+ * tests, and the same relation appearing under one model over and over is what that
69
+ * looks like from the outside.
70
+ */
71
+ const pathsOf = (loads, prefix = '') => loads.flatMap((load) => {
72
+ const path = `${prefix}${load.relation}`;
73
+ return load.nested.length === 0 ? [path] : pathsOf(load.nested, `${path}.`);
74
+ });
75
+ /**
76
+ * Notes that a query ran, and writes it down if it was slow.
77
+ *
78
+ * `answer` is what the adapter replied with, and it is absent when the query failed.
79
+ * Only an array is counted: a `count` replies with the number it was asked for, which
80
+ * is an answer rather than a row count, and this line carries no answers.
81
+ */
82
+ export const recordQuery = (query, durationMs, answer) => {
83
+ if (log === undefined || durationMs < log.slowerThanMs)
84
+ return;
85
+ const filters = shapeOf(query.where);
86
+ const relations = pathsOf(query.with);
87
+ log.logger.warn('A query was slower than the threshold', {
88
+ model: query.model,
89
+ operation: query.operation,
90
+ durationMs,
91
+ ...(Array.isArray(answer) ? { rows: answer.length } : {}),
92
+ ...(filters.length === 0 ? {} : { filters }),
93
+ ...(relations.length === 0 ? {} : { relations }),
94
+ });
95
+ };
96
+ //# sourceMappingURL=slow-queries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slow-queries.js","sourceRoot":"","sources":["../src/slow-queries.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@assemora/data",
3
+ "version": "0.1.0",
4
+ "description": "Assemora Data: model(), query builder, relations, scopes, Query AST",
5
+ "license": "Apache-2.0",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/assemora/assemora.git",
12
+ "directory": "packages/data"
13
+ },
14
+ "type": "module",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js"
19
+ }
20
+ },
21
+ "main": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "files": [
24
+ "dist",
25
+ "!dist/.tsbuildinfo"
26
+ ],
27
+ "dependencies": {
28
+ "@assemora/schema": "0.1.0",
29
+ "@assemora/core": "0.1.0",
30
+ "@assemora/database": "0.1.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "24.13.3"
34
+ },
35
+ "scripts": {
36
+ "build": "tsc -b tsconfig.build.json",
37
+ "typecheck": "tsc -p tsconfig.json --noEmit",
38
+ "clean": "rm -rf dist *.tsbuildinfo"
39
+ }
40
+ }