@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.
- package/LICENSE +202 -0
- package/README.md +13 -0
- package/dist/columns.d.ts +121 -0
- package/dist/columns.d.ts.map +1 -0
- package/dist/columns.js +101 -0
- package/dist/columns.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/instance.d.ts +46 -0
- package/dist/instance.d.ts.map +1 -0
- package/dist/instance.js +158 -0
- package/dist/instance.js.map +1 -0
- package/dist/model.d.ts +73 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +272 -0
- package/dist/model.js.map +1 -0
- package/dist/module.d.ts +19 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +29 -0
- package/dist/module.js.map +1 -0
- package/dist/pivot.d.ts +86 -0
- package/dist/pivot.d.ts.map +1 -0
- package/dist/pivot.js +145 -0
- package/dist/pivot.js.map +1 -0
- package/dist/query.d.ts +170 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +307 -0
- package/dist/query.js.map +1 -0
- package/dist/relations.d.ts +64 -0
- package/dist/relations.d.ts.map +1 -0
- package/dist/relations.js +26 -0
- package/dist/relations.js.map +1 -0
- package/dist/runtime.d.ts +28 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +149 -0
- package/dist/runtime.js.map +1 -0
- package/dist/slow-queries.d.ts +55 -0
- package/dist/slow-queries.d.ts.map +1 -0
- package/dist/slow-queries.js +96 -0
- package/dist/slow-queries.js.map +1 -0
- package/package.json +40 -0
|
@@ -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
|
+
}
|