@sqldoc/core 0.0.1
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 +33 -0
- package/src/__tests__/ast/sqlparser-ts.test.ts +117 -0
- package/src/__tests__/blocks.test.ts +80 -0
- package/src/__tests__/compile.test.ts +407 -0
- package/src/__tests__/compiler/compile.test.ts +363 -0
- package/src/__tests__/lint-rules.test.ts +249 -0
- package/src/__tests__/lint.test.ts +270 -0
- package/src/__tests__/parser.test.ts +169 -0
- package/src/__tests__/tags.sql +15 -0
- package/src/__tests__/validator.test.ts +210 -0
- package/src/ast/adapter.ts +10 -0
- package/src/ast/index.ts +3 -0
- package/src/ast/sqlparser-ts.ts +218 -0
- package/src/ast/types.ts +28 -0
- package/src/blocks.ts +242 -0
- package/src/compiler/compile.ts +783 -0
- package/src/compiler/config.ts +102 -0
- package/src/compiler/index.ts +29 -0
- package/src/compiler/types.ts +320 -0
- package/src/index.ts +72 -0
- package/src/lint.ts +127 -0
- package/src/loader.ts +102 -0
- package/src/parser.ts +202 -0
- package/src/ts-import.ts +70 -0
- package/src/types.ts +111 -0
- package/src/utils.ts +31 -0
- package/src/validator.ts +324 -0
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "module",
|
|
3
|
+
"name": "@sqldoc/core",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "Core engine for sqldoc -- parser, loader, validator, types",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"import": "./src/index.ts",
|
|
10
|
+
"default": "./src/index.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"main": "./src/index.ts",
|
|
14
|
+
"types": "./src/index.ts",
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"package.json"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"bundle-require": "^5.1.0",
|
|
21
|
+
"@sqldoc/sqlparser-ts": "0.61.1"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@biomejs/biome": "^2.4.8",
|
|
25
|
+
"typescript": "^5.9.3",
|
|
26
|
+
"vitest": "^4.1.0"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"lint": "biome check src/",
|
|
31
|
+
"typecheck": "tsc --noEmit"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it } from 'vitest'
|
|
2
|
+
import { SqlparserTsAdapter } from '../../ast/sqlparser-ts'
|
|
3
|
+
import type { SqlStatement } from '../../ast/types'
|
|
4
|
+
|
|
5
|
+
const TEST_SQL = `CREATE TABLE users (
|
|
6
|
+
id BIGSERIAL PRIMARY KEY,
|
|
7
|
+
email TEXT NOT NULL UNIQUE,
|
|
8
|
+
name TEXT
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
CREATE VIEW active_users AS SELECT * FROM users WHERE active = true;
|
|
12
|
+
|
|
13
|
+
CREATE INDEX idx_users_email ON users (email);
|
|
14
|
+
|
|
15
|
+
CREATE TYPE user_role AS ENUM ('admin', 'user', 'guest');
|
|
16
|
+
|
|
17
|
+
CREATE FUNCTION get_user(p_id BIGINT) RETURNS VOID LANGUAGE plpgsql AS $$ BEGIN END; $$;
|
|
18
|
+
|
|
19
|
+
CREATE TRIGGER user_audit AFTER INSERT ON users FOR EACH ROW EXECUTE FUNCTION audit_fn();`
|
|
20
|
+
|
|
21
|
+
describe('SqlparserTsAdapter', () => {
|
|
22
|
+
let adapter: SqlparserTsAdapter
|
|
23
|
+
let stmts: SqlStatement[]
|
|
24
|
+
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
adapter = new SqlparserTsAdapter()
|
|
27
|
+
await adapter.init()
|
|
28
|
+
stmts = adapter.parseStatements(TEST_SQL)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('parses CREATE TABLE with kind, objectName, and columns', () => {
|
|
32
|
+
const table = stmts.find((s) => s.kind === 'table')
|
|
33
|
+
expect(table).toBeDefined()
|
|
34
|
+
expect(table!.objectName).toBe('users')
|
|
35
|
+
expect(table!.columns).toBeDefined()
|
|
36
|
+
expect(table!.columns!.length).toBe(3)
|
|
37
|
+
|
|
38
|
+
const [id, email, name] = table!.columns!
|
|
39
|
+
expect(id.name).toBe('id')
|
|
40
|
+
expect(id.dataType).toBe('bigserial')
|
|
41
|
+
expect(email.name).toBe('email')
|
|
42
|
+
expect(email.dataType).toBe('text')
|
|
43
|
+
expect(name.name).toBe('name')
|
|
44
|
+
expect(name.dataType).toBe('text')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('parses CREATE VIEW with kind and objectName', () => {
|
|
48
|
+
const view = stmts.find((s) => s.kind === 'view')
|
|
49
|
+
expect(view).toBeDefined()
|
|
50
|
+
expect(view!.objectName).toBe('active_users')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('parses CREATE INDEX with kind and objectName', () => {
|
|
54
|
+
const index = stmts.find((s) => s.kind === 'index')
|
|
55
|
+
expect(index).toBeDefined()
|
|
56
|
+
expect(index!.objectName).toBe('idx_users_email')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('parses CREATE TYPE (ENUM) with kind and objectName', () => {
|
|
60
|
+
const type = stmts.find((s) => s.kind === 'type')
|
|
61
|
+
expect(type).toBeDefined()
|
|
62
|
+
expect(type!.objectName).toBe('user_role')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('parses CREATE FUNCTION with kind and objectName', () => {
|
|
66
|
+
const fn = stmts.find((s) => s.kind === 'function')
|
|
67
|
+
expect(fn).toBeDefined()
|
|
68
|
+
expect(fn!.objectName).toBe('get_user')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('parses CREATE TRIGGER with kind and objectName', () => {
|
|
72
|
+
const trigger = stmts.find((s) => s.kind === 'trigger')
|
|
73
|
+
expect(trigger).toBeDefined()
|
|
74
|
+
expect(trigger!.objectName).toBe('user_audit')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('returns multiple statements with correct line numbers', () => {
|
|
78
|
+
expect(stmts.length).toBe(6)
|
|
79
|
+
// Each statement should have a different line
|
|
80
|
+
const lines = stmts.map((s) => s.line)
|
|
81
|
+
const uniqueLines = new Set(lines)
|
|
82
|
+
expect(uniqueLines.size).toBe(6)
|
|
83
|
+
// Lines should be in ascending order
|
|
84
|
+
for (let i = 1; i < lines.length; i++) {
|
|
85
|
+
expect(lines[i]).toBeGreaterThan(lines[i - 1])
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('handles RETURNS SETOF gracefully (no crash)', () => {
|
|
90
|
+
const setofSql = `CREATE FUNCTION list_users() RETURNS SETOF users LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT * FROM users; END; $$;`
|
|
91
|
+
// Should not throw
|
|
92
|
+
const result = adapter.parseStatements(setofSql)
|
|
93
|
+
// May return empty array (parse failure filtered) or a result -- either is acceptable
|
|
94
|
+
expect(Array.isArray(result)).toBe(true)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('handles $$ function bodies correctly', () => {
|
|
98
|
+
const dollarSql = `CREATE TABLE t1 (id INT);
|
|
99
|
+
|
|
100
|
+
CREATE FUNCTION foo() RETURNS VOID LANGUAGE plpgsql AS $$
|
|
101
|
+
BEGIN
|
|
102
|
+
INSERT INTO t1 VALUES (1);
|
|
103
|
+
INSERT INTO t1 VALUES (2);
|
|
104
|
+
END;
|
|
105
|
+
$$;
|
|
106
|
+
|
|
107
|
+
CREATE TABLE t2 (id INT);`
|
|
108
|
+
|
|
109
|
+
const result = adapter.parseStatements(dollarSql)
|
|
110
|
+
const tableStmts = result.filter((s) => s.kind === 'table')
|
|
111
|
+
expect(tableStmts.length).toBe(2)
|
|
112
|
+
expect(tableStmts[0].objectName).toBe('t1')
|
|
113
|
+
expect(tableStmts[1].objectName).toBe('t2')
|
|
114
|
+
// t2 should be on a later line than t1
|
|
115
|
+
expect(tableStmts[1].line).toBeGreaterThan(tableStmts[0].line)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as fs from 'node:fs'
|
|
2
|
+
import * as path from 'node:path'
|
|
3
|
+
import { beforeAll, describe, expect, it } from 'vitest'
|
|
4
|
+
import { SqlparserTsAdapter } from '../ast/sqlparser-ts'
|
|
5
|
+
import type { SqlStatement } from '../ast/types'
|
|
6
|
+
import { buildBlocks } from '../blocks'
|
|
7
|
+
import { parse } from '../parser'
|
|
8
|
+
|
|
9
|
+
const fixture = fs.readFileSync(path.join(__dirname, 'tags.sql'), 'utf-8')
|
|
10
|
+
|
|
11
|
+
describe('block resolution from tags.sql', () => {
|
|
12
|
+
let stmts: SqlStatement[]
|
|
13
|
+
|
|
14
|
+
beforeAll(async () => {
|
|
15
|
+
const adapter = new SqlparserTsAdapter()
|
|
16
|
+
await adapter.init()
|
|
17
|
+
stmts = adapter.parseStatements(fixture)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
function getBlocks() {
|
|
21
|
+
const { tags } = parse(fixture)
|
|
22
|
+
const docLines = fixture.split('\n')
|
|
23
|
+
return buildBlocks(tags, fixture, docLines, stmts)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Find the block containing a tag with the given rawArgs value */
|
|
27
|
+
function blockFor(argValue: string) {
|
|
28
|
+
const blocks = getBlocks()
|
|
29
|
+
for (const block of blocks) {
|
|
30
|
+
for (const tag of block.tags) {
|
|
31
|
+
if (tag.rawArgs?.includes(argValue)) {
|
|
32
|
+
return { block, tag, ast: block.ast }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`No block found for arg "${argValue}"`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
it('@for("one") — above CREATE TABLE → table', () => {
|
|
40
|
+
const { ast } = blockFor('"one"')
|
|
41
|
+
expect(ast.target).toBe('table')
|
|
42
|
+
expect(ast.objectName).toBe('one')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('@for("one again") — same line as CREATE TABLE → table', () => {
|
|
46
|
+
const { ast } = blockFor('"one again"')
|
|
47
|
+
expect(ast.target).toBe('table')
|
|
48
|
+
expect(ast.objectName).toBe('one')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('@for("two") — above column two → column', () => {
|
|
52
|
+
const { ast } = blockFor('"two"')
|
|
53
|
+
expect(ast.target).toBe('column')
|
|
54
|
+
expect(ast.columnName).toBe('two')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('@for("two again") — same line as column two → column', () => {
|
|
58
|
+
const { ast } = blockFor('"two again"')
|
|
59
|
+
expect(ast.target).toBe('column')
|
|
60
|
+
expect(ast.columnName).toBe('two')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('@for("three") — same line as column three → column', () => {
|
|
64
|
+
const { ast } = blockFor('"three"')
|
|
65
|
+
expect(ast.target).toBe('column')
|
|
66
|
+
expect(ast.columnName).toBe('three')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('@for("four") — above column four with blank lines → column', () => {
|
|
70
|
+
const { ast } = blockFor('"four"')
|
|
71
|
+
expect(ast.target).toBe('column')
|
|
72
|
+
expect(ast.columnName).toBe('four')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('@for("one yet again") — after all columns → table', () => {
|
|
76
|
+
const { ast } = blockFor('"one yet again"')
|
|
77
|
+
expect(ast.target).toBe('table')
|
|
78
|
+
expect(ast.objectName).toBe('one')
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { SqlAstAdapter } from '../ast/adapter'
|
|
3
|
+
import { compile } from '../compiler/compile'
|
|
4
|
+
import type { NamespacePlugin, ProjectConfig, SqlOutput, TagContext } from '../compiler/types'
|
|
5
|
+
|
|
6
|
+
// ── Task 1: Type extension tests ─────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
describe('ProjectConfig type extensions', () => {
|
|
9
|
+
it('should accept devUrl field', () => {
|
|
10
|
+
const config: ProjectConfig = {
|
|
11
|
+
dialect: 'postgres',
|
|
12
|
+
devUrl: 'postgres://localhost:5432/test',
|
|
13
|
+
include: ['**/*.sql'],
|
|
14
|
+
}
|
|
15
|
+
expect(config.devUrl).toBe('postgres://localhost:5432/test')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('should require dialect', () => {
|
|
19
|
+
const config: ProjectConfig = { dialect: 'postgres' }
|
|
20
|
+
expect(config.devUrl).toBeUndefined()
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('TagContext atlas fields', () => {
|
|
25
|
+
it('should accept atlasTable field', () => {
|
|
26
|
+
const ctx = { atlasTable: { name: 'users', columns: [] } } as Partial<TagContext>
|
|
27
|
+
expect(ctx.atlasTable).toBeDefined()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should accept atlasColumn field', () => {
|
|
31
|
+
const ctx = { atlasColumn: { name: 'email', type: { raw: 'text' } } } as Partial<TagContext>
|
|
32
|
+
expect(ctx.atlasColumn).toBeDefined()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should accept atlasRealm field', () => {
|
|
36
|
+
const ctx = { atlasRealm: { schemas: [{ name: 'public', tables: [] }] } } as Partial<TagContext>
|
|
37
|
+
expect(ctx.atlasRealm).toBeDefined()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should work without atlas fields (Tier 1 backwards compat)', () => {
|
|
41
|
+
const ctx: Partial<TagContext> = {
|
|
42
|
+
target: 'table',
|
|
43
|
+
objectName: 'users',
|
|
44
|
+
tag: { name: null, args: {} },
|
|
45
|
+
namespaceTags: [],
|
|
46
|
+
siblingTags: [],
|
|
47
|
+
fileTags: [],
|
|
48
|
+
astNode: null,
|
|
49
|
+
fileStatements: [],
|
|
50
|
+
config: {},
|
|
51
|
+
filePath: 'test.sql',
|
|
52
|
+
}
|
|
53
|
+
expect(ctx.atlasTable).toBeUndefined()
|
|
54
|
+
expect(ctx.atlasColumn).toBeUndefined()
|
|
55
|
+
expect(ctx.atlasRealm).toBeUndefined()
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// ── Task 2: compile() with Atlas realm ───────────────────────────────
|
|
60
|
+
|
|
61
|
+
// Mock Atlas realm for testing (lowercase schema fields, PascalCase Attr variants)
|
|
62
|
+
const mockRealm = {
|
|
63
|
+
schemas: [
|
|
64
|
+
{
|
|
65
|
+
name: 'public',
|
|
66
|
+
tables: [
|
|
67
|
+
{
|
|
68
|
+
name: 'users',
|
|
69
|
+
columns: [
|
|
70
|
+
{
|
|
71
|
+
name: 'email',
|
|
72
|
+
type: { raw: 'text', null: false },
|
|
73
|
+
attrs: [{ Name: 'pii.mask', Args: '' }],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'id',
|
|
77
|
+
type: { raw: 'bigserial', null: false, T: 'bigserial' },
|
|
78
|
+
attrs: [],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
attrs: [{ Name: 'audit.track', Args: 'on: [delete, update]' }],
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
views: [
|
|
85
|
+
{
|
|
86
|
+
name: 'active_users',
|
|
87
|
+
columns: [{ name: 'email', type: { raw: 'text' } }],
|
|
88
|
+
attrs: [{ Name: 'docs', Args: '' }],
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Minimal mock adapter -- Tier 2 doesn't need real SQL parsing
|
|
96
|
+
const mockAdapter: SqlAstAdapter = {
|
|
97
|
+
init: async () => {},
|
|
98
|
+
parseStatements: () => [],
|
|
99
|
+
parseComments: () => [],
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function createMockPlugin(onTagFn?: (ctx: TagContext) => SqlOutput[] | undefined): NamespacePlugin {
|
|
103
|
+
return {
|
|
104
|
+
name: 'test-plugin',
|
|
105
|
+
tags: {},
|
|
106
|
+
apiVersion: 1,
|
|
107
|
+
onTag: onTagFn ?? (() => undefined),
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
describe('splitTagName', () => {
|
|
112
|
+
// splitTagName is internal, but we test it via compile behavior
|
|
113
|
+
it('should handle dotted tag name via compile (namespace.tag)', () => {
|
|
114
|
+
const capturedCtx: TagContext[] = []
|
|
115
|
+
const plugin = createMockPlugin((ctx) => {
|
|
116
|
+
capturedCtx.push(ctx)
|
|
117
|
+
return undefined
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const plugins = new Map<string, NamespacePlugin>([['audit', plugin]])
|
|
121
|
+
|
|
122
|
+
compile({
|
|
123
|
+
source: '',
|
|
124
|
+
filePath: 'test.sql',
|
|
125
|
+
plugins,
|
|
126
|
+
statements: [],
|
|
127
|
+
adapter: mockAdapter,
|
|
128
|
+
config: { dialect: 'postgres' },
|
|
129
|
+
atlasRealm: mockRealm,
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// Should have invoked audit plugin with tag="track" from "audit.track"
|
|
133
|
+
const auditCall = capturedCtx.find((c) => c.tag.name === 'track')
|
|
134
|
+
expect(auditCall).toBeDefined()
|
|
135
|
+
expect(auditCall!.objectName).toBe('users')
|
|
136
|
+
expect(auditCall!.target).toBe('table')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('should handle simple tag name as $self (namespace only)', () => {
|
|
140
|
+
const capturedCtx: TagContext[] = []
|
|
141
|
+
const plugin = createMockPlugin((ctx) => {
|
|
142
|
+
capturedCtx.push(ctx)
|
|
143
|
+
return undefined
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const plugins = new Map<string, NamespacePlugin>([['docs', plugin]])
|
|
147
|
+
|
|
148
|
+
compile({
|
|
149
|
+
source: '',
|
|
150
|
+
filePath: 'test.sql',
|
|
151
|
+
plugins,
|
|
152
|
+
statements: [],
|
|
153
|
+
adapter: mockAdapter,
|
|
154
|
+
config: { dialect: 'postgres' },
|
|
155
|
+
atlasRealm: mockRealm,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// "docs" with no dot -> namespace=docs, tag=null ($self)
|
|
159
|
+
const docsCall = capturedCtx.find((c) => c.tag.name === null)
|
|
160
|
+
expect(docsCall).toBeDefined()
|
|
161
|
+
expect(docsCall!.objectName).toBe('active_users')
|
|
162
|
+
expect(docsCall!.target).toBe('view')
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
describe('compile() Tier 2 (Atlas realm)', () => {
|
|
167
|
+
it('should invoke plugin onTag with atlasTable in context', () => {
|
|
168
|
+
const capturedCtx: TagContext[] = []
|
|
169
|
+
const plugin = createMockPlugin((ctx) => {
|
|
170
|
+
capturedCtx.push(ctx)
|
|
171
|
+
return undefined
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const plugins = new Map<string, NamespacePlugin>([['audit', plugin]])
|
|
175
|
+
|
|
176
|
+
const _result = compile({
|
|
177
|
+
source: '',
|
|
178
|
+
filePath: 'test.sql',
|
|
179
|
+
plugins,
|
|
180
|
+
statements: [],
|
|
181
|
+
adapter: mockAdapter,
|
|
182
|
+
config: { dialect: 'postgres' },
|
|
183
|
+
atlasRealm: mockRealm,
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
expect(capturedCtx.length).toBeGreaterThan(0)
|
|
187
|
+
const tableCall = capturedCtx.find((c) => c.target === 'table')
|
|
188
|
+
expect(tableCall).toBeDefined()
|
|
189
|
+
expect(tableCall!.atlasTable).toBeDefined()
|
|
190
|
+
expect((tableCall!.atlasTable as any).name).toBe('users')
|
|
191
|
+
expect(tableCall!.atlasRealm).toBe(mockRealm)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('should set correct columnName, columnType, atlasColumn for column tags', () => {
|
|
195
|
+
const capturedCtx: TagContext[] = []
|
|
196
|
+
const plugin = createMockPlugin((ctx) => {
|
|
197
|
+
capturedCtx.push(ctx)
|
|
198
|
+
return undefined
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
const plugins = new Map<string, NamespacePlugin>([['pii', plugin]])
|
|
202
|
+
|
|
203
|
+
compile({
|
|
204
|
+
source: '',
|
|
205
|
+
filePath: 'test.sql',
|
|
206
|
+
plugins,
|
|
207
|
+
statements: [],
|
|
208
|
+
adapter: mockAdapter,
|
|
209
|
+
config: { dialect: 'postgres' },
|
|
210
|
+
atlasRealm: mockRealm,
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
expect(capturedCtx.length).toBeGreaterThan(0)
|
|
214
|
+
const colCall = capturedCtx.find((c) => c.target === 'column')
|
|
215
|
+
expect(colCall).toBeDefined()
|
|
216
|
+
expect(colCall!.columnName).toBe('email')
|
|
217
|
+
expect(colCall!.columnType).toBe('text')
|
|
218
|
+
expect(colCall!.objectName).toBe('users')
|
|
219
|
+
expect(colCall!.atlasColumn).toBeDefined()
|
|
220
|
+
expect((colCall!.atlasColumn as any).name).toBe('email')
|
|
221
|
+
expect(colCall!.atlasTable).toBeDefined()
|
|
222
|
+
expect(colCall!.atlasRealm).toBe(mockRealm)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('should produce CompilerOutput structure', () => {
|
|
226
|
+
const plugin = createMockPlugin((ctx) => {
|
|
227
|
+
return [{ sql: `-- generated for ${ctx.objectName}` }]
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
const plugins = new Map<string, NamespacePlugin>([['audit', plugin]])
|
|
231
|
+
|
|
232
|
+
const result = compile({
|
|
233
|
+
source: '',
|
|
234
|
+
filePath: 'test.sql',
|
|
235
|
+
plugins,
|
|
236
|
+
statements: [],
|
|
237
|
+
adapter: mockAdapter,
|
|
238
|
+
config: { dialect: 'postgres' },
|
|
239
|
+
atlasRealm: mockRealm,
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
expect(result.sourceFile).toBe('test.sql')
|
|
243
|
+
expect(result.sqlOutputs.length).toBeGreaterThan(0)
|
|
244
|
+
expect(result.errors).toHaveLength(0)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('should build fileTags from Atlas data', () => {
|
|
248
|
+
const plugin = createMockPlugin(() => undefined)
|
|
249
|
+
|
|
250
|
+
const plugins = new Map<string, NamespacePlugin>([
|
|
251
|
+
['audit', plugin],
|
|
252
|
+
['pii', plugin],
|
|
253
|
+
['docs', plugin],
|
|
254
|
+
])
|
|
255
|
+
|
|
256
|
+
const result = compile({
|
|
257
|
+
source: '',
|
|
258
|
+
filePath: 'test.sql',
|
|
259
|
+
plugins,
|
|
260
|
+
statements: [],
|
|
261
|
+
adapter: mockAdapter,
|
|
262
|
+
config: { dialect: 'postgres' },
|
|
263
|
+
atlasRealm: mockRealm,
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
expect(result.fileTags.length).toBeGreaterThan(0)
|
|
267
|
+
const usersEntry = result.fileTags.find((ft) => ft.objectName === 'users')
|
|
268
|
+
expect(usersEntry).toBeDefined()
|
|
269
|
+
expect(usersEntry!.target).toBe('table')
|
|
270
|
+
expect(usersEntry!.tags.length).toBeGreaterThan(0)
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('should pass namespace config from SqldocConfig', () => {
|
|
274
|
+
const capturedCtx: TagContext[] = []
|
|
275
|
+
const plugin = createMockPlugin((ctx) => {
|
|
276
|
+
capturedCtx.push(ctx)
|
|
277
|
+
return undefined
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
const plugins = new Map<string, NamespacePlugin>([['audit', plugin]])
|
|
281
|
+
|
|
282
|
+
compile({
|
|
283
|
+
source: '',
|
|
284
|
+
filePath: 'test.sql',
|
|
285
|
+
plugins,
|
|
286
|
+
statements: [],
|
|
287
|
+
adapter: mockAdapter,
|
|
288
|
+
config: { dialect: 'postgres', namespaces: { audit: { destination: 'audit_log' } } },
|
|
289
|
+
atlasRealm: mockRealm,
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
const auditCall = capturedCtx.find((c) => c.tag.name === 'track')
|
|
293
|
+
expect(auditCall).toBeDefined()
|
|
294
|
+
expect((auditCall!.config as any).destination).toBe('audit_log')
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('should handle errors from plugin onTag gracefully', () => {
|
|
298
|
+
const plugin: NamespacePlugin = {
|
|
299
|
+
name: 'broken',
|
|
300
|
+
tags: {},
|
|
301
|
+
apiVersion: 1,
|
|
302
|
+
onTag: () => {
|
|
303
|
+
throw new Error('plugin crashed')
|
|
304
|
+
},
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const plugins = new Map<string, NamespacePlugin>([['audit', plugin]])
|
|
308
|
+
|
|
309
|
+
const result = compile({
|
|
310
|
+
source: '',
|
|
311
|
+
filePath: 'test.sql',
|
|
312
|
+
plugins,
|
|
313
|
+
statements: [],
|
|
314
|
+
adapter: mockAdapter,
|
|
315
|
+
config: { dialect: 'postgres' },
|
|
316
|
+
atlasRealm: mockRealm,
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
expect(result.errors.length).toBeGreaterThan(0)
|
|
320
|
+
expect(result.errors[0].message).toBe('plugin crashed')
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it('should handle Atlas realm with views', () => {
|
|
324
|
+
const capturedCtx: TagContext[] = []
|
|
325
|
+
const plugin = createMockPlugin((ctx) => {
|
|
326
|
+
capturedCtx.push(ctx)
|
|
327
|
+
return undefined
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
const plugins = new Map<string, NamespacePlugin>([['docs', plugin]])
|
|
331
|
+
|
|
332
|
+
compile({
|
|
333
|
+
source: '',
|
|
334
|
+
filePath: 'test.sql',
|
|
335
|
+
plugins,
|
|
336
|
+
statements: [],
|
|
337
|
+
adapter: mockAdapter,
|
|
338
|
+
config: { dialect: 'postgres' },
|
|
339
|
+
atlasRealm: mockRealm,
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
const viewCall = capturedCtx.find((c) => c.target === 'view')
|
|
343
|
+
expect(viewCall).toBeDefined()
|
|
344
|
+
expect(viewCall!.objectName).toBe('active_users')
|
|
345
|
+
expect(viewCall!.atlasTable).toBeDefined()
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it('should parse atlas tag args', () => {
|
|
349
|
+
const capturedCtx: TagContext[] = []
|
|
350
|
+
const plugin = createMockPlugin((ctx) => {
|
|
351
|
+
capturedCtx.push(ctx)
|
|
352
|
+
return undefined
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
const plugins = new Map<string, NamespacePlugin>([['audit', plugin]])
|
|
356
|
+
|
|
357
|
+
compile({
|
|
358
|
+
source: '',
|
|
359
|
+
filePath: 'test.sql',
|
|
360
|
+
plugins,
|
|
361
|
+
statements: [],
|
|
362
|
+
adapter: mockAdapter,
|
|
363
|
+
config: { dialect: 'postgres' },
|
|
364
|
+
atlasRealm: mockRealm,
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
const auditCall = capturedCtx.find((c) => c.tag.name === 'track')
|
|
368
|
+
expect(auditCall).toBeDefined()
|
|
369
|
+
// Args should be parsed from the Atlas tag args string
|
|
370
|
+
expect(auditCall!.tag.args).toBeDefined()
|
|
371
|
+
})
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
describe('compile() Tier 1 (no Atlas realm)', () => {
|
|
375
|
+
it('should still use block resolution when no atlasRealm provided', () => {
|
|
376
|
+
// Source with tags that block resolution can handle
|
|
377
|
+
const source = `-- @audit.track
|
|
378
|
+
CREATE TABLE users (id bigserial PRIMARY KEY);`
|
|
379
|
+
|
|
380
|
+
const plugin = createMockPlugin((ctx) => {
|
|
381
|
+
return [{ sql: `-- audited: ${ctx.objectName}` }]
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
const plugins = new Map<string, NamespacePlugin>([['audit', plugin]])
|
|
385
|
+
|
|
386
|
+
// No atlasRealm -- Tier 1 mode
|
|
387
|
+
const result = compile({
|
|
388
|
+
source,
|
|
389
|
+
filePath: 'test.sql',
|
|
390
|
+
plugins,
|
|
391
|
+
statements: [
|
|
392
|
+
{
|
|
393
|
+
objectName: 'users',
|
|
394
|
+
kind: 'table',
|
|
395
|
+
line: 2,
|
|
396
|
+
raw: null,
|
|
397
|
+
columns: [{ name: 'id', dataType: 'bigserial', line: 2, raw: null }],
|
|
398
|
+
},
|
|
399
|
+
],
|
|
400
|
+
adapter: mockAdapter,
|
|
401
|
+
config: { dialect: 'postgres' },
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
// Should still produce output via block resolution
|
|
405
|
+
expect(result.sqlOutputs.length).toBeGreaterThan(0)
|
|
406
|
+
})
|
|
407
|
+
})
|