terrier-engine 4.0.3
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 +3 -0
- package/api.ts +112 -0
- package/db-client.ts +389 -0
- package/package.json +22 -0
package/README.md
ADDED
package/api.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Logger } from "tuff-core/logging"
|
|
2
|
+
import { QueryParams } from "tuff-core/urls"
|
|
3
|
+
|
|
4
|
+
const log = new Logger('Api')
|
|
5
|
+
log.level = 'debug'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* All API responses containing these fields.
|
|
9
|
+
*/
|
|
10
|
+
export type ApiResponse = {
|
|
11
|
+
status: 'success' | 'error'
|
|
12
|
+
message: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Exception that gets thrown when an API call fails.
|
|
17
|
+
*/
|
|
18
|
+
export class ApiException extends Error {
|
|
19
|
+
constructor(message: string) {
|
|
20
|
+
super(`ApiException: ${message}`)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function request<ResponseType>(url: string, config: RequestInit): Promise<ResponseType> {
|
|
25
|
+
const response = await fetch(url, config)
|
|
26
|
+
return await response.json()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function apiRequest<ResponseType>(url: string, config: RequestInit): Promise<ApiResponse & ResponseType> {
|
|
30
|
+
const response = await request<{ status?: unknown, message?: string } & ResponseType>(url, config)
|
|
31
|
+
|
|
32
|
+
if (response.status && typeof response.status == 'string' && (response.status == 'success' || response.status == 'error')) {
|
|
33
|
+
return response as ApiResponse & ResponseType
|
|
34
|
+
} else {
|
|
35
|
+
// If response.status does not exist or is not a string, e.g. 200, 404, etc.
|
|
36
|
+
throw new ApiException(response.message ?? "Unknown API Exception")
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Performs a GET request for the given datatype.
|
|
42
|
+
* This will only return if the response status=success, it will throw on error.
|
|
43
|
+
* `ResponseType` does not need to include the `status` or `message` fields, this is handled automatically.
|
|
44
|
+
* @param url the base URL for the request
|
|
45
|
+
* @param params a set of parameters that will be added to the URL as a query string
|
|
46
|
+
*/
|
|
47
|
+
async function safeGet<ResponseType>(url: string, params: QueryParams | Record<string, string | undefined>): Promise<ResponseType> {
|
|
48
|
+
if (!params.raw) {
|
|
49
|
+
params = new QueryParams(params as Record<string, string>)
|
|
50
|
+
}
|
|
51
|
+
const fullUrl = (params as QueryParams).serialize(url)
|
|
52
|
+
log.debug(`Safe getting ${fullUrl}`)
|
|
53
|
+
const response = await apiRequest<ResponseType>(fullUrl, {
|
|
54
|
+
method: 'GET',
|
|
55
|
+
headers: {
|
|
56
|
+
'Accept': 'application/json'
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
if (response.status == 'error') {
|
|
60
|
+
throw new ApiException(response.message)
|
|
61
|
+
}
|
|
62
|
+
return response
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Performs a POST API request for the given data type.
|
|
67
|
+
* This will only return if the response status=success, it will throw on error.
|
|
68
|
+
* `ResponseType` does not need to include the `status` or `message` fields, this is handled automatically.
|
|
69
|
+
* @param url the URL of the API endpoint
|
|
70
|
+
* @param body the body of the request (will be transmitted as JSON)
|
|
71
|
+
*/
|
|
72
|
+
async function safePost<ResponseType>(url: string, body: Record<string, unknown>): Promise<ResponseType> {
|
|
73
|
+
log.debug(`Safe posting to ${url} with body`, body)
|
|
74
|
+
const response = await apiRequest<ResponseType>(url, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: {
|
|
77
|
+
'Content-Type': 'application/json'
|
|
78
|
+
},
|
|
79
|
+
body: JSON.stringify(body)
|
|
80
|
+
})
|
|
81
|
+
if (response.status == 'error') {
|
|
82
|
+
throw new ApiException(response.message)
|
|
83
|
+
}
|
|
84
|
+
return response
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Performs a POST API request for the given data type.
|
|
89
|
+
* Unlike `safePost`, this will return regardless of the status of the request.
|
|
90
|
+
* The result will automatically include ApiResponse.
|
|
91
|
+
* @param url the URL of the API endpoint
|
|
92
|
+
* @param body the body of the request (will be transmitted as JSON)
|
|
93
|
+
*/
|
|
94
|
+
async function post<ResponseType>(url: string, body: Record<string, unknown> | FormData): Promise<ResponseType & ApiResponse> {
|
|
95
|
+
log.debug(`Posting to ${url} with body`, body)
|
|
96
|
+
const config = { method: 'POST' } as RequestInit
|
|
97
|
+
if (body instanceof FormData) {
|
|
98
|
+
config.body = body
|
|
99
|
+
} else {
|
|
100
|
+
config.body = JSON.stringify(body)
|
|
101
|
+
config.headers = { 'Content-Type': 'application/json' }
|
|
102
|
+
}
|
|
103
|
+
return await request<ResponseType & ApiResponse>(url, config)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
const Api = {
|
|
108
|
+
safeGet,
|
|
109
|
+
safePost,
|
|
110
|
+
post
|
|
111
|
+
}
|
|
112
|
+
export default Api
|
package/db-client.ts
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// noinspection JSUnusedGlobalSymbols
|
|
2
|
+
|
|
3
|
+
import {Logger} from "tuff-core/logging"
|
|
4
|
+
import Api, {ApiResponse} from "./api"
|
|
5
|
+
|
|
6
|
+
const log = new Logger('Db')
|
|
7
|
+
log.level = 'debug'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Type that maps keys to other types.
|
|
11
|
+
*/
|
|
12
|
+
type ModelTypeMap = {
|
|
13
|
+
[name: string]: any
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type ModelIncludesMap<M extends ModelTypeMap> = Record<keyof M, any>
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Map of columns to values for the given model type.
|
|
20
|
+
*/
|
|
21
|
+
type WhereMap<M> = {
|
|
22
|
+
[col in keyof M]?: unknown
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A raw SQL where clause with an arbitrary number of additional arguments.
|
|
27
|
+
*/
|
|
28
|
+
type WhereClause = {
|
|
29
|
+
clause: string,
|
|
30
|
+
args: unknown[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generic type for a get response.
|
|
35
|
+
*/
|
|
36
|
+
type DbGetResponse<T> = {
|
|
37
|
+
records: T[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Type for a count response.
|
|
42
|
+
*/
|
|
43
|
+
type DbCountResponse = {
|
|
44
|
+
count: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Describes one or more associations to include (same as ActiveRecord `includes`).
|
|
49
|
+
*/
|
|
50
|
+
export type Includes<M extends ModelTypeMap, T extends keyof M, I extends ModelIncludesMap<M>> = {
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
|
+
[rel in I[T]]?: Includes<M,any,any>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Constructs an ActiveRecord query on the client side and executes it on the server.
|
|
57
|
+
*/
|
|
58
|
+
class ModelQuery<PM extends ModelTypeMap, T extends keyof PM & string, I extends ModelIncludesMap<PM>> {
|
|
59
|
+
|
|
60
|
+
constructor(readonly modelType: T) {
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private whereMaps = Array<WhereMap<PM[T]>>()
|
|
64
|
+
private whereClauses = Array<WhereClause>()
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Adds one or more filters to the query.
|
|
68
|
+
* @param map a map of columns to scalar values.
|
|
69
|
+
*/
|
|
70
|
+
where(map: WhereMap<PM[T]>): ModelQuery<PM,T,I>
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Adds one or more filters to the query.
|
|
74
|
+
* @param clause an arbitrary string WHERE clause
|
|
75
|
+
* @param args any injected arguments into the clause string
|
|
76
|
+
*/
|
|
77
|
+
where(clause: string, ...args: unknown[]): ModelQuery<PM,T,I>
|
|
78
|
+
|
|
79
|
+
where(mapOrClause: WhereMap<PM[T]> | string, ...args: unknown[]): ModelQuery<PM,T,I> {
|
|
80
|
+
if (typeof mapOrClause == 'object') {
|
|
81
|
+
this.whereMaps.push(mapOrClause)
|
|
82
|
+
} else {
|
|
83
|
+
this.whereClauses.push({clause: mapOrClause, args})
|
|
84
|
+
}
|
|
85
|
+
return this
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private _includes: Includes<PM,T,I> = {}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Add one or more ActiveRecord-style includes.
|
|
92
|
+
* @param inc maps association names to potentially more association names, or empty objects.
|
|
93
|
+
*/
|
|
94
|
+
includes(inc: Includes<PM,T,I>): ModelQuery<PM,T,I> {
|
|
95
|
+
this._includes = {...inc, ...this._includes}
|
|
96
|
+
return this
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private _joins = Array<string>()
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Adds an ActiveRecord-style joins statement.
|
|
103
|
+
* @param join the association to join
|
|
104
|
+
*/
|
|
105
|
+
joins(join: string): ModelQuery<PM,T,I> {
|
|
106
|
+
this._joins.push(join)
|
|
107
|
+
return this
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private order = ''
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Set an ORDER BY statement for the query.
|
|
114
|
+
* @param order a SQL ORDER BY statement
|
|
115
|
+
*/
|
|
116
|
+
orderBy(order: string): ModelQuery<PM,T,I> {
|
|
117
|
+
this.order = order
|
|
118
|
+
return this
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private _limit = 0
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Adds a result limit to the query.
|
|
125
|
+
* @param max the query limit
|
|
126
|
+
*/
|
|
127
|
+
limit(max: number): ModelQuery<PM,T,I> {
|
|
128
|
+
this._limit = max
|
|
129
|
+
return this
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Asynchronously execute the query on the server and returns the response.
|
|
134
|
+
* Only returns on success, throws otherwise.
|
|
135
|
+
*/
|
|
136
|
+
async exec(): Promise<PM[T][]> {
|
|
137
|
+
const url = `/db/model/${this.modelType}.json`
|
|
138
|
+
const body = {
|
|
139
|
+
where_maps: this.whereMaps,
|
|
140
|
+
where_clauses: this.whereClauses,
|
|
141
|
+
includes: this._includes,
|
|
142
|
+
joins: this._joins,
|
|
143
|
+
limit: this._limit,
|
|
144
|
+
order: this.order
|
|
145
|
+
}
|
|
146
|
+
log.debug(`Getting ${this.modelType} query at ${url} with body`, body)
|
|
147
|
+
const res = await Api.safePost<DbGetResponse<PM[T]>>(url, body)
|
|
148
|
+
return res.records
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Retrieves the first record.
|
|
153
|
+
*/
|
|
154
|
+
async first(): Promise<PM[T] | undefined> {
|
|
155
|
+
const records = await this.limit(1).exec()
|
|
156
|
+
return records[0]
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Counts the number of records represented by the query.
|
|
161
|
+
*/
|
|
162
|
+
async count(): Promise<number> {
|
|
163
|
+
const url = `/db/model/${this.modelType}/count.json`
|
|
164
|
+
const body = {
|
|
165
|
+
where_maps: this.whereMaps,
|
|
166
|
+
where_clauses: this.whereClauses,
|
|
167
|
+
joins: this._joins
|
|
168
|
+
}
|
|
169
|
+
log.debug(`Counting ${this.modelType} query at ${url} with body`, body)
|
|
170
|
+
const res = await Api.safePost<DbCountResponse>(url, body)
|
|
171
|
+
return res.count
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Generic database client that works with persisted and unpersisted type maps.
|
|
178
|
+
* @template PM the type map for persisted model types
|
|
179
|
+
* @template UM the type map for unpersisted model types
|
|
180
|
+
* @template I the type map for model includes
|
|
181
|
+
*/
|
|
182
|
+
export default class DbClient<PM extends ModelTypeMap, UM extends ModelTypeMap, I extends ModelIncludesMap<PM>> {
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Start a new query for the given model type.
|
|
186
|
+
* @param modelType the camel_case name of the model
|
|
187
|
+
*/
|
|
188
|
+
query<T extends keyof PM & string>(modelType: T) {
|
|
189
|
+
return new ModelQuery<PM,T,I>(modelType)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Fetches a single record by id.
|
|
195
|
+
* Throws an error if the record doesn't exist.
|
|
196
|
+
* @param modelType the camel_case name of the model
|
|
197
|
+
* @param id the id of the record
|
|
198
|
+
* @param includes relations to include in the returned object
|
|
199
|
+
*/
|
|
200
|
+
async find<T extends keyof PM & string>(modelType: T, id: string, includes?: I[T]): Promise<PM[T]> {
|
|
201
|
+
const query = new ModelQuery<PM,T,I>(modelType).where("id = ?", id)
|
|
202
|
+
if (includes) {
|
|
203
|
+
query.includes(includes)
|
|
204
|
+
}
|
|
205
|
+
const record = await query.first()
|
|
206
|
+
if (record) {
|
|
207
|
+
return record as PM[T]
|
|
208
|
+
} else {
|
|
209
|
+
throw new DbFindException(`No ${modelType} with id=${id}`)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Fetches a single record by id *or* slug.
|
|
215
|
+
* Throws an error if the record doesn't exist.
|
|
216
|
+
* @todo See if we can narrow the definition of T to only include models with a slug column.
|
|
217
|
+
* @param modelType the camel_case name of the model
|
|
218
|
+
* @param idOrSlug the id or slug of the record
|
|
219
|
+
* @param includes relations to include in the returned object
|
|
220
|
+
*/
|
|
221
|
+
async findByIdOrSlug<T extends keyof PM & string>(modelType: T, idOrSlug: string, includes?: I[T]): Promise<PM[T]> {
|
|
222
|
+
const column = isUuid(idOrSlug) ? "id" : "slug"
|
|
223
|
+
const query = new ModelQuery(modelType).where(`${column} = ?`, idOrSlug)
|
|
224
|
+
if (includes) {
|
|
225
|
+
query.includes(includes)
|
|
226
|
+
}
|
|
227
|
+
const record = await query.first()
|
|
228
|
+
if (record) {
|
|
229
|
+
return record as PM[T]
|
|
230
|
+
} else {
|
|
231
|
+
throw new DbFindException(`No ${modelType} with id or slug ${idOrSlug}`)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Updates the given record.
|
|
238
|
+
* @param modelType the camel_case name of the model
|
|
239
|
+
* @param record the record to update
|
|
240
|
+
* @param includes relations to include in the returned record
|
|
241
|
+
*/
|
|
242
|
+
async update<T extends keyof PM & string>(modelType: T, record: PM[T], includes: Includes<PM,T,I> = {}): Promise<DbUpsertResponse<PM,T> & ApiResponse> {
|
|
243
|
+
const url = `/db/model/${modelType}/upsert.json`
|
|
244
|
+
const body = {record, includes}
|
|
245
|
+
log.debug(`Updating ${modelType} at ${url} with body`, body)
|
|
246
|
+
return await Api.post<DbUpsertResponse<PM,T>>(url, body)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Update the given record and assume it will succeed.
|
|
251
|
+
* This call should be wrapped in a try/catch.
|
|
252
|
+
* @param modelType the camel_case name of the model
|
|
253
|
+
* @param record the record to update
|
|
254
|
+
* @param includes relations to include in the returned record
|
|
255
|
+
*/
|
|
256
|
+
async safeUpdate<T extends keyof PM & string>(modelType: T, record: PM[T], includes: Includes<PM,T,I> = {}): Promise<PM[T]> {
|
|
257
|
+
const res = await this.update(modelType, record, includes)
|
|
258
|
+
if (res.status == 'success') {
|
|
259
|
+
return res.record
|
|
260
|
+
} else if (res.record) {
|
|
261
|
+
throw new DbSaveException<PM, T>(res.message, res.record as UM[T], res.errors)
|
|
262
|
+
} else {
|
|
263
|
+
throw `Error updating ${modelType}: ${res.message}`
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Inserts a new record for the given model type.
|
|
269
|
+
* @param modelType the camel_case name of the model
|
|
270
|
+
* @param record the record to update
|
|
271
|
+
* @param includes relations to include in the returned record
|
|
272
|
+
*/
|
|
273
|
+
async insert<T extends keyof PM & string>(modelType: T, record: UM[T], includes: Includes<PM,T,I> = {}): Promise<DbUpsertResponse<PM,T> & ApiResponse> {
|
|
274
|
+
const url = `/db/model/${modelType}/upsert.json`
|
|
275
|
+
const body = {record, includes}
|
|
276
|
+
log.debug(`Inserting ${modelType} at ${url} with body`, body)
|
|
277
|
+
return await Api.post<DbUpsertResponse<PM,T>>(url, body)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Inserts or updates a new record for the given model type,
|
|
282
|
+
* depending on if it has an id.
|
|
283
|
+
* @param modelType the camel_case name of the model
|
|
284
|
+
* @param record the record to update
|
|
285
|
+
* @param includes relations to include in the returned record
|
|
286
|
+
*/
|
|
287
|
+
async upsert<T extends keyof PM & string>(modelType: T, record: UM[T], includes: Includes<PM,T,I> = {}) {
|
|
288
|
+
const url = `/db/model/${modelType}/upsert.json`
|
|
289
|
+
const body = {record, includes}
|
|
290
|
+
log.debug(`Upserting ${modelType} at ${url} with body`, body)
|
|
291
|
+
return await Api.post<DbUpsertResponse<PM,T>>(url, body)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Upserts the given record and assume it will succeed.
|
|
296
|
+
* This call should be wrapped in a try/catch.
|
|
297
|
+
* @param modelType the camel_case name of the model
|
|
298
|
+
* @param record the record to update
|
|
299
|
+
* @param includes relations to include in the returned record
|
|
300
|
+
*/
|
|
301
|
+
async safeUpsert<T extends keyof PM & string>(modelType: T, record: UM[T], includes: Includes<PM,T,I> = {}) {
|
|
302
|
+
const res = await this.upsert(modelType, record, includes)
|
|
303
|
+
if (res.status == 'success') {
|
|
304
|
+
return res.record
|
|
305
|
+
} else if (res.record) {
|
|
306
|
+
throw new DbSaveException<PM,T>(res.message, res.record as UM[T], res.errors)
|
|
307
|
+
} else {
|
|
308
|
+
throw `Error upserting ${modelType}: ${res.message}`
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The exception that gets raised when a find() call fails.
|
|
317
|
+
*/
|
|
318
|
+
class DbFindException extends Error {
|
|
319
|
+
constructor(message: string) {
|
|
320
|
+
super(message)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export const UuidRegex = /[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}/gi
|
|
325
|
+
|
|
326
|
+
const CompleteUuidRegex = new RegExp(`^${UuidRegex.source}$`)
|
|
327
|
+
|
|
328
|
+
function isUuid(str: string): boolean {
|
|
329
|
+
return !!str.match(CompleteUuidRegex)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Base validation errors that aren't associated with a particular column.
|
|
334
|
+
*/
|
|
335
|
+
type DbBaseErrors = {
|
|
336
|
+
base?: string[]
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Generic type of the errors object returned from ActiveRecord validations.
|
|
341
|
+
*/
|
|
342
|
+
type DbModelErrors<T extends {}> = {
|
|
343
|
+
// either array of string errors or array of error objects on related records
|
|
344
|
+
[col in keyof T]?: string[]
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
export type DbErrors<T extends {}> = DbModelErrors<T> & DbBaseErrors
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Generic type for a create or update response.
|
|
352
|
+
*/
|
|
353
|
+
type DbUpsertResponse<PM extends ModelTypeMap, T extends keyof PM & string> = SuccessfulDbUpsertResponse<PM,T> | UnsuccessfulDbUpsertResponse<PM,T>
|
|
354
|
+
|
|
355
|
+
type SuccessfulDbUpsertResponse<PM extends ModelTypeMap, T extends keyof PM & string> = ApiResponse & {
|
|
356
|
+
status: 'success'
|
|
357
|
+
record: PM[T]
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
type UnsuccessfulDbUpsertResponse<PM extends ModelTypeMap, T extends keyof PM & string> = ApiResponse & {
|
|
361
|
+
status: 'error'
|
|
362
|
+
errors: DbErrors<PM[T]>
|
|
363
|
+
record?: PM[T]
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* The exception that gets raised when an insert or update call fails.
|
|
369
|
+
*/
|
|
370
|
+
export class DbSaveException<UM extends ModelTypeMap, T extends keyof UM> extends Error {
|
|
371
|
+
record?: UM[T]
|
|
372
|
+
errors?: DbErrors<UM[T]>
|
|
373
|
+
|
|
374
|
+
constructor(message: string, record?: UM[T], errors?: DbErrors<UM[T]>) {
|
|
375
|
+
super(message)
|
|
376
|
+
this.record = record
|
|
377
|
+
this.errors = errors
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
log(message: string, logger: Logger | Console = window.console) {
|
|
381
|
+
logger.error(message, "| message:", `"${this.message}"`, "| errors:", this.errors, "| record:", this.record)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Raised when a client-side validation fails (usually during form serialization)
|
|
387
|
+
*/
|
|
388
|
+
export class ValidationException<PM extends ModelTypeMap, T extends keyof PM> extends DbSaveException<PM,T> {
|
|
389
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "terrier-engine",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"files": [
|
|
5
|
+
"*"
|
|
6
|
+
],
|
|
7
|
+
"version": "4.0.3",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/Terrier-Tech/terrier-engine"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"scripts" : {
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"tuff-core": "^0.24.1"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^18.16.3",
|
|
20
|
+
"prettier": "^2.8.8"
|
|
21
|
+
}
|
|
22
|
+
}
|