@sungen/driver-db 3.1.2-beta.100
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/package.json +26 -0
- package/src/index.ts +60 -0
- package/src/patterns/database-patterns.ts +96 -0
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sungen/driver-db",
|
|
3
|
+
"version": "3.1.2-beta.100",
|
|
4
|
+
"description": "Sungen DB capability (Data Driver) — the @query annotation, query catalog, declarative DB steps, and the specs/db.ts runtime template. Plugs into @sun-asterisk/sungen via the capability SPI.",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"sungen": {
|
|
8
|
+
"capability": "db",
|
|
9
|
+
"emitDependencies": {
|
|
10
|
+
"pg": "^8",
|
|
11
|
+
"better-sqlite3": "^11"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "rm -rf dist && tsc -p tsconfig.json"
|
|
16
|
+
},
|
|
17
|
+
"author": "eqe team (engineer & quality) — Sun Asterisk",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@sun-asterisk/sungen": "3.1.2-beta.100"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"src"
|
|
25
|
+
]
|
|
26
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sungen/driver-db — Sungen DB capability (Data Driver).
|
|
3
|
+
*
|
|
4
|
+
* Owns the `@query` annotation: the named-query catalog (queries.yaml), the precondition codegen that
|
|
5
|
+
* binds a query result to `{{name}}`, the declarative DB step patterns, and the gate sensor that
|
|
6
|
+
* verifies every referenced query resolves + lints clean. Plugs into core via the capability SPI;
|
|
7
|
+
* core discovers and calls `register()` at runtime (core never imports this package).
|
|
8
|
+
*
|
|
9
|
+
* Relocated from core in R5.5 (was `database-patterns.ts` + the db parts of `builtins.ts`). Two
|
|
10
|
+
* pieces stay in core, mirroring the R5.4 boundary calls:
|
|
11
|
+
* - `query-catalog` (resolve/compile/lint) — shared with core's data-driven advisory lint, so it
|
|
12
|
+
* stays in core's harness and is imported here from `@sun-asterisk/sungen`;
|
|
13
|
+
* - the `specs-db.ts` runtime-helper template — emitted by the core generator via `runtimeHelpers`
|
|
14
|
+
* (resolved by filename from core's templates dir). Full relocation is an OE concern (#287).
|
|
15
|
+
* Verification of `@query` refs stays in core's generic `verification` gate sensor (it uses the
|
|
16
|
+
* core-resident catalog), so this driver registers no gate sensor.
|
|
17
|
+
*/
|
|
18
|
+
import type { CapabilityRegistry } from '@sun-asterisk/sungen';
|
|
19
|
+
import { parseQueryOverrides, resolveQuery, compileQuery } from '@sun-asterisk/sungen';
|
|
20
|
+
import { databasePatterns, isDbStep } from './patterns/database-patterns';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* DB precondition codegen: `@query:<name>[(p={{v}},…)]` → run the catalog query and bind its rows
|
|
24
|
+
* to `{{name}}`.
|
|
25
|
+
*/
|
|
26
|
+
function dbPreconditionCodegen(input: { tags: string[]; screenName: string; cwd: string }): Array<{ comment?: string; code: string; boundVars?: string[] }> {
|
|
27
|
+
const out: Array<{ comment?: string; code: string; boundVars?: string[] }> = [];
|
|
28
|
+
const TAG = /^@query:([A-Za-z_][A-Za-z0-9_]*)(?:\((.*)\))?$/;
|
|
29
|
+
for (const tag of input.tags) {
|
|
30
|
+
const m = tag.match(TAG);
|
|
31
|
+
if (!m) continue;
|
|
32
|
+
const name = m[1];
|
|
33
|
+
const overrides = parseQueryOverrides(m[2]);
|
|
34
|
+
const entry = resolveQuery(name, input.screenName, input.cwd); // throws (fail-fast) if missing/ambiguous
|
|
35
|
+
const { sql, paramNames } = compileQuery(entry);
|
|
36
|
+
const paramExprs = paramNames.map((p) => (p in overrides ? overrides[p] : `testData.get(${JSON.stringify(p)})`));
|
|
37
|
+
const label = JSON.stringify(entry.description ? `query "${name}" — ${entry.description}` : `query "${name}"`);
|
|
38
|
+
const ds = entry.datasource ? JSON.stringify(entry.datasource) : 'undefined';
|
|
39
|
+
out.push({
|
|
40
|
+
comment: `@query:${name} → bind {{${name}}} from ${entry.datasource || 'default datasource'}`,
|
|
41
|
+
code: `testData.bind(${JSON.stringify(name)}, await db.fetchQuery(${label}, ${JSON.stringify(sql)}, [${paramExprs.join(', ')}], ${ds}));`,
|
|
42
|
+
boundVars: [name],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Register the DB (Data Driver) capability. */
|
|
49
|
+
export function register(registry: CapabilityRegistry): void {
|
|
50
|
+
registry.register({
|
|
51
|
+
id: 'db',
|
|
52
|
+
annotations: ['@query'],
|
|
53
|
+
patterns: [...databasePatterns],
|
|
54
|
+
runtimeHelpers: [{ file: 'db.ts', template: 'specs-db.ts' }], // template resolved from core's templates dir
|
|
55
|
+
detectsStep: isDbStep, // declarative `User see [table] row where …` (no annotation tag)
|
|
56
|
+
preconditionCodegen: dbPreconditionCodegen, // @query:<name> → bind {{name}}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const sungenDriver = { capability: 'db', register } as const;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { ParsedStep } from '@sun-asterisk/sungen';
|
|
2
|
+
import { StepPattern, PatternContext } from '@sun-asterisk/sungen';
|
|
3
|
+
import { MappedStep } from '@sun-asterisk/sungen';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Database verification patterns (Data Driver v1) — declarative, no-SQL DB assertions
|
|
7
|
+
* that compile to calls on the runtime `db` helper (specs/db.ts). Read-only.
|
|
8
|
+
*
|
|
9
|
+
* User see [users] row where [email] is {{reg_email}}
|
|
10
|
+
* User see [users] row where [email] is {{reg_email}} has [status] = "active"
|
|
11
|
+
* User see [users] no row where [email] is {{dup_email}}
|
|
12
|
+
* User see [orders] where [buyer] is {{buyer}} count is {{expected_count}}
|
|
13
|
+
*
|
|
14
|
+
* Identifiers ([table]/[column]) are validated by the helper; values bind as parameters.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const TABLE = String.raw`\[([A-Za-z_][A-Za-z0-9_]*)\]`;
|
|
18
|
+
const VALUE = String.raw`\{\{[^}]+\}\}|"[^"]*"|'[^']*'|-?\d+(?:\.\d+)?`;
|
|
19
|
+
|
|
20
|
+
const reRow = new RegExp(`see\\s+${TABLE}\\s+row\\s+where\\b`, 'i');
|
|
21
|
+
const reNoRow = new RegExp(`see\\s+${TABLE}\\s+no\\s+row\\s+where\\b`, 'i');
|
|
22
|
+
const reCount = new RegExp(`see\\s+${TABLE}.*\\bcount\\s+is\\b`, 'i');
|
|
23
|
+
|
|
24
|
+
/** True when a step is a declarative DB-verification step (used to wire the `db` import).
|
|
25
|
+
* Named queries are invoked via the `@query:` annotation, not a step — see code-generator. */
|
|
26
|
+
export function isDbStep(text: string): boolean {
|
|
27
|
+
return reNoRow.test(text) || reRow.test(text) || reCount.test(text);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Render a value token (`{{var}}` | "literal" | 'literal' | number) as a JS expression. */
|
|
31
|
+
function valueExpr(token: string): string {
|
|
32
|
+
const t = token.trim();
|
|
33
|
+
const v = t.match(/^\{\{\s*([^}]+?)\s*\}\}$/);
|
|
34
|
+
if (v) return `testData.get(${JSON.stringify(v[1])})`;
|
|
35
|
+
const q = t.match(/^["'](.*)["']$/);
|
|
36
|
+
if (q) return JSON.stringify(q[1]);
|
|
37
|
+
if (/^-?\d+(?:\.\d+)?$/.test(t)) return t;
|
|
38
|
+
return JSON.stringify(t);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Parse a `[col] is VALUE [and [col2] is VALUE2]` segment into a JS object literal. */
|
|
42
|
+
function parseFilter(segment: string): string {
|
|
43
|
+
const re = new RegExp(`\\[([A-Za-z_][A-Za-z0-9_]*)\\]\\s+is\\s+(${VALUE})`, 'gi');
|
|
44
|
+
const parts: string[] = [];
|
|
45
|
+
let m: RegExpExecArray | null;
|
|
46
|
+
while ((m = re.exec(segment))) parts.push(`${JSON.stringify(m[1])}: ${valueExpr(m[2])}`);
|
|
47
|
+
return `{ ${parts.join(', ')} }`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Parse a `has [col] = VALUE [and [col2] = VALUE2]` segment into a JS object literal. */
|
|
51
|
+
function parseExpected(segment: string): string {
|
|
52
|
+
const re = new RegExp(`\\[([A-Za-z_][A-Za-z0-9_]*)\\]\\s*=\\s*(${VALUE})`, 'gi');
|
|
53
|
+
const parts: string[] = [];
|
|
54
|
+
let m: RegExpExecArray | null;
|
|
55
|
+
while ((m = re.exec(segment))) parts.push(`${JSON.stringify(m[1])}: ${valueExpr(m[2])}`);
|
|
56
|
+
return parts.length ? `{ ${parts.join(', ')} }` : '';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const databasePatterns: StepPattern[] = [
|
|
60
|
+
{
|
|
61
|
+
name: 'db-no-row',
|
|
62
|
+
priority: 60, // above generic see-assertions
|
|
63
|
+
matcher: (step: ParsedStep) => reNoRow.test(step.text),
|
|
64
|
+
generator: (step: ParsedStep, _ctx: PatternContext): MappedStep => {
|
|
65
|
+
const m = step.text.match(new RegExp(`${TABLE}\\s+no\\s+row\\s+where\\s+(.+)$`, 'i'))!;
|
|
66
|
+
const table = m[1];
|
|
67
|
+
const filter = parseFilter(m[2]);
|
|
68
|
+
return { code: `await db.assertNoRow(${JSON.stringify(table)}, ${filter});`, comment: `DB: no row in ${table}` };
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'db-count',
|
|
73
|
+
priority: 60,
|
|
74
|
+
matcher: (step: ParsedStep) => reCount.test(step.text) && !reRow.test(step.text),
|
|
75
|
+
generator: (step: ParsedStep, _ctx: PatternContext): MappedStep => {
|
|
76
|
+
const m = step.text.match(new RegExp(`${TABLE}(?:\\s+where\\s+(.+?))?\\s+count\\s+is\\s+(${VALUE})`, 'i'))!;
|
|
77
|
+
const table = m[1];
|
|
78
|
+
const filter = m[2] ? parseFilter(m[2]) : '{}';
|
|
79
|
+
return { code: `await db.assertCount(${JSON.stringify(table)}, ${filter}, Number(${valueExpr(m[3])}));`, comment: `DB: count rows in ${table}` };
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'db-row',
|
|
84
|
+
priority: 60,
|
|
85
|
+
matcher: (step: ParsedStep) => reRow.test(step.text),
|
|
86
|
+
generator: (step: ParsedStep, _ctx: PatternContext): MappedStep => {
|
|
87
|
+
// [table] row where <filter> [has <expected>]
|
|
88
|
+
const m = step.text.match(new RegExp(`${TABLE}\\s+row\\s+where\\s+(.+?)(?:\\s+has\\s+(.+))?$`, 'i'))!;
|
|
89
|
+
const table = m[1];
|
|
90
|
+
const filter = parseFilter(m[2]);
|
|
91
|
+
const expected = m[3] ? parseExpected(m[3]) : '';
|
|
92
|
+
const args = expected ? `${JSON.stringify(table)}, ${filter}, ${expected}` : `${JSON.stringify(table)}, ${filter}`;
|
|
93
|
+
return { code: `await db.assertRow(${args});`, comment: `DB: row in ${table}` };
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
];
|