Skip to content

Commit 30373b2

Browse files
committed
feat: add lib files for @supabase/postgres-meta
1 parent a71ea30 commit 30373b2

15 files changed

+1608
-0
lines changed

src/lib/PostgresMeta.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { ClientConfig } from 'pg'
2+
import PostgresMetaColumn from './PostgresMetaColumn'
3+
import PostgresMetaConfig from './PostgresMetaConfig'
4+
import PostgresMetaExtension from './PostgresMetaExtension'
5+
import PostgresMetaFunction from './PostgresMetaFunction'
6+
import PostgresMetaPolicy from './PostgresMetaPolicy'
7+
import PostgresMetaPublication from './PostgresMetaPublication'
8+
import PostgresMetaRole from './PostgresMetaRole'
9+
import PostgresMetaSchema from './PostgresMetaSchema'
10+
import PostgresMetaTable from './PostgresMetaTable'
11+
import PostgresMetaType from './PostgresMetaType'
12+
import PostgresMetaVersion from './PostgresMetaVersion'
13+
import { init } from './db'
14+
15+
export default class PostgresMeta {
16+
query: Function
17+
column: PostgresMetaColumn
18+
config: PostgresMetaConfig
19+
extension: PostgresMetaExtension
20+
function: PostgresMetaFunction
21+
policy: PostgresMetaPolicy
22+
publication: PostgresMetaPublication
23+
role: PostgresMetaRole
24+
schema: PostgresMetaSchema
25+
table: PostgresMetaTable
26+
type: PostgresMetaType
27+
version: PostgresMetaVersion
28+
29+
constructor(config: ClientConfig) {
30+
this.query = init(config)
31+
this.column = new PostgresMetaColumn(this.query)
32+
this.config = new PostgresMetaConfig(this.query)
33+
this.extension = new PostgresMetaExtension(this.query)
34+
this.function = new PostgresMetaFunction(this.query)
35+
this.policy = new PostgresMetaPolicy(this.query)
36+
this.publication = new PostgresMetaPublication(this.query)
37+
this.role = new PostgresMetaRole(this.query)
38+
this.schema = new PostgresMetaSchema(this.query)
39+
this.table = new PostgresMetaTable(this.query)
40+
this.type = new PostgresMetaType(this.query)
41+
this.version = new PostgresMetaVersion(this.query)
42+
}
43+
}

src/lib/PostgresMetaColumn.ts

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
import { ident, literal } from 'pg-format'
2+
import PostgresMetaTable from './PostgresMetaTable'
3+
import { DEFAULT_SYSTEM_SCHEMAS } from './constants'
4+
import { columnsSql } from './sql'
5+
import { PostgresMetaResult, PostgresColumn } from './types'
6+
7+
export default class PostgresMetaColumn {
8+
query: Function
9+
metaTable: PostgresMetaTable
10+
11+
constructor(query: Function) {
12+
this.query = query
13+
this.metaTable = new PostgresMetaTable(query)
14+
}
15+
16+
async list({ includeSystemSchemas = false } = {}): Promise<PostgresMetaResult<PostgresColumn[]>> {
17+
const sql = includeSystemSchemas
18+
? columnsSql
19+
: `${columnsSql} AND NOT (nc.nspname IN (${DEFAULT_SYSTEM_SCHEMAS.map(literal).join(',')}));`
20+
return await this.query(sql)
21+
}
22+
23+
async retrieve({ id }: { id: string }): Promise<PostgresMetaResult<PostgresColumn>>
24+
async retrieve({
25+
name,
26+
table,
27+
schema,
28+
}: {
29+
name: string
30+
table: string
31+
schema: string
32+
}): Promise<PostgresMetaResult<PostgresColumn>>
33+
async retrieve({
34+
id,
35+
name,
36+
table,
37+
schema = 'public',
38+
}: {
39+
id?: string
40+
name?: string
41+
table?: string
42+
schema?: string
43+
}): Promise<PostgresMetaResult<PostgresColumn>> {
44+
if (id) {
45+
const regexp = /^(\d+)\.(\d+)$/
46+
if (!regexp.test(id)) {
47+
return { data: null, error: { message: 'Invalid format for column ID' } }
48+
}
49+
const matches = id.match(regexp) as RegExpMatchArray
50+
const [tableId, ordinalPos] = matches.slice(1).map(Number)
51+
const sql = `${columnsSql} AND c.oid = ${tableId} AND a.attnum = ${ordinalPos};`
52+
const { data, error } = await this.query(sql)
53+
if (error) {
54+
return { data, error }
55+
} else if (data.length === 0) {
56+
return { data: null, error: { message: `Cannot find a column with ID ${id}` } }
57+
} else {
58+
return { data: data[0], error }
59+
}
60+
} else if (name && table) {
61+
const sql = `${columnsSql} AND a.attname = ${literal(name)} AND c.relname = ${literal(
62+
table
63+
)} AND nc.nspname = ${literal(schema)};`
64+
const { data, error } = await this.query(sql)
65+
if (error) {
66+
return { data, error }
67+
} else if (data.length === 0) {
68+
return {
69+
data: null,
70+
error: { message: `Cannot find a column named ${name} in table ${schema}.${table}` },
71+
}
72+
} else {
73+
return { data: data[0], error }
74+
}
75+
} else {
76+
return { data: null, error: { message: 'Invalid parameters on column retrieve' } }
77+
}
78+
}
79+
80+
async create({
81+
table_id,
82+
name,
83+
type,
84+
default_value,
85+
default_value_format = 'literal',
86+
is_identity = false,
87+
identity_generation = 'BY DEFAULT',
88+
is_nullable = true,
89+
is_primary_key = false,
90+
is_unique = false,
91+
comment,
92+
}: {
93+
table_id: number
94+
name: string
95+
type: string
96+
default_value?: any
97+
default_value_format?: 'expression' | 'literal'
98+
is_identity?: boolean
99+
identity_generation?: 'BY DEFAULT' | 'ALWAYS'
100+
is_nullable?: boolean
101+
is_primary_key?: boolean
102+
is_unique?: boolean
103+
comment?: string
104+
}): Promise<PostgresMetaResult<PostgresColumn>> {
105+
const { data, error } = await this.metaTable.retrieve({ id: table_id })
106+
if (error) {
107+
return { data: null, error }
108+
}
109+
const { name: table, schema } = data!
110+
111+
let defaultValueClause: string
112+
if (default_value === undefined) {
113+
defaultValueClause = ''
114+
} else if (default_value_format === 'expression') {
115+
defaultValueClause = `DEFAULT ${default_value}`
116+
} else {
117+
defaultValueClause = `DEFAULT ${literal(default_value)}`
118+
}
119+
const isIdentityClause = is_identity ? `GENERATED ${identity_generation} AS IDENTITY` : ''
120+
const isNullableClause = is_nullable ? 'NULL' : 'NOT NULL'
121+
const isPrimaryKeyClause = is_primary_key ? 'PRIMARY KEY' : ''
122+
const isUniqueClause = is_unique ? 'UNIQUE' : ''
123+
const commentSql =
124+
comment === undefined
125+
? ''
126+
: `COMMENT ON COLUMN ${ident(schema)}.${ident(table)}.${ident(name)} IS ${literal(comment)}`
127+
128+
const sql = `
129+
BEGIN;
130+
ALTER TABLE ${ident(schema)}.${ident(table)} ADD COLUMN ${ident(name)} ${type}
131+
${defaultValueClause}
132+
${isIdentityClause}
133+
${isNullableClause}
134+
${isPrimaryKeyClause}
135+
${isUniqueClause};
136+
${commentSql};
137+
COMMIT;`
138+
{
139+
const { error } = await this.query(sql)
140+
if (error) {
141+
return { data: null, error }
142+
}
143+
}
144+
return await this.retrieve({ name, table, schema })
145+
}
146+
147+
async update(
148+
id: string,
149+
{
150+
name,
151+
type,
152+
drop_default = false,
153+
default_value,
154+
default_value_format = 'literal',
155+
is_identity,
156+
identity_generation,
157+
is_nullable,
158+
comment,
159+
}: {
160+
name?: string
161+
type?: string
162+
drop_default?: boolean
163+
default_value?: any
164+
default_value_format?: 'expression' | 'literal'
165+
is_identity?: boolean
166+
identity_generation?: 'BY DEFAULT' | 'ALWAYS'
167+
is_nullable?: boolean
168+
comment?: string
169+
}
170+
): Promise<PostgresMetaResult<PostgresColumn>> {
171+
const { data: old, error } = await this.retrieve({ id })
172+
if (error) {
173+
return { data: null, error }
174+
}
175+
176+
const nameSql =
177+
name === undefined || name === old!.name
178+
? ''
179+
: `ALTER TABLE ${old!.schema}.${old!.table} RENAME COLUMN ${old!.name} TO ${name};`
180+
// We use USING to allow implicit conversion of incompatible types (e.g. int4 -> text).
181+
const typeSql =
182+
type === undefined
183+
? ''
184+
: `ALTER TABLE ${old!.schema}.${old!.table} ALTER COLUMN ${
185+
old!.name
186+
} SET DATA TYPE ${type} USING ${old!.name}::${type};`
187+
188+
let defaultValueSql: string
189+
if (drop_default) {
190+
defaultValueSql = `ALTER TABLE ${old!.schema}.${old!.table} ALTER COLUMN ${
191+
old!.name
192+
} DROP DEFAULT;`
193+
} else if (default_value === undefined) {
194+
defaultValueSql = ''
195+
} else {
196+
const defaultValue =
197+
default_value_format === 'expression' ? default_value : literal(default_value)
198+
defaultValueSql = `ALTER TABLE ${old!.schema}.${old!.table} ALTER COLUMN ${
199+
old!.name
200+
} SET DEFAULT ${defaultValue};`
201+
}
202+
// What identitySql does vary depending on the old and new values of
203+
// is_identity and identity_generation.
204+
//
205+
// | is_identity: old \ new | undefined | true | false |
206+
// |------------------------+--------------------+--------------------+----------------|
207+
// | true | maybe set identity | maybe set identity | drop if exists |
208+
// |------------------------+--------------------+--------------------+----------------|
209+
// | false | - | add identity | drop if exists |
210+
let identitySql = `ALTER TABLE ${ident(old!.schema)}.${ident(old!.table)} ALTER COLUMN ${ident(
211+
old!.name
212+
)};`
213+
if (is_identity === false) {
214+
identitySql += 'DROP IDENTITY IF EXISTS;'
215+
} else if (old!.is_identity === true) {
216+
if (identity_generation === undefined) {
217+
identitySql = ''
218+
} else {
219+
identitySql += `SET GENERATED ${identity_generation};`
220+
}
221+
} else if (is_identity === undefined) {
222+
identitySql = ''
223+
} else {
224+
identitySql += `ADD GENERATED ${identity_generation} AS IDENTITY;`
225+
}
226+
let isNullableSql: string
227+
if (is_nullable === undefined) {
228+
isNullableSql = ''
229+
} else {
230+
isNullableSql = is_nullable
231+
? `ALTER TABLE ${old!.schema}.${old!.table} ALTER COLUMN ${old!.name} DROP NOT NULL;`
232+
: `ALTER TABLE ${old!.schema}.${old!.table} ALTER COLUMN ${old!.name} SET NOT NULL;`
233+
}
234+
const commentSql =
235+
comment === undefined
236+
? ''
237+
: `COMMENT ON COLUMN ${old!.schema}.${old!.table}.${old!.name} IS ${comment};`
238+
239+
// nameSql must be last.
240+
// defaultValueSql must be after typeSql.
241+
// TODO: Can't set default if column is previously identity even if is_identity: false.
242+
// Must do two separate PATCHes (once to drop identity and another to set default).
243+
const sql = `
244+
BEGIN;
245+
${isNullableSql}
246+
${typeSql}
247+
${defaultValueSql}
248+
${identitySql}
249+
${commentSql}
250+
${nameSql}
251+
COMMIT;`
252+
{
253+
const { error } = await this.query(sql)
254+
if (error) {
255+
return { data: null, error }
256+
}
257+
}
258+
return await this.retrieve({ id })
259+
}
260+
261+
async remove(id: string): Promise<PostgresMetaResult<PostgresColumn>> {
262+
const { data: column, error } = await this.retrieve({ id })
263+
if (error) {
264+
return { data: null, error }
265+
}
266+
const sql = `ALTER TABLE ${column!.schema}.${column!.table} DROP COLUMN ${column!.name};`
267+
{
268+
const { error } = await this.query(sql)
269+
if (error) {
270+
return { data: null, error }
271+
}
272+
}
273+
return { data: column!, error: null }
274+
}
275+
}

src/lib/PostgresMetaConfig.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { configSql } from './sql'
2+
import { PostgresMetaResult, PostgresConfig } from './types'
3+
4+
export default class PostgresMetaConfig {
5+
query: Function
6+
7+
constructor(query: Function) {
8+
this.query = query
9+
}
10+
11+
async list(): Promise<PostgresMetaResult<PostgresConfig[]>> {
12+
return await this.query(configSql)
13+
}
14+
}

0 commit comments

Comments
 (0)