@sqldoc/ns-postgraphile 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 +30 -0
- package/src/__tests__/postgraphile.test.ts +187 -0
- package/src/index.ts +166 -0
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "module",
|
|
3
|
+
"name": "@sqldoc/ns-postgraphile",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "PostGraphile smart comments namespace for sqldoc -- generates COMMENT ON statements with @tag syntax",
|
|
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
|
+
"peerDependencies": {
|
|
20
|
+
"@sqldoc/core": "0.0.1"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"typescript": "^5.9.3",
|
|
24
|
+
"vitest": "^4.1.0",
|
|
25
|
+
"@sqldoc/core": "0.0.1"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "vitest run"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { TagContext } from '@sqldoc/core'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import plugin from '../index'
|
|
4
|
+
|
|
5
|
+
function makeCtx(overrides: Partial<TagContext> = {}): TagContext {
|
|
6
|
+
return {
|
|
7
|
+
target: 'table',
|
|
8
|
+
objectName: 'users',
|
|
9
|
+
tag: { name: 'omit', args: {} },
|
|
10
|
+
namespaceTags: [{ tag: 'omit', args: {} }],
|
|
11
|
+
siblingTags: [],
|
|
12
|
+
fileTags: [],
|
|
13
|
+
astNode: null,
|
|
14
|
+
fileStatements: [],
|
|
15
|
+
config: {},
|
|
16
|
+
filePath: 'test.sql',
|
|
17
|
+
...overrides,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('ns-postgraphile plugin', () => {
|
|
22
|
+
it('exports apiVersion === 1', () => {
|
|
23
|
+
expect(plugin.apiVersion).toBe(1)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('exports name === "pg"', () => {
|
|
27
|
+
expect(plugin.name).toBe('pg')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('has all expected tag entries', () => {
|
|
31
|
+
expect(plugin.tags).toHaveProperty('omit')
|
|
32
|
+
expect(plugin.tags).toHaveProperty(['omit.operations'])
|
|
33
|
+
expect(plugin.tags).toHaveProperty('name')
|
|
34
|
+
expect(plugin.tags).toHaveProperty('deprecated')
|
|
35
|
+
expect(plugin.tags).toHaveProperty('simpleCollections')
|
|
36
|
+
expect(plugin.tags).toHaveProperty('behavior')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('onTag', () => {
|
|
40
|
+
it('@pg.omit on a table', () => {
|
|
41
|
+
const ctx = makeCtx()
|
|
42
|
+
const result = plugin.onTag!(ctx) as any
|
|
43
|
+
expect(result.sql).toEqual([{ sql: `COMMENT ON TABLE "users" IS E'@omit';` }])
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('@pg.omit on a column', () => {
|
|
47
|
+
const ctx = makeCtx({
|
|
48
|
+
target: 'column',
|
|
49
|
+
objectName: 'users',
|
|
50
|
+
columnName: 'secret',
|
|
51
|
+
tag: { name: 'omit', args: {} },
|
|
52
|
+
namespaceTags: [{ tag: 'omit', args: {} }],
|
|
53
|
+
})
|
|
54
|
+
const result = plugin.onTag!(ctx) as any
|
|
55
|
+
expect(result.sql).toEqual([{ sql: `COMMENT ON COLUMN "users"."secret" IS E'@omit';` }])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('@pg.omit on a view', () => {
|
|
59
|
+
const ctx = makeCtx({
|
|
60
|
+
target: 'view',
|
|
61
|
+
objectName: 'active_users',
|
|
62
|
+
tag: { name: 'omit', args: {} },
|
|
63
|
+
namespaceTags: [{ tag: 'omit', args: {} }],
|
|
64
|
+
})
|
|
65
|
+
const result = plugin.onTag!(ctx) as any
|
|
66
|
+
expect(result.sql).toEqual([{ sql: `COMMENT ON VIEW "active_users" IS E'@omit';` }])
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('@pg.omit on a function', () => {
|
|
70
|
+
const ctx = makeCtx({
|
|
71
|
+
target: 'function',
|
|
72
|
+
objectName: 'my_func',
|
|
73
|
+
tag: { name: 'omit', args: {} },
|
|
74
|
+
namespaceTags: [{ tag: 'omit', args: {} }],
|
|
75
|
+
})
|
|
76
|
+
const result = plugin.onTag!(ctx) as any
|
|
77
|
+
expect(result.sql).toEqual([{ sql: `COMMENT ON FUNCTION "my_func" IS E'@omit';` }])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('@pg.omit.operations with specific ops', () => {
|
|
81
|
+
const ctx = makeCtx({
|
|
82
|
+
tag: { name: 'omit.operations', args: { ops: ['create', 'update'] } },
|
|
83
|
+
namespaceTags: [{ tag: 'omit.operations', args: { ops: ['create', 'update'] } }],
|
|
84
|
+
})
|
|
85
|
+
const result = plugin.onTag!(ctx) as any
|
|
86
|
+
expect(result.sql).toEqual([{ sql: `COMMENT ON TABLE "users" IS E'@omit create,update';` }])
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('@pg.name renames in GraphQL', () => {
|
|
90
|
+
const ctx = makeCtx({
|
|
91
|
+
tag: { name: 'name', args: ['Person'] },
|
|
92
|
+
namespaceTags: [{ tag: 'name', args: ['Person'] }],
|
|
93
|
+
})
|
|
94
|
+
const result = plugin.onTag!(ctx) as any
|
|
95
|
+
expect(result.sql).toEqual([{ sql: `COMMENT ON TABLE "users" IS E'@name Person';` }])
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('@pg.name on a type', () => {
|
|
99
|
+
const ctx = makeCtx({
|
|
100
|
+
target: 'type',
|
|
101
|
+
objectName: 'user_role',
|
|
102
|
+
tag: { name: 'name', args: ['UserRole'] },
|
|
103
|
+
namespaceTags: [{ tag: 'name', args: ['UserRole'] }],
|
|
104
|
+
})
|
|
105
|
+
const result = plugin.onTag!(ctx) as any
|
|
106
|
+
expect(result.sql).toEqual([{ sql: `COMMENT ON TYPE "user_role" IS E'@name UserRole';` }])
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('@pg.deprecated with reason', () => {
|
|
110
|
+
const ctx = makeCtx({
|
|
111
|
+
tag: { name: 'deprecated', args: ['Use accounts instead'] },
|
|
112
|
+
namespaceTags: [{ tag: 'deprecated', args: ['Use accounts instead'] }],
|
|
113
|
+
})
|
|
114
|
+
const result = plugin.onTag!(ctx) as any
|
|
115
|
+
expect(result.sql).toEqual([{ sql: `COMMENT ON TABLE "users" IS E'@deprecated Use accounts instead';` }])
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('@pg.deprecated with default reason', () => {
|
|
119
|
+
const ctx = makeCtx({
|
|
120
|
+
tag: { name: 'deprecated', args: [] },
|
|
121
|
+
namespaceTags: [{ tag: 'deprecated', args: [] }],
|
|
122
|
+
})
|
|
123
|
+
const result = plugin.onTag!(ctx) as any
|
|
124
|
+
expect(result.sql).toEqual([{ sql: `COMMENT ON TABLE "users" IS E'@deprecated Deprecated';` }])
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('@pg.simpleCollections on a table', () => {
|
|
128
|
+
const ctx = makeCtx({
|
|
129
|
+
tag: { name: 'simpleCollections', args: {} },
|
|
130
|
+
namespaceTags: [{ tag: 'simpleCollections', args: {} }],
|
|
131
|
+
})
|
|
132
|
+
const result = plugin.onTag!(ctx) as any
|
|
133
|
+
expect(result.sql).toEqual([{ sql: `COMMENT ON TABLE "users" IS E'@simpleCollections only';` }])
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('@pg.behavior with custom string', () => {
|
|
137
|
+
const ctx = makeCtx({
|
|
138
|
+
tag: { name: 'behavior', args: ['-query:resource:list'] },
|
|
139
|
+
namespaceTags: [{ tag: 'behavior', args: ['-query:resource:list'] }],
|
|
140
|
+
})
|
|
141
|
+
const result = plugin.onTag!(ctx) as any
|
|
142
|
+
expect(result.sql).toEqual([{ sql: `COMMENT ON TABLE "users" IS E'@behavior -query:resource:list';` }])
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('multiple tags combined into one COMMENT ON', () => {
|
|
146
|
+
it('combines @pg.omit.operations and @pg.name into one statement', () => {
|
|
147
|
+
const namespaceTags = [
|
|
148
|
+
{ tag: 'omit.operations', args: { ops: ['create', 'delete'] } },
|
|
149
|
+
{ tag: 'name', args: ['Person'] },
|
|
150
|
+
]
|
|
151
|
+
// First tag generates the combined SQL
|
|
152
|
+
const ctx1 = makeCtx({
|
|
153
|
+
tag: { name: 'omit.operations', args: { ops: ['create', 'delete'] } },
|
|
154
|
+
namespaceTags,
|
|
155
|
+
})
|
|
156
|
+
const result1 = plugin.onTag!(ctx1) as any
|
|
157
|
+
expect(result1.sql).toEqual([{ sql: `COMMENT ON TABLE "users" IS E'@omit create,delete\\n@name Person';` }])
|
|
158
|
+
|
|
159
|
+
// Second tag is skipped (returns undefined)
|
|
160
|
+
const ctx2 = makeCtx({
|
|
161
|
+
tag: { name: 'name', args: ['Person'] },
|
|
162
|
+
namespaceTags,
|
|
163
|
+
})
|
|
164
|
+
const result2 = plugin.onTag!(ctx2)
|
|
165
|
+
expect(result2).toBeUndefined()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('combines three tags: omit + deprecated + simpleCollections', () => {
|
|
169
|
+
const namespaceTags = [
|
|
170
|
+
{ tag: 'omit', args: {} },
|
|
171
|
+
{ tag: 'deprecated', args: ['Will be removed in v2'] },
|
|
172
|
+
{ tag: 'simpleCollections', args: {} },
|
|
173
|
+
]
|
|
174
|
+
const ctx = makeCtx({
|
|
175
|
+
tag: { name: 'omit', args: {} },
|
|
176
|
+
namespaceTags,
|
|
177
|
+
})
|
|
178
|
+
const result = plugin.onTag!(ctx) as any
|
|
179
|
+
expect(result.sql).toEqual([
|
|
180
|
+
{
|
|
181
|
+
sql: `COMMENT ON TABLE "users" IS E'@omit\\n@deprecated Will be removed in v2\\n@simpleCollections only';`,
|
|
182
|
+
},
|
|
183
|
+
])
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { NamespacePlugin, SqlOutput, TagContext, TagOutput } from '@sqldoc/core'
|
|
2
|
+
|
|
3
|
+
function commentTarget(target: string, objectName: string, columnName?: string): string {
|
|
4
|
+
switch (target) {
|
|
5
|
+
case 'column':
|
|
6
|
+
return `COLUMN "${objectName}"."${columnName}"`
|
|
7
|
+
case 'table':
|
|
8
|
+
return `TABLE "${objectName}"`
|
|
9
|
+
case 'view':
|
|
10
|
+
return `VIEW "${objectName}"`
|
|
11
|
+
case 'function':
|
|
12
|
+
return `FUNCTION "${objectName}"`
|
|
13
|
+
case 'type':
|
|
14
|
+
return `TYPE "${objectName}"`
|
|
15
|
+
default:
|
|
16
|
+
return `TABLE "${objectName}"`
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function tagLine(entry: { tag: string | null; args: Record<string, unknown> | unknown[] }): string {
|
|
21
|
+
const name = entry.tag
|
|
22
|
+
const args = entry.args
|
|
23
|
+
|
|
24
|
+
switch (name) {
|
|
25
|
+
case '$self':
|
|
26
|
+
case null:
|
|
27
|
+
return ''
|
|
28
|
+
case 'omit':
|
|
29
|
+
return '@omit'
|
|
30
|
+
case 'omit.operations': {
|
|
31
|
+
const namedArgs = args as Record<string, unknown>
|
|
32
|
+
const ops = namedArgs.ops as string[]
|
|
33
|
+
return `@omit ${ops.join(',')}`
|
|
34
|
+
}
|
|
35
|
+
case 'name': {
|
|
36
|
+
const positional = args as unknown[]
|
|
37
|
+
return `@name ${positional[0]}`
|
|
38
|
+
}
|
|
39
|
+
case 'deprecated': {
|
|
40
|
+
const positional = args as unknown[]
|
|
41
|
+
const reason = positional.length > 0 ? positional[0] : 'Deprecated'
|
|
42
|
+
return `@deprecated ${reason}`
|
|
43
|
+
}
|
|
44
|
+
case 'simpleCollections':
|
|
45
|
+
return '@simpleCollections only'
|
|
46
|
+
case 'behavior': {
|
|
47
|
+
const positional = args as unknown[]
|
|
48
|
+
return `@behavior ${positional[0]}`
|
|
49
|
+
}
|
|
50
|
+
default:
|
|
51
|
+
return ''
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Build a human-readable GraphQL doc label from all pg tags on this object */
|
|
56
|
+
function buildGraphqlLabel(
|
|
57
|
+
namespaceTags: Array<{ tag: string | null; args: Record<string, unknown> | unknown[] }>,
|
|
58
|
+
): string | undefined {
|
|
59
|
+
const parts: string[] = []
|
|
60
|
+
for (const entry of namespaceTags) {
|
|
61
|
+
switch (entry.tag) {
|
|
62
|
+
case 'omit':
|
|
63
|
+
parts.push('Omitted')
|
|
64
|
+
break
|
|
65
|
+
case 'omit.operations': {
|
|
66
|
+
const ops = (entry.args as Record<string, unknown>).ops as string[]
|
|
67
|
+
parts.push(`Omit: ${ops.join(', ')}`)
|
|
68
|
+
break
|
|
69
|
+
}
|
|
70
|
+
case 'name': {
|
|
71
|
+
const name = (entry.args as unknown[])[0]
|
|
72
|
+
parts.push(`→ ${name}`)
|
|
73
|
+
break
|
|
74
|
+
}
|
|
75
|
+
case 'simpleCollections':
|
|
76
|
+
parts.push('Simple collections')
|
|
77
|
+
break
|
|
78
|
+
case 'behavior': {
|
|
79
|
+
const b = (entry.args as unknown[])[0]
|
|
80
|
+
parts.push(`Behavior: ${b}`)
|
|
81
|
+
break
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return parts.length > 0 ? parts.join(', ') : undefined
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const plugin: NamespacePlugin = {
|
|
89
|
+
apiVersion: 1,
|
|
90
|
+
name: 'pg',
|
|
91
|
+
tags: {
|
|
92
|
+
omit: {
|
|
93
|
+
description: 'Omit this object from the PostGraphile GraphQL schema',
|
|
94
|
+
targets: ['table', 'column', 'view', 'function'],
|
|
95
|
+
args: {},
|
|
96
|
+
},
|
|
97
|
+
'omit.operations': {
|
|
98
|
+
description: 'Omit specific operations from the PostGraphile GraphQL schema',
|
|
99
|
+
targets: ['table', 'column', 'view', 'function'],
|
|
100
|
+
args: {
|
|
101
|
+
ops: {
|
|
102
|
+
type: 'array',
|
|
103
|
+
items: { type: 'enum', values: ['create', 'read', 'update', 'delete', 'all', 'many'] },
|
|
104
|
+
required: true,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
name: {
|
|
109
|
+
description: 'Rename this object in the PostGraphile GraphQL schema',
|
|
110
|
+
targets: ['table', 'column', 'view', 'function', 'type'],
|
|
111
|
+
args: [{ type: 'string' }],
|
|
112
|
+
},
|
|
113
|
+
deprecated: {
|
|
114
|
+
description: 'Mark this object as deprecated in the PostGraphile GraphQL schema',
|
|
115
|
+
targets: ['table', 'column', 'view', 'function', 'type'],
|
|
116
|
+
args: [{ type: 'string' }],
|
|
117
|
+
},
|
|
118
|
+
simpleCollections: {
|
|
119
|
+
description: 'Enable simple collections on this table or view in PostGraphile',
|
|
120
|
+
targets: ['table', 'view'],
|
|
121
|
+
args: {},
|
|
122
|
+
},
|
|
123
|
+
behavior: {
|
|
124
|
+
description: 'Set a custom behavior string (PostGraphile V5)',
|
|
125
|
+
targets: ['table', 'column', 'view', 'function', 'type'],
|
|
126
|
+
args: [{ type: 'string' }],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
onTag(ctx: TagContext): TagOutput | undefined {
|
|
131
|
+
const { tag, objectName, columnName, target, namespaceTags } = ctx
|
|
132
|
+
|
|
133
|
+
// Only generate on the FIRST pg tag — combines all into one COMMENT ON
|
|
134
|
+
const firstTag = namespaceTags[0]
|
|
135
|
+
if (firstTag.tag !== tag.name || JSON.stringify(firstTag.args) !== JSON.stringify(tag.args)) {
|
|
136
|
+
return undefined
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const lines: string[] = []
|
|
140
|
+
for (const entry of namespaceTags) {
|
|
141
|
+
const line = tagLine(entry)
|
|
142
|
+
if (line) lines.push(line)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (lines.length === 0) return undefined
|
|
146
|
+
|
|
147
|
+
const commentBody = lines.join('\\n')
|
|
148
|
+
const targetStr = commentTarget(target, objectName, columnName)
|
|
149
|
+
const sql: SqlOutput[] = [{ sql: `COMMENT ON ${targetStr} IS E'${commentBody}';` }]
|
|
150
|
+
|
|
151
|
+
// Build docs column entry
|
|
152
|
+
const label = buildGraphqlLabel(namespaceTags)
|
|
153
|
+
const docTarget = target === 'column' ? { object: objectName, column: columnName } : { object: objectName }
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
sql,
|
|
157
|
+
docs: label
|
|
158
|
+
? {
|
|
159
|
+
columns: [{ header: 'GraphQL', ...docTarget, value: label }],
|
|
160
|
+
}
|
|
161
|
+
: undefined,
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export default plugin
|