@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 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