@owlmeans/postgres-resource 0.1.18-rc.3 → 0.1.18-rc.31
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/README.md +185 -101
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/postgres-resource/SKILL.md +82 -18
- package/build/consts.d.ts +8 -1
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +8 -1
- package/build/consts.js.map +1 -1
- package/build/declarations.js +5 -5
- package/build/resource.d.ts +2 -2
- package/build/resource.d.ts.map +1 -1
- package/build/resource.js +95 -108
- package/build/resource.js.map +1 -1
- package/build/types.d.ts +27 -19
- package/build/types.d.ts.map +1 -1
- package/build/utils/criteria.d.ts +10 -6
- package/build/utils/criteria.d.ts.map +1 -1
- package/build/utils/criteria.js +13 -5
- package/build/utils/criteria.js.map +1 -1
- package/build/utils/diff.d.ts.map +1 -1
- package/build/utils/diff.js +31 -1
- package/build/utils/diff.js.map +1 -1
- package/build/utils/migrations.d.ts +2 -2
- package/build/utils/migrations.js +2 -2
- package/build/utils/name.d.ts +3 -3
- package/build/utils/name.js +3 -3
- package/build/utils/sql.d.ts +10 -2
- package/build/utils/sql.d.ts.map +1 -1
- package/build/utils/sql.js.map +1 -1
- package/build/utils/table.d.ts +4 -0
- package/build/utils/table.d.ts.map +1 -1
- package/build/utils/table.js +4 -0
- package/build/utils/table.js.map +1 -1
- package/package.json +7 -7
- package/src/consts.ts +8 -1
- package/src/declarations.ts +5 -5
- package/src/resource.ts +118 -135
- package/src/types.ts +29 -18
- package/src/utils/criteria.ts +25 -17
- package/src/utils/diff.ts +31 -1
- package/src/utils/migrations.ts +2 -2
- package/src/utils/name.ts +3 -3
- package/src/utils/sql.ts +0 -0
- package/src/utils/table.ts +5 -1
- package/tests/criteria.spec.ts +187 -0
- package/tests/diff.spec.ts +60 -0
- package/tests/name.spec.ts +3 -3
- package/tests/sql.spec.ts +4 -3
package/src/resource.ts
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { appendContextual, assertContext } from '@owlmeans/context'
|
|
2
|
-
import type { BasicContext, Contextual } from '@owlmeans/context'
|
|
3
2
|
import {
|
|
4
|
-
MisshapedRecord, RecordExists, RecordUpdateFailed, UnknownRecordError,
|
|
5
|
-
UnsupportedArgumentError, prepareListOptions
|
|
3
|
+
MisshapedRecord, RecordExists, RecordUpdateFailed, UnknownRecordError, UnsupportedArgumentError
|
|
6
4
|
} from '@owlmeans/resource'
|
|
7
5
|
import type {
|
|
8
|
-
|
|
6
|
+
Criteria, FirstOptions, ListOptions, ListResult, MigrationStage, ResourceRecord, WriteOptions
|
|
9
7
|
} from '@owlmeans/resource'
|
|
10
8
|
import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
|
|
11
9
|
import type { AnySchema, JSONSchemaType } from 'ajv'
|
|
12
10
|
import { eq, sql } from 'drizzle-orm'
|
|
13
11
|
import type { SQL } from 'drizzle-orm'
|
|
14
|
-
import type { PgTable } from 'drizzle-orm/pg-core'
|
|
15
12
|
import type { PoolClient, QueryResultRow } from 'pg'
|
|
16
13
|
|
|
17
14
|
import { DEFAULT_DB_ALIAS, DEFAULT_PAGE_SIZE, ID_FIELD } from './consts.js'
|
|
@@ -31,29 +28,18 @@ import { refOf, resolvePlaceholders } from './utils/sql.js'
|
|
|
31
28
|
type Config = ServerConfig
|
|
32
29
|
type Context<C extends Config = Config> = ServerContext<C>
|
|
33
30
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
typeof opts === 'object' ? opts.ttl : undefined
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Hand a runtime built table to Drizzle's query builder.
|
|
44
|
-
*
|
|
45
|
-
* {@link PgRuntimeTable} is deliberately untyped — the table is compiled from a JSON schema
|
|
46
|
-
* at runtime, so there is no static shape to infer. Naming the erased `PgTable` here is what
|
|
47
|
-
* keeps the builder's *result* types usable; casting the argument to `never` instead would
|
|
48
|
-
* silently collapse every returned row to `never`.
|
|
49
|
-
*/
|
|
50
|
-
const pgTable = (table: PgRuntimeTable): PgTable => table as unknown as PgTable
|
|
31
|
+
/** Postgres has no row expiry — silently ignoring a TTL would lose data. */
|
|
32
|
+
const refuseTtl = (opts?: WriteOptions): void => {
|
|
33
|
+
if (opts?.ttl != null) {
|
|
34
|
+
throw new UnsupportedArgumentError('ttl')
|
|
35
|
+
}
|
|
36
|
+
}
|
|
51
37
|
|
|
52
38
|
export const makePostgresResource = <
|
|
53
39
|
R extends ResourceRecord, T extends PostgresResource<R> = PostgresResource<R>
|
|
54
40
|
>(
|
|
55
41
|
alias: string, dbAlias: string = DEFAULT_DB_ALIAS, serviceAlias: string = DEFAULT_DB_ALIAS,
|
|
56
|
-
|
|
42
|
+
tableName?: string
|
|
57
43
|
): T => {
|
|
58
44
|
const location = `postgres-resource:${alias}`
|
|
59
45
|
const declaration = getDeclaration(alias)
|
|
@@ -103,11 +89,24 @@ export const makePostgresResource = <
|
|
|
103
89
|
return column
|
|
104
90
|
}
|
|
105
91
|
|
|
106
|
-
|
|
107
|
-
|
|
92
|
+
/** `WHERE <column> = <value>` for one property, by its schema name. */
|
|
93
|
+
const identify = (
|
|
94
|
+
table: TableSpec, from: PgRuntimeTable, property: string, value: unknown
|
|
95
|
+
): SQL => eq(from[columnOf(table, property).property], value)
|
|
108
96
|
|
|
109
|
-
|
|
110
|
-
|
|
97
|
+
/**
|
|
98
|
+
* The condition a read or a write is keyed on. A string is the record's id; a criteria
|
|
99
|
+
* object is translated in full, so one call can ask by several fields at once.
|
|
100
|
+
*/
|
|
101
|
+
const where = (
|
|
102
|
+
table: TableSpec, from: PgRuntimeTable, query: string | Criteria<R>
|
|
103
|
+
): SQL | undefined => typeof query === 'string'
|
|
104
|
+
? identify(table, from, ID_FIELD, query)
|
|
105
|
+
: criteriaToSql(query, table, from)
|
|
106
|
+
|
|
107
|
+
/** What an unknown record is called in the error — the id, or the query that missed. */
|
|
108
|
+
const describe = (query: string | Criteria<R>): string =>
|
|
109
|
+
typeof query === 'string' ? query : JSON.stringify(query)
|
|
111
110
|
|
|
112
111
|
const raw = async <Row extends QueryResultRow = QueryResultRow>(
|
|
113
112
|
text: string, params?: unknown[]
|
|
@@ -125,67 +124,68 @@ export const makePostgresResource = <
|
|
|
125
124
|
}
|
|
126
125
|
}
|
|
127
126
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
127
|
+
/**
|
|
128
|
+
* The single-record read both `get` and `load` answer with. Standalone so neither closes over
|
|
129
|
+
* `resource` — a member that referenced it could not be inferred against the generic.
|
|
130
|
+
*/
|
|
131
|
+
const loadOne = async (query: string | Criteria<R>, opts?: FirstOptions<R>): Promise<R | null> => {
|
|
132
|
+
const { db, spec: table, entity: from } = await ensure()
|
|
134
133
|
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
const rows = await db.drizzle.select().from(from).where(where(table, from, query))
|
|
135
|
+
.orderBy(...sortToSql(opts?.sort, table, from)).limit(1)
|
|
137
136
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
throw new
|
|
137
|
+
return rows.length < 1 ? null : resultToRecord<R>(rows[0] as Record<string, unknown>, table)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const members: Partial<PostgresResource<R>> = {
|
|
141
|
+
get: (async (query: string | Criteria<R>, opts?: FirstOptions<R>): Promise<R> => {
|
|
142
|
+
const record = await loadOne(query, opts)
|
|
143
|
+
if (record == null) {
|
|
144
|
+
throw new UnknownRecordError(describe(query))
|
|
146
145
|
}
|
|
147
|
-
const { db, spec: table, entity: from } = await ensure()
|
|
148
146
|
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
return record
|
|
148
|
+
}) as T['get'],
|
|
151
149
|
|
|
152
|
-
|
|
153
|
-
},
|
|
150
|
+
load: loadOne as T['load'],
|
|
154
151
|
|
|
155
|
-
list: async (criteria
|
|
152
|
+
list: async (criteria?: Criteria<R>, opts?: ListOptions<R>): Promise<ListResult<R>> => {
|
|
156
153
|
const { db, spec: table, entity: from } = await ensure()
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
const size = pager.size ?? DEFAULT_PAGE_SIZE
|
|
160
|
-
const where = criteriaToSql(options.criteria, table, from)
|
|
154
|
+
const condition = criteriaToSql(criteria, table, from)
|
|
155
|
+
const order = sortToSql(opts?.sort, table, from)
|
|
161
156
|
|
|
157
|
+
/** Counted separately, so `total` describes the whole match rather than the page. */
|
|
162
158
|
const totals = await db.drizzle
|
|
163
|
-
.select({ total: sql<number>`count(*)::int` }).from(
|
|
159
|
+
.select({ total: sql<number>`count(*)::int` }).from(from).where(condition)
|
|
164
160
|
const total = Number((totals[0] as { total: number } | undefined)?.total ?? 0)
|
|
165
|
-
pager.total = total
|
|
166
161
|
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
return { items: [], pager }
|
|
170
|
-
}
|
|
162
|
+
const marshal = (rows: unknown[]): R[] =>
|
|
163
|
+
rows.map(row => resultToRecord<R>(row as Record<string, unknown>, table))
|
|
171
164
|
|
|
172
|
-
|
|
173
|
-
|
|
165
|
+
/** `size: 0` lifts the limit — the explicit, greppable way to read a whole table. */
|
|
166
|
+
const size = opts?.size ?? DEFAULT_PAGE_SIZE
|
|
167
|
+
if (size === 0) {
|
|
168
|
+
const all = total === 0
|
|
169
|
+
? []
|
|
170
|
+
: await db.drizzle.select().from(from).where(condition).orderBy(...order)
|
|
174
171
|
|
|
175
|
-
|
|
176
|
-
|
|
172
|
+
return { items: marshal(all), total }
|
|
173
|
+
}
|
|
177
174
|
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
?
|
|
182
|
-
:
|
|
175
|
+
const page = opts?.page ?? 0
|
|
176
|
+
const skip = page * size
|
|
177
|
+
const rows = total === 0 || skip >= total
|
|
178
|
+
? []
|
|
179
|
+
: await db.drizzle.select().from(from).where(condition).orderBy(...order)
|
|
180
|
+
.limit(size).offset(skip)
|
|
183
181
|
|
|
184
|
-
return
|
|
185
|
-
? resource.update(record, opts)
|
|
186
|
-
: resource.create(record, typeof opts !== 'string' ? opts : undefined)
|
|
182
|
+
return { items: marshal(rows), total, page, size }
|
|
187
183
|
},
|
|
188
184
|
|
|
185
|
+
save: async (record, opts) => record.id != null
|
|
186
|
+
? resource.update(record, opts)
|
|
187
|
+
: resource.create(record, opts),
|
|
188
|
+
|
|
189
189
|
create: async (record, opts) => {
|
|
190
190
|
if (ID_FIELD in record && record.id == null) {
|
|
191
191
|
delete record.id
|
|
@@ -193,9 +193,7 @@ export const makePostgresResource = <
|
|
|
193
193
|
if (record.id != null) {
|
|
194
194
|
throw new RecordExists('id-present')
|
|
195
195
|
}
|
|
196
|
-
|
|
197
|
-
throw new UnsupportedArgumentError('ttl')
|
|
198
|
-
}
|
|
196
|
+
refuseTtl(opts)
|
|
199
197
|
|
|
200
198
|
return resource.insert(record)
|
|
201
199
|
},
|
|
@@ -206,12 +204,12 @@ export const makePostgresResource = <
|
|
|
206
204
|
{ ...resource.getDefaults(), ...record } as Record<string, unknown>, table
|
|
207
205
|
)
|
|
208
206
|
try {
|
|
209
|
-
const rows = await db.drizzle.insert(
|
|
207
|
+
const rows = await db.drizzle.insert(into).values(values as never).returning()
|
|
210
208
|
if (rows.length < 1) {
|
|
211
209
|
throw new RecordUpdateFailed('creation')
|
|
212
210
|
}
|
|
213
211
|
|
|
214
|
-
return resultToRecord(rows[0] as Record<string, unknown>, table)
|
|
212
|
+
return resultToRecord<R>(rows[0] as Record<string, unknown>, table)
|
|
215
213
|
} catch (error) {
|
|
216
214
|
throw pgErrorToResourceError(error)
|
|
217
215
|
}
|
|
@@ -240,47 +238,47 @@ export const makePostgresResource = <
|
|
|
240
238
|
}
|
|
241
239
|
|
|
242
240
|
try {
|
|
243
|
-
const rows = await db.drizzle.insert(
|
|
244
|
-
.onConflictDoUpdate({ target
|
|
241
|
+
const rows = await db.drizzle.insert(into).values(values as never)
|
|
242
|
+
.onConflictDoUpdate({ target, set: set as never }).returning()
|
|
245
243
|
if (rows.length < 1) {
|
|
246
244
|
throw new RecordUpdateFailed('upsert')
|
|
247
245
|
}
|
|
248
246
|
|
|
249
|
-
return resultToRecord(rows[0] as Record<string, unknown>, table)
|
|
247
|
+
return resultToRecord<R>(rows[0] as Record<string, unknown>, table)
|
|
250
248
|
} catch (error) {
|
|
251
249
|
throw pgErrorToResourceError(error)
|
|
252
250
|
}
|
|
253
251
|
},
|
|
254
252
|
|
|
255
253
|
update: async (record, opts) => {
|
|
254
|
+
refuseTtl(opts)
|
|
256
255
|
const { db, spec: table, entity: target } = await ensure()
|
|
257
|
-
const
|
|
258
|
-
const key = record[field as keyof typeof record]
|
|
256
|
+
const key = record.id
|
|
259
257
|
if (key == null) {
|
|
260
|
-
throw new MisshapedRecord(
|
|
258
|
+
throw new MisshapedRecord(ID_FIELD)
|
|
261
259
|
}
|
|
262
260
|
|
|
263
261
|
/** Replace, not merge — the semantics mongo's `replaceOne` gives. `patch()` merges. */
|
|
264
262
|
const values = recordToFullValues(record as Record<string, unknown>, table)
|
|
265
263
|
try {
|
|
266
|
-
const rows = await db.drizzle.update(
|
|
267
|
-
.where(identify(table,
|
|
264
|
+
const rows = await db.drizzle.update(target).set(values as never)
|
|
265
|
+
.where(identify(table, target, ID_FIELD, key)).returning()
|
|
268
266
|
if (rows.length < 1) {
|
|
269
|
-
throw new UnknownRecordError(`${
|
|
267
|
+
throw new UnknownRecordError(`${key}`)
|
|
270
268
|
}
|
|
271
269
|
|
|
272
|
-
return resultToRecord(rows[0] as Record<string, unknown>, table)
|
|
270
|
+
return resultToRecord<R>(rows[0] as Record<string, unknown>, table)
|
|
273
271
|
} catch (error) {
|
|
274
272
|
throw pgErrorToResourceError(error)
|
|
275
273
|
}
|
|
276
274
|
},
|
|
277
275
|
|
|
278
276
|
patch: async (record, opts) => {
|
|
277
|
+
refuseTtl(opts)
|
|
279
278
|
const { db, spec: table, entity: target } = await ensure()
|
|
280
|
-
const
|
|
281
|
-
const key = record[field as keyof typeof record]
|
|
279
|
+
const key = record.id
|
|
282
280
|
if (key == null) {
|
|
283
|
-
throw new MisshapedRecord(
|
|
281
|
+
throw new MisshapedRecord(ID_FIELD)
|
|
284
282
|
}
|
|
285
283
|
|
|
286
284
|
const values = recordToValues(record as Record<string, unknown>, table)
|
|
@@ -288,41 +286,36 @@ export const makePostgresResource = <
|
|
|
288
286
|
delete values[table.byColumn[column]?.property ?? column]
|
|
289
287
|
}
|
|
290
288
|
if (Object.keys(values).length < 1) {
|
|
291
|
-
return resource.get(`${key}
|
|
289
|
+
return resource.get(`${key}`)
|
|
292
290
|
}
|
|
293
291
|
|
|
294
292
|
try {
|
|
295
|
-
const rows = await db.drizzle.update(
|
|
296
|
-
.where(identify(table,
|
|
293
|
+
const rows = await db.drizzle.update(target).set(values as never)
|
|
294
|
+
.where(identify(table, target, ID_FIELD, key)).returning()
|
|
297
295
|
if (rows.length < 1) {
|
|
298
|
-
throw new UnknownRecordError(`${
|
|
296
|
+
throw new UnknownRecordError(`${key}`)
|
|
299
297
|
}
|
|
300
298
|
|
|
301
|
-
return resultToRecord(rows[0] as Record<string, unknown>, table)
|
|
299
|
+
return resultToRecord<R>(rows[0] as Record<string, unknown>, table)
|
|
302
300
|
} catch (error) {
|
|
303
301
|
throw pgErrorToResourceError(error)
|
|
304
302
|
}
|
|
305
303
|
},
|
|
306
304
|
|
|
307
|
-
delete: async
|
|
305
|
+
delete: async id => {
|
|
308
306
|
const { db, spec: table, entity: from } = await ensure()
|
|
309
|
-
const field = fieldOf(opts) ?? ID_FIELD
|
|
310
|
-
const key = typeof id === 'object' ? id[field as keyof typeof id] : id
|
|
311
|
-
if (key == null) {
|
|
312
|
-
throw new MisshapedRecord(field === ID_FIELD ? ID_FIELD : 'no-field-value')
|
|
313
|
-
}
|
|
314
307
|
|
|
315
308
|
/** One statement — mongo's read-then-delete can lose the record between the two. */
|
|
316
|
-
const rows = await db.drizzle.delete(
|
|
317
|
-
.where(identify(table,
|
|
309
|
+
const rows = await db.drizzle.delete(from)
|
|
310
|
+
.where(identify(table, from, ID_FIELD, id)).returning()
|
|
318
311
|
|
|
319
|
-
return rows.length < 1 ? null : resultToRecord(rows[0] as Record<string, unknown>, table)
|
|
312
|
+
return rows.length < 1 ? null : resultToRecord<R>(rows[0] as Record<string, unknown>, table)
|
|
320
313
|
},
|
|
321
314
|
|
|
322
|
-
|
|
323
|
-
const record = await resource.delete(id
|
|
315
|
+
take: async id => {
|
|
316
|
+
const record = await resource.delete(id)
|
|
324
317
|
if (record == null) {
|
|
325
|
-
throw new UnknownRecordError(
|
|
318
|
+
throw new UnknownRecordError(id)
|
|
326
319
|
}
|
|
327
320
|
|
|
328
321
|
return record
|
|
@@ -330,21 +323,20 @@ export const makePostgresResource = <
|
|
|
330
323
|
|
|
331
324
|
purge: async criteria => {
|
|
332
325
|
const { db, spec: table, entity: from } = await ensure()
|
|
333
|
-
const
|
|
334
|
-
if (
|
|
326
|
+
const condition = criteriaToSql(criteria, table, from)
|
|
327
|
+
if (condition == null) {
|
|
335
328
|
/** An empty criteria object here would silently truncate the table. */
|
|
336
329
|
throw new UnsupportedArgumentError('purge:no-criteria')
|
|
337
330
|
}
|
|
338
|
-
const rows = await db.drizzle.delete(
|
|
331
|
+
const rows = await db.drizzle.delete(from).where(condition).returning()
|
|
339
332
|
|
|
340
333
|
return rows.length
|
|
341
334
|
},
|
|
342
335
|
|
|
343
336
|
count: async criteria => {
|
|
344
337
|
const { db, spec: table, entity: from } = await ensure()
|
|
345
|
-
const options = prepareListOptions(DEFAULT_PAGE_SIZE, criteria)
|
|
346
338
|
const rows = await db.drizzle.select({ total: sql<number>`count(*)::int` })
|
|
347
|
-
.from(
|
|
339
|
+
.from(from).where(criteriaToSql(criteria, table, from))
|
|
348
340
|
|
|
349
341
|
return Number((rows[0] as { total: number } | undefined)?.total ?? 0)
|
|
350
342
|
},
|
|
@@ -366,9 +358,9 @@ export const makePostgresResource = <
|
|
|
366
358
|
}, {}) as Partial<R>
|
|
367
359
|
},
|
|
368
360
|
|
|
369
|
-
query: async (text, params) => (await raw(text, params)).rows,
|
|
361
|
+
query: (async (text: string, params?: unknown[]) => (await raw(text, params)).rows) as T['query'],
|
|
370
362
|
|
|
371
|
-
queryOne: async (text, params) => (await raw(text, params)).rows[0] ?? null,
|
|
363
|
+
queryOne: (async (text: string, params?: unknown[]) => (await raw(text, params)).rows[0] ?? null) as T['queryOne'],
|
|
372
364
|
|
|
373
365
|
execute: async (text, params) => (await raw(text, params)).count,
|
|
374
366
|
|
|
@@ -376,14 +368,14 @@ export const makePostgresResource = <
|
|
|
376
368
|
const { spec: table } = await ensure()
|
|
377
369
|
const result = await raw(text, params)
|
|
378
370
|
|
|
379
|
-
return result.rows.map(row => rowToRecord(row, table))
|
|
371
|
+
return result.rows.map(row => rowToRecord<R>(row, table))
|
|
380
372
|
},
|
|
381
373
|
|
|
382
374
|
selectOne: async (text, params) => {
|
|
383
375
|
const { spec: table } = await ensure()
|
|
384
376
|
const result = await raw(text, params)
|
|
385
377
|
|
|
386
|
-
return result.rows.length < 1 ? null : rowToRecord(result.rows[0], table)
|
|
378
|
+
return result.rows.length < 1 ? null : rowToRecord<R>(result.rows[0], table)
|
|
387
379
|
},
|
|
388
380
|
|
|
389
381
|
ref: resourceAlias => {
|
|
@@ -444,13 +436,14 @@ export const makePostgresResource = <
|
|
|
444
436
|
return postgres.client(dbAlias)
|
|
445
437
|
},
|
|
446
438
|
|
|
447
|
-
|
|
439
|
+
/** Chainable — the declaration store is keyed by alias, so this object is the whole state. */
|
|
440
|
+
index: (name: string, index: PgIndexSpec) => {
|
|
448
441
|
declaration.indexes.push({ ...index, name })
|
|
449
442
|
|
|
450
|
-
return resource
|
|
443
|
+
return resource
|
|
451
444
|
},
|
|
452
445
|
|
|
453
|
-
/**
|
|
446
|
+
/** Self-returning in the interface — the implementation returns that very object. */
|
|
454
447
|
migration: ((name: string, apply: (tx: PostgresTx) => Promise<void>, stage?: MigrationStage) => {
|
|
455
448
|
declaration.migrations.register(name, apply, stage)
|
|
456
449
|
|
|
@@ -458,12 +451,14 @@ export const makePostgresResource = <
|
|
|
458
451
|
}) as T['migration'],
|
|
459
452
|
|
|
460
453
|
migrations: () => declaration.migrations
|
|
461
|
-
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const resource: T = appendContextual<T>(alias, members as Partial<T>)
|
|
462
457
|
|
|
463
458
|
/**
|
|
464
|
-
* The AJV schema is stored per alias rather than on the object, so
|
|
465
|
-
*
|
|
466
|
-
*
|
|
459
|
+
* The AJV schema is stored per alias rather than on the object, so a second maker run for
|
|
460
|
+
* the same alias (a custom maker, a repeated maker, a spec) picks up the schema the app
|
|
461
|
+
* assigned after construction instead of silently taking the table's structure with it.
|
|
467
462
|
*/
|
|
468
463
|
Object.defineProperty(resource, 'schema', {
|
|
469
464
|
enumerable: true,
|
|
@@ -483,8 +478,7 @@ export const makePostgresResource = <
|
|
|
483
478
|
|
|
484
479
|
/**
|
|
485
480
|
* Explicit table name override, decoupled from the registration alias — an alias may
|
|
486
|
-
* carry characters no identifier allows.
|
|
487
|
-
* below so it survives a context switch.
|
|
481
|
+
* carry characters no identifier allows.
|
|
488
482
|
*/
|
|
489
483
|
if (tableName != null) {
|
|
490
484
|
resource.name = tableName
|
|
@@ -505,16 +499,5 @@ export const makePostgresResource = <
|
|
|
505
499
|
entity = initialized.entity
|
|
506
500
|
}
|
|
507
501
|
|
|
508
|
-
resource.reinitializeContext = <Type extends Contextual>(context: BasicContext<Config>) => {
|
|
509
|
-
const replacement = (makeCustomResource?.(dbAlias, serviceAlias)
|
|
510
|
-
?? makePostgresResource<R, T>(
|
|
511
|
-
alias, dbAlias, serviceAlias, makeCustomResource, tableName
|
|
512
|
-
)) as unknown as Type
|
|
513
|
-
|
|
514
|
-
replacement.ctx = context
|
|
515
|
-
|
|
516
|
-
return replacement
|
|
517
|
-
}
|
|
518
|
-
|
|
519
502
|
return resource
|
|
520
503
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
DbLocker,
|
|
3
|
-
ResourceDbService,
|
|
2
|
+
DbLocker, LockableResource, MigratableResource, Resource,
|
|
3
|
+
ResourceDbService, ResourceRecord, WriteOptions
|
|
4
4
|
} from '@owlmeans/resource'
|
|
5
5
|
import type { AnySchema } from 'ajv'
|
|
6
6
|
import type { Pool, PoolClient, QueryResultRow } from 'pg'
|
|
7
7
|
import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
|
|
8
|
+
import type { PgColumn, PgTable } from 'drizzle-orm/pg-core'
|
|
8
9
|
|
|
9
10
|
import type { PgAutoSync, PgIndexMethod, PgReferentialAction } from './consts.js'
|
|
10
11
|
|
|
@@ -17,9 +18,9 @@ export interface PostgresDb {
|
|
|
17
18
|
/** Drizzle bound to this config alias' pool. */
|
|
18
19
|
drizzle: NodePgDatabase<Record<string, never>>
|
|
19
20
|
pool: Pool
|
|
20
|
-
/** The Postgres SCHEMA — i.e. `service.name(alias)
|
|
21
|
+
/** The Postgres SCHEMA this config alias' resources live in — i.e. `service.name(alias)`. */
|
|
21
22
|
schema: string
|
|
22
|
-
/** The Postgres DATABASE — i.e. `config.meta.database`.
|
|
23
|
+
/** The Postgres DATABASE the pool is connected to — i.e. `config.meta.database`. */
|
|
23
24
|
database: string
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -72,7 +73,16 @@ export interface PostgresTx {
|
|
|
72
73
|
ref: (resourceAlias?: string) => string
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
/**
|
|
77
|
+
* A `Resource<T>` over one Postgres table.
|
|
78
|
+
*
|
|
79
|
+
* Every method beyond the base contract exists because a table has structure a document
|
|
80
|
+
* store does not: identity supplied by the caller, conflict arbiters, partial writes, and
|
|
81
|
+
* SQL that no criteria object can express. Type the resource once —
|
|
82
|
+
* `context.resource<PostgresResource<Project>>(alias)` — rather than per call.
|
|
83
|
+
*/
|
|
84
|
+
export interface PostgresResource<T extends ResourceRecord>
|
|
85
|
+
extends Resource<T>, LockableResource<T>, MigratableResource<PostgresTx, PostgresResource<T>> {
|
|
76
86
|
/** Physical table name override. Defaults to the resource alias, sanitized. */
|
|
77
87
|
name?: string
|
|
78
88
|
/** The AJV schema — single source of truth for the table structure. */
|
|
@@ -86,7 +96,7 @@ export interface PostgresResource<T extends ResourceRecord> extends Resource<T>,
|
|
|
86
96
|
client: () => Promise<Pool>
|
|
87
97
|
|
|
88
98
|
/** Declare an index the JSON schema can't express. Chainable. */
|
|
89
|
-
index:
|
|
99
|
+
index: (name: string, spec: PgIndexSpec) => PostgresResource<T>
|
|
90
100
|
|
|
91
101
|
getDefaults: () => Partial<T>
|
|
92
102
|
|
|
@@ -96,22 +106,19 @@ export interface PostgresResource<T extends ResourceRecord> extends Resource<T>,
|
|
|
96
106
|
/** Returns the affected row count. */
|
|
97
107
|
execute: (text: string, params?: unknown[]) => Promise<number>
|
|
98
108
|
/** Rows marshalled back into `T` through the table spec. */
|
|
99
|
-
select:
|
|
100
|
-
selectOne:
|
|
109
|
+
select: (text: string, params?: unknown[]) => Promise<T[]>
|
|
110
|
+
selectOne: (text: string, params?: unknown[]) => Promise<T | null>
|
|
101
111
|
/** Fully qualified `"schema"."table"`; pass an alias to reference another resource. */
|
|
102
112
|
ref: (resourceAlias?: string) => string
|
|
103
113
|
|
|
104
114
|
transaction: <R>(fn: (tx: PostgresTx) => Promise<R>) => Promise<R>
|
|
105
115
|
|
|
106
116
|
/** Create with a caller supplied id — `create()` refuses one, mirroring mongo. */
|
|
107
|
-
insert:
|
|
117
|
+
insert: (record: Partial<T>) => Promise<T>
|
|
108
118
|
/** `INSERT ... ON CONFLICT (...) DO UPDATE`. Defaults to conflicting on the primary key. */
|
|
109
|
-
upsert:
|
|
119
|
+
upsert: (record: Partial<T>, conflict?: string[]) => Promise<T>
|
|
110
120
|
/** Merge semantics — `update()` replaces the whole record, mirroring mongo's `replaceOne`. */
|
|
111
|
-
patch:
|
|
112
|
-
/** Bulk delete by criteria. Returns the deleted row count. */
|
|
113
|
-
purge: (criteria: ListCriteria) => Promise<number>
|
|
114
|
-
count: (criteria?: ListCriteria | ListOptions) => Promise<number>
|
|
121
|
+
patch: (record: Partial<T>, opts?: WriteOptions) => Promise<T>
|
|
115
122
|
}
|
|
116
123
|
|
|
117
124
|
/** Compiled, database ready description of a resource's table. */
|
|
@@ -245,11 +252,15 @@ export interface PgReferenceSpec {
|
|
|
245
252
|
}
|
|
246
253
|
|
|
247
254
|
/**
|
|
248
|
-
* The Drizzle table
|
|
249
|
-
*
|
|
250
|
-
*
|
|
255
|
+
* The Drizzle table `specToTable` compiles from a {@link TableSpec}.
|
|
256
|
+
*
|
|
257
|
+
* Both halves are load bearing. `PgTable` is what the query builder accepts and what keeps
|
|
258
|
+
* the rows it hands back usable — erasing it to `never` would collapse every result row.
|
|
259
|
+
* The index signature is how a column is reached: the columns are keyed by schema property
|
|
260
|
+
* name and only known at runtime, so `eq(entity[column.property], value)` is a plain lookup
|
|
261
|
+
* rather than a cast.
|
|
251
262
|
*/
|
|
252
|
-
export type PgRuntimeTable = Record<string,
|
|
263
|
+
export type PgRuntimeTable = PgTable & Record<string, PgColumn>
|
|
253
264
|
|
|
254
265
|
/** One column as Postgres currently reports it. */
|
|
255
266
|
export interface LiveColumn {
|