nucleus-core-ts 0.10.113 → 0.10.115
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/dist/.build-ok +1 -1
- package/dist/client.js +1 -1
- package/dist/index.js +11 -11
- package/dist/server.js +11 -11
- package/dist/src/Client/ApiCaller/server.d.ts +9 -0
- package/dist/src/Client/ApiCaller/types.d.ts +18 -0
- package/dist/src/Nucleus/routes/entity/sortCollation.d.ts +16 -0
- package/dist/src/Nucleus/routes/entity/types.d.ts +2 -0
- package/dist/src/types.d.ts +33 -0
- package/package.json +1 -1
- package/scripts/generate-schema.test.ts +76 -0
- package/scripts/generate-schema.ts +34 -2
|
@@ -35,6 +35,15 @@ export declare function serverRequestId(response: {
|
|
|
35
35
|
* `requiresCaptcha` one the control-flag list above exists for.
|
|
36
36
|
*/
|
|
37
37
|
export declare function normalizeResponseShape(response: unknown): unknown;
|
|
38
|
+
/**
|
|
39
|
+
* Rewrite a `cookie` request header so it carries the values the backend just
|
|
40
|
+
* issued.
|
|
41
|
+
*
|
|
42
|
+
* The retry has to send the NEW access token. Headers here are built from the
|
|
43
|
+
* INCOMING request, so without this the second attempt re-sends the same expired
|
|
44
|
+
* token and earns the same 401 — a refresh that changes nothing.
|
|
45
|
+
*/
|
|
46
|
+
export declare function mergeCookieHeader(existing: string | undefined, setCookies: string[]): string;
|
|
38
47
|
export declare function createServerFactory<TEndpoints extends Record<string, EndpointDefinition>>(endpoints: TEndpoints, config: ServerFactoryConfig, getCookies: () => Promise<CookieStore>, getHeaders: () => Promise<HeadersStore>): <K extends keyof TEndpoints & string>(endpointKey: K, payload: TEndpoints[K]["_payload"]) => Promise<ApiResponse<TEndpoints[K]["_success"], TEndpoints[K]["_error"]>>;
|
|
39
48
|
export type ServerFactory<TEndpoints extends Record<string, EndpointDefinition>> = ReturnType<typeof createServerFactory<TEndpoints>>;
|
|
40
49
|
/**
|
|
@@ -2472,6 +2472,24 @@ export interface ServerFactoryConfig {
|
|
|
2472
2472
|
sessionToken?: string;
|
|
2473
2473
|
};
|
|
2474
2474
|
debug?: boolean;
|
|
2475
|
+
/**
|
|
2476
|
+
* What to do when the backend answers 401 because the access token has aged
|
|
2477
|
+
* out. Without this the caller simply sees the 401 and the person is bounced
|
|
2478
|
+
* to the login screen mid-task — on a thirty-minute token, every thirty
|
|
2479
|
+
* minutes, no matter how recently they typed their password.
|
|
2480
|
+
*/
|
|
2481
|
+
refresh?: {
|
|
2482
|
+
/** Default: true. */
|
|
2483
|
+
enabled?: boolean;
|
|
2484
|
+
/** Where to ask. Default: `/auth/refresh`. */
|
|
2485
|
+
endpoint?: string;
|
|
2486
|
+
/**
|
|
2487
|
+
* Paths that must never trigger a refresh, because a 401 from them IS the
|
|
2488
|
+
* answer. Matched as substrings of the endpoint path. Defaults to the
|
|
2489
|
+
* login, logout, register and refresh routes themselves.
|
|
2490
|
+
*/
|
|
2491
|
+
excludePaths?: string[];
|
|
2492
|
+
};
|
|
2475
2493
|
}
|
|
2476
2494
|
export interface ClientHookConfig<TEndpoints extends Record<string, EndpointDefinition>> {
|
|
2477
2495
|
endpoints: TEndpoints;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type SQL } from 'drizzle-orm';
|
|
2
|
+
export declare const COLLATABLE_COLUMN_TYPES: Set<string>;
|
|
3
|
+
/**
|
|
4
|
+
* A collation name is an IDENTIFIER, so it is interpolated into the SQL text
|
|
5
|
+
* and cannot be sent as a bound parameter. It comes from config rather than
|
|
6
|
+
* from a request, but "not attacker-controlled today" is not the same as safe:
|
|
7
|
+
* accept a bare identifier and nothing else.
|
|
8
|
+
*/
|
|
9
|
+
export declare function normalizeSortCollation(raw: unknown): string;
|
|
10
|
+
/** True when `raw` was given but is not usable — the caller should log it. */
|
|
11
|
+
export declare function isRejectedSortCollation(raw: unknown): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Wraps a sort column in the configured collation when — and only when — the
|
|
14
|
+
* column is a character type.
|
|
15
|
+
*/
|
|
16
|
+
export declare function collateColumn(col: unknown, collation: string): SQL;
|
|
@@ -30,6 +30,8 @@ export interface EntityRouteConfig {
|
|
|
30
30
|
entities: NucleusTable[];
|
|
31
31
|
logger: Logger;
|
|
32
32
|
databaseUrl?: string;
|
|
33
|
+
/** ICU collation for text-column sorts (see `database.sortCollation`). */
|
|
34
|
+
sortCollation?: string;
|
|
33
35
|
dbPool?: import('pg').Pool | null;
|
|
34
36
|
storage?: {
|
|
35
37
|
enabled?: boolean;
|
package/dist/src/types.d.ts
CHANGED
|
@@ -26,6 +26,15 @@ export interface NucleusColumnValidation {
|
|
|
26
26
|
customMessage?: string;
|
|
27
27
|
}
|
|
28
28
|
export type NucleusSanitizeOption = 'trim' | 'lowercase' | 'uppercase' | 'escapeHtml' | 'stripTags' | 'normalizeEmail' | 'toNumber' | 'toBoolean' | 'slugify';
|
|
29
|
+
export interface NucleusColumnIdentity {
|
|
30
|
+
/** First value the sequence hands out. Set it above the largest number already stored. */
|
|
31
|
+
startWith?: number;
|
|
32
|
+
increment?: number;
|
|
33
|
+
minValue?: number;
|
|
34
|
+
maxValue?: number;
|
|
35
|
+
cache?: number;
|
|
36
|
+
cycle?: boolean;
|
|
37
|
+
}
|
|
29
38
|
export interface NucleusColumn {
|
|
30
39
|
name: string;
|
|
31
40
|
type: NucleusColumnType;
|
|
@@ -49,6 +58,12 @@ export interface NucleusColumn {
|
|
|
49
58
|
generatedAlwaysAs?: string;
|
|
50
59
|
generatedAlwaysAsIdentity?: boolean;
|
|
51
60
|
generatedByDefaultAsIdentity?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Where an identity column's numbering starts and how it steps. Needed when a
|
|
63
|
+
* column adopts identity over a table that already has rows: the sequence
|
|
64
|
+
* would otherwise restart at 1 and hand out numbers those rows already carry.
|
|
65
|
+
*/
|
|
66
|
+
identity?: NucleusColumnIdentity;
|
|
52
67
|
check?: string;
|
|
53
68
|
comment?: string;
|
|
54
69
|
validation?: NucleusColumnValidation;
|
|
@@ -210,6 +225,24 @@ export interface NucleusConfigOptions {
|
|
|
210
225
|
maxUses?: number;
|
|
211
226
|
};
|
|
212
227
|
isMultiTenant?: boolean;
|
|
228
|
+
/**
|
|
229
|
+
* Collation applied to text-column sorts, e.g. `tr-TR-x-icu`.
|
|
230
|
+
*
|
|
231
|
+
* Postgres' default collation on a `C`/`POSIX` database orders by BYTE, so
|
|
232
|
+
* an "A-Z" list is not alphabetical in any language with letters outside
|
|
233
|
+
* ASCII. Measured on a Turkish catalogue: `ALBITROL` sorted before
|
|
234
|
+
* `Airtide` (uppercase letters sort before lowercase), and every name
|
|
235
|
+
* starting with ç/ğ/ı/ö/ş/ü landed after `z`. The list looked sorted and
|
|
236
|
+
* was not — the worst kind of wrong, because nothing about it invites a
|
|
237
|
+
* second look.
|
|
238
|
+
*
|
|
239
|
+
* Set the ICU collation for the install's language (`tr-TR-x-icu`,
|
|
240
|
+
* `de-DE-x-icu`, ...) and every entity sort on a text column uses it.
|
|
241
|
+
* Unset (default) keeps the database's own collation, so existing installs
|
|
242
|
+
* do not change order under them. An unknown collation is refused at boot
|
|
243
|
+
* rather than applied — see the entity route.
|
|
244
|
+
*/
|
|
245
|
+
sortCollation?: string;
|
|
213
246
|
schemas?: string[];
|
|
214
247
|
/**
|
|
215
248
|
* Deadline for the startup schema push (default 120000 ms).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nucleus-core-ts",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.115",
|
|
4
4
|
"description": "Production-ready, enterprise-grade TypeScript framework for building multi-tenant APIs",
|
|
5
5
|
"author": "Hidayet Can Özcan <hidayetcan@gmail.com>",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -273,3 +273,79 @@ describe('generateRelationsFile', () => {
|
|
|
273
273
|
expect(code).toContain('surveys: one(surveys')
|
|
274
274
|
})
|
|
275
275
|
})
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Postgres starts a new identity sequence at 1.
|
|
279
|
+
*
|
|
280
|
+
* On an empty table that is correct. On a table that already holds rows it
|
|
281
|
+
* hands the next insert a number those rows already carry — and identity adds
|
|
282
|
+
* no unique index, so nothing objects. Biltim's `five_s_findings` is the case:
|
|
283
|
+
* `finding_no` is NOT NULL with no default, nothing in the app has ever
|
|
284
|
+
* assigned it, and every attempt to record a finding was refused with
|
|
285
|
+
* "finding_no is required". Giving the column identity fixes the refusal, and
|
|
286
|
+
* would then have produced a second finding numbered 1 sitting beside the
|
|
287
|
+
* first, in a register whose Excel export orders by that number.
|
|
288
|
+
*/
|
|
289
|
+
describe('an identity column can say where its numbering resumes', () => {
|
|
290
|
+
const emit = (col: Record<string, unknown>) => generateColumnCode(col as never, [], 'findings')
|
|
291
|
+
|
|
292
|
+
it('still emits a bare identity when nothing is asked for', () => {
|
|
293
|
+
// The existing behaviour, unchanged: adding the option must not alter what
|
|
294
|
+
// every column that does not use it generates.
|
|
295
|
+
expect(emit({ name: 'finding_no', type: 'integer', generatedByDefaultAsIdentity: true })).toBe(
|
|
296
|
+
"integer('finding_no').generatedByDefaultAsIdentity()"
|
|
297
|
+
)
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('carries the starting value through to Drizzle', () => {
|
|
301
|
+
expect(
|
|
302
|
+
emit({
|
|
303
|
+
name: 'finding_no',
|
|
304
|
+
type: 'integer',
|
|
305
|
+
generatedByDefaultAsIdentity: true,
|
|
306
|
+
identity: { startWith: 3 },
|
|
307
|
+
})
|
|
308
|
+
).toContain('.generatedByDefaultAsIdentity({ startWith: 3 })')
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('carries the rest of the sequence options', () => {
|
|
312
|
+
const code = emit({
|
|
313
|
+
name: 'ticket_no',
|
|
314
|
+
type: 'integer',
|
|
315
|
+
generatedAlwaysAsIdentity: true,
|
|
316
|
+
identity: { startWith: 1000, increment: 10, minValue: 1, maxValue: 999999, cache: 5 },
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
expect(code).toContain('startWith: 1000')
|
|
320
|
+
expect(code).toContain('increment: 10')
|
|
321
|
+
expect(code).toContain('minValue: 1')
|
|
322
|
+
expect(code).toContain('maxValue: 999999')
|
|
323
|
+
expect(code).toContain('cache: 5')
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it('an identity column still does not repeat notNull', () => {
|
|
327
|
+
// Identity implies NOT NULL; emitting both produced a schema Drizzle
|
|
328
|
+
// rejected. The option must not reopen that.
|
|
329
|
+
const code = emit({
|
|
330
|
+
name: 'finding_no',
|
|
331
|
+
type: 'integer',
|
|
332
|
+
notNull: true,
|
|
333
|
+
generatedByDefaultAsIdentity: true,
|
|
334
|
+
identity: { startWith: 3 },
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
expect(code).not.toContain('.notNull()')
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('still applies a unique index when one is declared', () => {
|
|
341
|
+
const code = emit({
|
|
342
|
+
name: 'finding_no',
|
|
343
|
+
type: 'integer',
|
|
344
|
+
generatedByDefaultAsIdentity: true,
|
|
345
|
+
unique: true,
|
|
346
|
+
identity: { startWith: 3 },
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
expect(code).toContain('.unique()')
|
|
350
|
+
})
|
|
351
|
+
})
|
|
@@ -33,6 +33,14 @@ interface Column {
|
|
|
33
33
|
generatedAlwaysAs?: string
|
|
34
34
|
generatedByDefaultAsIdentity?: boolean
|
|
35
35
|
generatedAlwaysAsIdentity?: boolean
|
|
36
|
+
identity?: {
|
|
37
|
+
startWith?: number
|
|
38
|
+
increment?: number
|
|
39
|
+
minValue?: number
|
|
40
|
+
maxValue?: number
|
|
41
|
+
cache?: number
|
|
42
|
+
cycle?: boolean
|
|
43
|
+
}
|
|
36
44
|
comment?: string
|
|
37
45
|
check?: string
|
|
38
46
|
sensitive?: boolean
|
|
@@ -228,6 +236,30 @@ export function selectEmittedTables<T extends { table_name: string }>(
|
|
|
228
236
|
)
|
|
229
237
|
}
|
|
230
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Identity sequence options, rendered for Drizzle.
|
|
241
|
+
*
|
|
242
|
+
* Postgres starts a new identity sequence at 1. On an empty table that is
|
|
243
|
+
* right; on a table that already holds rows it is not, and nothing says so —
|
|
244
|
+
* the next insert is handed 1 again and sits alongside the row that already
|
|
245
|
+
* carries it. Without a unique index (identity does not add one) there is no
|
|
246
|
+
* error, just two records wearing the same number and a report that orders by
|
|
247
|
+
* it. So a column adopting identity over existing data has to be able to say
|
|
248
|
+
* where the numbering resumes.
|
|
249
|
+
*/
|
|
250
|
+
function identityOptions(col: Column): string {
|
|
251
|
+
const id = col.identity
|
|
252
|
+
if (!id) return ''
|
|
253
|
+
const parts: string[] = []
|
|
254
|
+
if (id.startWith !== undefined) parts.push(`startWith: ${id.startWith}`)
|
|
255
|
+
if (id.increment !== undefined) parts.push(`increment: ${id.increment}`)
|
|
256
|
+
if (id.minValue !== undefined) parts.push(`minValue: ${id.minValue}`)
|
|
257
|
+
if (id.maxValue !== undefined) parts.push(`maxValue: ${id.maxValue}`)
|
|
258
|
+
if (id.cache !== undefined) parts.push(`cache: ${id.cache}`)
|
|
259
|
+
if (id.cycle !== undefined) parts.push(`cycle: ${id.cycle}`)
|
|
260
|
+
return parts.length ? `{ ${parts.join(', ')} }` : ''
|
|
261
|
+
}
|
|
262
|
+
|
|
231
263
|
export function generateColumnCode(
|
|
232
264
|
col: Column,
|
|
233
265
|
allTables: string[],
|
|
@@ -260,12 +292,12 @@ export function generateColumnCode(
|
|
|
260
292
|
if (col.primaryKey) code += '.primaryKey()'
|
|
261
293
|
|
|
262
294
|
if (col.generatedByDefaultAsIdentity) {
|
|
263
|
-
code +=
|
|
295
|
+
code += `.generatedByDefaultAsIdentity(${identityOptions(col)})`
|
|
264
296
|
if (col.unique) code += '.unique()'
|
|
265
297
|
return code
|
|
266
298
|
}
|
|
267
299
|
if (col.generatedAlwaysAsIdentity) {
|
|
268
|
-
code +=
|
|
300
|
+
code += `.generatedAlwaysAsIdentity(${identityOptions(col)})`
|
|
269
301
|
if (col.unique) code += '.unique()'
|
|
270
302
|
return code
|
|
271
303
|
}
|