prostgles-server 2.0.183 → 2.0.186

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.
@@ -1,63 +1,90 @@
1
- import { getKeys, asName } from "prostgles-types";
2
- import { JoinInfo } from "./DboBuilder";
1
+ import { doesNotMatch } from "assert";
2
+ import { getKeys, asName, AnyObject, TableInfo } from "prostgles-types";
3
+ import { DboBuilder, isPlainObject, JoinInfo } from "./DboBuilder";
3
4
  import { ALLOWED_EXTENSION, ALLOWED_CONTENT_TYPE } from "./FileManager";
4
5
  import { DB, DBHandlerServer, Prostgles } from "./Prostgles";
5
6
  import { asValue } from "./PubSubManager";
6
7
 
7
8
  type ColExtraInfo = {
8
- min?: string | number;
9
- max?: string | number;
10
- hint?: string;
9
+ min?: string | number;
10
+ max?: string | number;
11
+ hint?: string;
11
12
  };
12
13
 
13
- type BaseTableDefinition = {
14
- dropIfExistsCascade?: boolean;
15
- dropIfExists?: boolean;
14
+ export type I18N_Config<LANG_IDS> = {
15
+ [lang_id in keyof LANG_IDS]: string;
16
+ }
17
+
18
+ export const parseI18N = <LANG_IDS, Def extends string | undefined>(params: {
19
+ config?: I18N_Config<LANG_IDS> | string;
20
+ lang?: keyof LANG_IDS | string;
21
+ defaultLang: keyof LANG_IDS | string;
22
+ defaultValue: Def;
23
+ }): Def | string => {
24
+ const { config, lang, defaultLang, defaultValue } = params;
25
+ if(config){
26
+ if(isPlainObject(config)){
27
+ //@ts-ignore
28
+ return config[lang] ?? config[defaultLang];
29
+ } else if(typeof config === "string"){
30
+ return config;
31
+ }
32
+ }
33
+
34
+ return defaultValue;
35
+ }
36
+
37
+ type BaseTableDefinition<LANG_IDS = AnyObject> = {
38
+ info?: {
39
+ label?: string | I18N_Config<LANG_IDS>;
40
+ }
41
+ dropIfExistsCascade?: boolean;
42
+ dropIfExists?: boolean;
16
43
  }
17
44
 
18
45
  type LookupTableDefinition<LANG_IDS> = {
19
- isLookupTable: {
20
- values: {
21
- [id_value: string]: {} | {
22
- [lang_id in keyof LANG_IDS]: string
23
- }
24
- }
46
+ isLookupTable: {
47
+ values: {
48
+ [id_value: string]: {} | {
49
+ [lang_id in keyof LANG_IDS]: string
50
+ }
25
51
  }
52
+ }
26
53
  }
27
54
 
28
55
  type BaseColumn<LANG_IDS> = {
29
- /**
30
- * Will add these values to .getColumns() result
31
- */
32
- info?: ColExtraInfo;
56
+ /**
57
+ * Will add these values to .getColumns() result
58
+ */
59
+ info?: ColExtraInfo;
33
60
 
34
- label?: string | Partial<{ [lang_id in keyof LANG_IDS]: string; }>;
61
+ label?: string | Partial<{ [lang_id in keyof LANG_IDS]: string; }>;
35
62
  }
36
63
 
37
64
  type SQLDefColumn = {
38
65
 
39
- /**
40
- * Raw sql statement used in creating/adding column
41
- */
42
- sqlDefinition?: string;
66
+ /**
67
+ * Raw sql statement used in creating/adding column
68
+ */
69
+ sqlDefinition?: string;
43
70
  }
44
71
 
45
72
  type TextColDef = {
46
- defaultValue?: string;
47
- nullable?: boolean;
73
+ defaultValue?: string;
74
+ nullable?: boolean;
48
75
  }
49
76
 
50
77
  type TextColumn = TextColDef & {
51
- isText: true;
52
- /**
53
- * Value will be trimmed before update/insert
54
- */
55
- trimmed?: boolean;
56
-
57
- /**
58
- * Value will be lower cased before update/insert
59
- */
60
- lowerCased?: boolean;
78
+ isText: true;
79
+ /**
80
+ * Value will be trimmed before update/insert
81
+ */
82
+ trimmed?: boolean;
83
+
84
+ /**
85
+ * Value will be lower cased before update/insert
86
+ */
87
+ lowerCased?: boolean;
61
88
  }
62
89
 
63
90
  /**
@@ -65,364 +92,390 @@ type TextColumn = TextColDef & {
65
92
  * Requires this table to have a primary key AND a valid fileTable config
66
93
  */
67
94
  type MediaColumn = ({
68
-
69
- name: string;
70
- label?: string;
71
- files: "one" | "many";
95
+
96
+ name: string;
97
+ label?: string;
98
+ files: "one" | "many";
72
99
  } & (
73
100
  {
74
101
 
75
- /**
76
- * https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept
77
- */
78
- allowedContentType?: Record<Partial<("audio/*" | "video/*" | "image/*" | "text/*" | ALLOWED_CONTENT_TYPE)>, 1>
102
+ /**
103
+ * https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept
104
+ */
105
+ allowedContentType?: Record<Partial<("audio/*" | "video/*" | "image/*" | "text/*" | ALLOWED_CONTENT_TYPE)>, 1>
79
106
  } |
80
- {
81
- allowedExtensions?: Record<Partial<ALLOWED_EXTENSION>, 1>
107
+ {
108
+ allowedExtensions?: Record<Partial<ALLOWED_EXTENSION>, 1>
82
109
  }
83
- ));
110
+ ));
84
111
 
85
112
  type ReferencedColumn = {
86
113
 
87
- /**
88
- * Will create a lookup table that this column will reference
89
- */
90
- references?: TextColDef & {
114
+ /**
115
+ * Will create a lookup table that this column will reference
116
+ */
117
+ references?: TextColDef & {
91
118
 
92
119
 
93
- tableName: string;
120
+ tableName: string;
94
121
 
95
- /**
96
- * Defaults to id
97
- */
98
- columnName?: string;
99
- }
122
+ /**
123
+ * Defaults to id
124
+ */
125
+ columnName?: string;
126
+ }
100
127
  }
101
128
 
102
129
  type JoinDef = {
103
- sourceTable: string;
104
- targetTable: string;
130
+ sourceTable: string;
131
+ targetTable: string;
105
132
 
106
- /**
107
- * E.g.: [sourceCol: string, targetCol: string][];
108
- */
109
- on: [string, string][];
133
+ /**
134
+ * E.g.: [sourceCol: string, targetCol: string][];
135
+ */
136
+ on: [string, string][];
110
137
  }
111
138
 
112
139
  /**
113
140
  * Used in specifying a join path to a table. This column name can then be used in select
114
141
  */
115
142
  type NamedJoinColumn = {
116
- label?: string;
117
- joinDef: JoinDef[];
143
+ label?: string;
144
+ joinDef: JoinDef[];
118
145
  }
119
146
 
120
147
  type ColumnConfig<LANG_IDS = { en: 1 }> = NamedJoinColumn | MediaColumn | (BaseColumn<LANG_IDS> & (SQLDefColumn | ReferencedColumn | TextColumn))
121
148
 
122
149
  type TableDefinition<LANG_IDS> = {
123
- columns: {
124
- [column_name: string]: ColumnConfig<LANG_IDS>
125
- },
126
- constraints?: {
127
- [constraint_name: string]: string
128
- },
129
-
130
- /**
131
- * Similar to unique constraints but expressions are allowed inside definition
132
- */
133
- replaceUniqueIndexes?: boolean;
134
- indexes?: {
135
- [index_name: string]: {
136
-
137
- /**
138
- * Overrides replaceUniqueIndexes
139
- */
140
- replace?: boolean;
141
-
142
- /**
143
- * Causes the system to check for duplicate values in the table when the index is created (if data already exist) and each time data is added.
144
- * Attempts to insert or update data which would result in duplicate entries will generate an error.
145
- */
146
- unique?: boolean;
147
-
148
- /**
149
- * When this option is used, PostgreSQL will build the index without taking any locks that prevent
150
- * concurrent inserts, updates, or deletes on the table; whereas a standard index build locks out writes (but not reads) on the table until it's done.
151
- * There are several caveats to be aware of when using this option — see Building Indexes Concurrently.
152
- */
153
- concurrently?: boolean;
154
-
155
- /**
156
- * Table name
157
- */
158
- // on?: string;
159
-
160
- /**
161
- * Raw sql statement used excluding parentheses. e.g.: column_name
162
- */
163
- definition: string;
164
-
165
- /**
166
- * The name of the index method to be used.
167
- * Choices are btree, hash, gist, and gin. The default method is btree.
168
- */
169
- using?: "btree" | "hash" | "gist" | "gin"
170
- }
150
+ columns?: {
151
+ [column_name: string]: ColumnConfig<LANG_IDS>
152
+ },
153
+ constraints?: {
154
+ [constraint_name: string]: string
155
+ },
156
+
157
+ /**
158
+ * Similar to unique constraints but expressions are allowed inside definition
159
+ */
160
+ replaceUniqueIndexes?: boolean;
161
+ indexes?: {
162
+ [index_name: string]: {
163
+
164
+ /**
165
+ * Overrides replaceUniqueIndexes
166
+ */
167
+ replace?: boolean;
168
+
169
+ /**
170
+ * Causes the system to check for duplicate values in the table when the index is created (if data already exist) and each time data is added.
171
+ * Attempts to insert or update data which would result in duplicate entries will generate an error.
172
+ */
173
+ unique?: boolean;
174
+
175
+ /**
176
+ * When this option is used, PostgreSQL will build the index without taking any locks that prevent
177
+ * concurrent inserts, updates, or deletes on the table; whereas a standard index build locks out writes (but not reads) on the table until it's done.
178
+ * There are several caveats to be aware of when using this option — see Building Indexes Concurrently.
179
+ */
180
+ concurrently?: boolean;
181
+
182
+ /**
183
+ * Table name
184
+ */
185
+ // on?: string;
186
+
187
+ /**
188
+ * Raw sql statement used excluding parentheses. e.g.: column_name
189
+ */
190
+ definition: string;
191
+
192
+ /**
193
+ * The name of the index method to be used.
194
+ * Choices are btree, hash, gist, and gin. The default method is btree.
195
+ */
196
+ using?: "btree" | "hash" | "gist" | "gin"
171
197
  }
198
+ }
172
199
  }
173
200
 
174
201
  /**
175
202
  * Helper utility to create lookup tables for TEXT columns
176
203
  */
177
204
  export type TableConfig<LANG_IDS = { en: 1 }> = {
178
- [table_name: string]: BaseTableDefinition & (TableDefinition<LANG_IDS> | LookupTableDefinition<LANG_IDS>);
205
+ [table_name: string]: BaseTableDefinition<LANG_IDS> & (TableDefinition<LANG_IDS> | LookupTableDefinition<LANG_IDS>);
179
206
  }
180
207
 
181
208
  /**
182
209
  * Will be run between initSQL and fileTable
183
210
  */
184
- export default class TableConfigurator {
185
-
186
- config?: TableConfig;
187
- get dbo(): DBHandlerServer {
188
- if(!this.prostgles.dbo) throw "this.prostgles.dbo missing"
189
- return this.prostgles.dbo
190
- };
191
- get db(): DB {
192
- if(!this.prostgles.db) throw "this.prostgles.db missing"
193
- return this.prostgles.db
194
- };
195
- // sidKeyName: string;
196
- prostgles: Prostgles
197
-
198
- constructor(prostgles: Prostgles){
199
- this.config = prostgles.opts.tableConfig;
200
- this.prostgles = prostgles;
211
+ export default class TableConfigurator<LANG_IDS = { en: 1 }> {
212
+
213
+ config?: TableConfig<LANG_IDS>;
214
+ get dbo(): DBHandlerServer {
215
+ if (!this.prostgles.dbo) throw "this.prostgles.dbo missing"
216
+ return this.prostgles.dbo
217
+ };
218
+ get db(): DB {
219
+ if (!this.prostgles.db) throw "this.prostgles.db missing"
220
+ return this.prostgles.db
221
+ };
222
+ // sidKeyName: string;
223
+ prostgles: Prostgles
224
+
225
+ constructor(prostgles: Prostgles) {
226
+ this.config = prostgles.opts.tableConfig as any;
227
+ this.prostgles = prostgles;
228
+ }
229
+
230
+ getColumnConfig = (tableName: string, colName: string): ColumnConfig | undefined => {
231
+ const tconf = this.config?.[tableName];
232
+ if (tconf && "columns" in tconf) {
233
+ return tconf.columns?.[colName];
201
234
  }
235
+ return undefined;
236
+ }
202
237
 
203
- getColumnConfig = (tableName: string, colName: string): ColumnConfig | undefined => {
204
- const tconf = this.config?.[tableName];
205
- if(tconf && "columns" in tconf){
206
- return tconf.columns[colName];
207
- }
208
- return undefined;
238
+ getTableInfo = (params: { tableName: string; lang?: string }): TableInfo["info"] | undefined => {
239
+ const tconf = this.config?.[params.tableName];
240
+
241
+ return {
242
+ label: parseI18N<LANG_IDS, string>({ config: tconf?.info?.label, lang: params.lang, defaultLang: "en", defaultValue: params.tableName })
209
243
  }
244
+ }
210
245
 
211
- getColInfo = (params: {col: string, table: string, lang?: string }): (ColExtraInfo & { label?: string }) | undefined => {
212
- const colConf = this.getColumnConfig(params.table, params.col);
213
- let result: (ColExtraInfo & { label?: string }) | undefined = undefined;
214
- if(colConf){
215
-
216
- if("info" in colConf){
217
- result = {
218
- ...(result ?? {}),
219
- ...colConf?.info
220
- }
221
- }
246
+ getColInfo = (params: { col: string, table: string, lang?: string }): (ColExtraInfo & { label?: string }) | undefined => {
247
+ const colConf = this.getColumnConfig(params.table, params.col);
248
+ let result: (ColExtraInfo & { label?: string }) | undefined = undefined;
249
+ if (colConf) {
222
250
 
223
- /**
224
- * Get labels from TableConfig if specified
225
- */
226
- if(colConf.label){
227
- const { lang } = params;
228
- const lbl = colConf?.label;
229
- if(["string", "object"].includes(typeof lbl)){
230
- if(typeof lbl === "string") {
231
- result ??= {};
232
- result.label = lbl
233
- } else if(lang && (lbl?.[lang as "en"] || lbl?.en)) {
234
- result ??= {};
235
- result.label = (lbl?.[lang as "en"]) || lbl?.en;
236
- }
237
- }
238
-
239
- }
251
+ if ("info" in colConf) {
252
+ result = {
253
+ ...(result ?? {}),
254
+ ...colConf?.info
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Get labels from TableConfig if specified
260
+ */
261
+ if (colConf.label) {
262
+ const { lang } = params;
263
+ const lbl = colConf?.label;
264
+ if (["string", "object"].includes(typeof lbl)) {
265
+ if (typeof lbl === "string") {
266
+ result ??= {};
267
+ result.label = lbl
268
+ } else if (lang && (lbl?.[lang as "en"] || lbl?.en)) {
269
+ result ??= {};
270
+ result.label = (lbl?.[lang as "en"]) || lbl?.en;
271
+ }
240
272
  }
241
273
 
242
-
243
- return result;
274
+ }
244
275
  }
245
276
 
246
- checkColVal = (params: {col: string, table: string, value: any }): void => {
247
- const conf = this.getColInfo(params);
248
- if(conf){
249
- const { value } = params;
250
- const { min, max } = conf;
251
- if(min !== undefined && value !== undefined && value < min) throw `${params.col} must be less than ${min}`
252
- if(max !== undefined && value !== undefined && value > max) throw `${params.col} must be greater than ${max}`
253
- }
254
- }
255
277
 
256
- getJoinInfo = (sourceTable: string, targetTable: string): JoinInfo | undefined => {
257
- if(
258
- this.config &&
259
- sourceTable in this.config &&
260
- this.config[sourceTable] &&
261
- "columns" in this.config[sourceTable]
262
- ){
263
- const td = this.config[sourceTable];
264
- if("columns" in td && td.columns[targetTable]){
265
- const cd = td.columns[targetTable];
266
- if("joinDef" in cd){
267
- const { joinDef } = cd;
268
- const res: JoinInfo = {
269
- expectOne: false,
270
- paths: joinDef.map(({ sourceTable, targetTable: table, on }) => ({
271
- source: sourceTable,
272
- target: targetTable,
273
- table,
274
- on
275
- })),
276
- }
277
-
278
- return res;
279
- }
280
- }
278
+ return result;
279
+ }
280
+
281
+ checkColVal = (params: { col: string, table: string, value: any }): void => {
282
+ const conf = this.getColInfo(params);
283
+ if (conf) {
284
+ const { value } = params;
285
+ const { min, max } = conf;
286
+ if (min !== undefined && value !== undefined && value < min) throw `${params.col} must be less than ${min}`
287
+ if (max !== undefined && value !== undefined && value > max) throw `${params.col} must be greater than ${max}`
288
+ }
289
+ }
290
+
291
+ getJoinInfo = (sourceTable: string, targetTable: string): JoinInfo | undefined => {
292
+ if (
293
+ this.config &&
294
+ sourceTable in this.config &&
295
+ this.config[sourceTable] &&
296
+ "columns" in this.config[sourceTable]
297
+ ) {
298
+ const td = this.config[sourceTable];
299
+ if ("columns" in td && td.columns?.[targetTable]) {
300
+ const cd = td.columns[targetTable];
301
+ if ("joinDef" in cd) {
302
+ const { joinDef } = cd;
303
+ const res: JoinInfo = {
304
+ expectOne: false,
305
+ paths: joinDef.map(({ sourceTable, targetTable: table, on }) => ({
306
+ source: sourceTable,
307
+ target: targetTable,
308
+ table,
309
+ on
310
+ })),
311
+ }
312
+
313
+ return res;
281
314
  }
282
- return undefined;
315
+ }
283
316
  }
284
-
285
- async init(){
286
- let queries: string[] = [];
287
-
288
- if(!this.config || !this.prostgles.pgp) throw "config or pgp missing"
289
- /* Create lookup tables */
290
- Object.keys(this.config).map(tableName => {
291
- const tableConf = this.config![tableName];
292
- const { dropIfExists = false, dropIfExistsCascade = false } = tableConf;
293
- if(dropIfExistsCascade){
294
- queries.push(`DROP TABLE IF EXISTS ${tableName} CASCADE;`);
295
- } else if(dropIfExists){
296
- queries.push(`DROP TABLE IF EXISTS ${tableName} ;`);
297
- }
298
- if("isLookupTable" in tableConf && Object.keys(tableConf.isLookupTable?.values).length){
299
- const rows = Object.keys(tableConf.isLookupTable?.values).map(id => ({ id, ...(tableConf.isLookupTable?.values[id]) }));
300
- if(dropIfExists || dropIfExistsCascade || !this.dbo?.[tableName]){
301
- const keys = Object.keys(rows[0]).filter(k => k !== "id");
302
- queries.push(`CREATE TABLE IF NOT EXISTS ${tableName} (
317
+ return undefined;
318
+ }
319
+
320
+ async init() {
321
+ let queries: string[] = [];
322
+
323
+ if (!this.config || !this.prostgles.pgp) throw "config or pgp missing"
324
+ /* Create lookup tables */
325
+ Object.keys(this.config).map(tableName => {
326
+ const tableConf = this.config![tableName];
327
+ const { dropIfExists = false, dropIfExistsCascade = false } = tableConf;
328
+ if (dropIfExistsCascade) {
329
+ queries.push(`DROP TABLE IF EXISTS ${tableName} CASCADE;`);
330
+ } else if (dropIfExists) {
331
+ queries.push(`DROP TABLE IF EXISTS ${tableName} ;`);
332
+ }
333
+ if ("isLookupTable" in tableConf && Object.keys(tableConf.isLookupTable?.values).length) {
334
+ const rows = Object.keys(tableConf.isLookupTable?.values).map(id => ({ id, ...(tableConf.isLookupTable?.values[id]) }));
335
+ if (dropIfExists || dropIfExistsCascade || !this.dbo?.[tableName]) {
336
+ const keys = Object.keys(rows[0]).filter(k => k !== "id");
337
+ queries.push(`CREATE TABLE IF NOT EXISTS ${tableName} (
303
338
  id TEXT PRIMARY KEY
304
- ${keys.length? (", " + keys.map(k => asName(k) + " TEXT ").join(", ")) : ""}
339
+ ${keys.length ? (", " + keys.map(k => asName(k) + " TEXT ").join(", ")) : ""}
305
340
  );`);
306
341
 
307
- rows.map(row => {
308
- const values = this.prostgles.pgp!.helpers.values(row)
309
- queries.push(this.prostgles.pgp!.as.format(`INSERT INTO ${tableName} (${["id", ...keys].map(t => asName(t)).join(", ")}) ` + " VALUES ${values:raw} ;", { values} ))
310
- });
311
- // console.log("Created lookup table " + tableName)
312
- }
342
+ rows.map(row => {
343
+ const values = this.prostgles.pgp!.helpers.values(row)
344
+ queries.push(this.prostgles.pgp!.as.format(`INSERT INTO ${tableName} (${["id", ...keys].map(t => asName(t)).join(", ")}) ` + " VALUES ${values:raw} ;", { values }))
345
+ });
346
+ // console.log("Created lookup table " + tableName)
347
+ }
348
+ }
349
+ });
350
+
351
+ if (queries.length) {
352
+ const q = queries.join("\n");
353
+ console.log("TableConfig: \n", q)
354
+ await this.db.multi(q);
355
+ await this.prostgles.refreshDBO()
356
+ }
357
+ queries = [];
358
+
359
+ /* Create referenced columns */
360
+ await Promise.all(Object.keys(this.config).map(async tableName => {
361
+ const tableConf = this.config![tableName];
362
+ if ("columns" in tableConf) {
363
+ const getColDef = (name: string, colConf: ColumnConfig): string => {
364
+ const colNameEsc = asName(name);
365
+ const getTextDef = (colConf: TextColDef) => {
366
+ const { nullable, defaultValue } = colConf;
367
+ return ` TEXT ${!nullable ? " NOT NULL " : ""} ${defaultValue ? ` DEFAULT ${asValue(defaultValue)} ` : ""}`
368
+ }
369
+ if ("references" in colConf && colConf.references) {
370
+
371
+ const { tableName: lookupTable, columnName: lookupCol = "id" } = colConf.references;
372
+ return ` ${colNameEsc} ${getTextDef(colConf.references)} REFERENCES ${lookupTable} (${lookupCol}) `;
373
+
374
+ } else if ("sqlDefinition" in colConf && colConf.sqlDefinition) {
375
+
376
+ return ` ${colNameEsc} ${colConf.sqlDefinition} `;
377
+
378
+ } else if ("isText" in colConf && colConf.isText) {
379
+ let checks = "", cArr = [];
380
+ if (colConf.lowerCased) {
381
+ cArr.push(`${colNameEsc} = LOWER(${colNameEsc})`)
313
382
  }
314
- });
315
-
316
- if(queries.length){
317
- const q = queries.join("\n");
318
- console.log("TableConfig: \n", q)
319
- await this.db.multi(q);
320
- await this.prostgles.refreshDBO()
383
+ if (colConf.trimmed) {
384
+ cArr.push(`${colNameEsc} = BTRIM(${colNameEsc})`)
385
+ }
386
+ if (cArr.length) {
387
+ checks = `CHECK (${cArr.join(" AND ")})`
388
+ }
389
+ return ` ${colNameEsc} ${getTextDef(colConf)} ${checks}`;
390
+ } else {
391
+ throw "Unknown column config: " + JSON.stringify(colConf);
392
+ }
321
393
  }
322
- queries = [];
323
-
324
- /* Create referenced columns */
325
- await Promise.all(Object.keys(this.config).map(async tableName => {
326
- const tableConf = this.config![tableName];
327
- if("columns" in tableConf){
328
- const getColDef = (name: string, colConf: ColumnConfig): string => {
329
- const colNameEsc = asName(name);
330
- const getTextDef = (colConf: TextColDef) => {
331
- const { nullable, defaultValue } = colConf;
332
- return ` TEXT ${!nullable? " NOT NULL " : ""} ${defaultValue? ` DEFAULT ${asValue(defaultValue)} ` : "" }`
333
- }
334
- if("references" in colConf && colConf.references){
335
-
336
- const { tableName: lookupTable, columnName: lookupCol = "id" } = colConf.references;
337
- return ` ${colNameEsc} ${getTextDef(colConf.references)} REFERENCES ${lookupTable} (${lookupCol}) `;
338
-
339
- } else if("sqlDefinition" in colConf && colConf.sqlDefinition){
340
-
341
- return ` ${colNameEsc} ${colConf.sqlDefinition} `;
342
-
343
- } else if("isText" in colConf && colConf.isText){
344
- let checks = "", cArr = [];
345
- if(colConf.lowerCased){
346
- cArr.push(`${colNameEsc} = LOWER(${colNameEsc})`)
347
- }
348
- if(colConf.trimmed){
349
- cArr.push(`${colNameEsc} = BTRIM(${colNameEsc})`)
350
- }
351
- if(cArr.length){
352
- checks = `CHECK (${cArr.join(" AND ")})`
353
- }
354
- return ` ${colNameEsc} ${getTextDef(colConf)} ${checks}`;
355
- } else {
356
- throw "Unknown column config: " + JSON.stringify(colConf);
357
- }
358
- }
359
-
360
- const colDefs: string[] = [];
361
- Object.keys(tableConf.columns).filter(c => !("joinDef" in tableConf.columns[c])).map(colName => {
362
- const colConf = tableConf.columns[colName];
363
-
364
- if(!this.dbo[tableName]){
365
- colDefs.push(getColDef(colName, colConf))
366
- } else if(!colDefs.length && !this.dbo[tableName].columns?.find(c => colName === c.name)) {
367
-
368
- if("references" in colConf && colConf.references){
369
-
370
- const { tableName: lookupTable, } = colConf.references;
371
- queries.push(`
372
- ALTER TABLE ${asName(tableName)}
373
- ADD COLUMN ${getColDef(colName, colConf)};
374
- `)
375
- console.log(`TableConfigurator: ${tableName}(${colName})` + " referenced lookup table " + lookupTable);
376
394
 
377
- } else if("sqlDefinition" in colConf && colConf.sqlDefinition){
378
-
379
- queries.push(`
395
+ const colDefs: string[] = [];
396
+ if (tableConf.columns) {
397
+ getKeys(tableConf?.columns).filter(c => !("joinDef" in tableConf.columns![c])).map(colName => {
398
+ const colConf = tableConf.columns![colName];
399
+
400
+ if (!this.dbo[tableName]) {
401
+ colDefs.push(getColDef(colName, colConf))
402
+ } else if (!colDefs.length && !this.dbo[tableName].columns?.find(c => colName === c.name)) {
403
+
404
+ if ("references" in colConf && colConf.references) {
405
+
406
+ const { tableName: lookupTable, } = colConf.references;
407
+ queries.push(`
408
+ ALTER TABLE ${asName(tableName)}
409
+ ADD COLUMN ${getColDef(colName, colConf)};
410
+ `)
411
+ console.log(`TableConfigurator: ${tableName}(${colName})` + " referenced lookup table " + lookupTable);
412
+
413
+ } else if ("sqlDefinition" in colConf && colConf.sqlDefinition) {
414
+
415
+ queries.push(`
380
416
  ALTER TABLE ${asName(tableName)}
381
417
  ADD COLUMN ${getColDef(colName, colConf)};
382
418
  `)
383
- console.log(`TableConfigurator: created/added column ${tableName}(${colName}) ` + colConf.sqlDefinition)
384
- }
385
- }
386
- });
419
+ console.log(`TableConfigurator: created/added column ${tableName}(${colName}) ` + colConf.sqlDefinition)
420
+ }
421
+ }
422
+ });
423
+ }
387
424
 
388
- if(colDefs.length){
389
- queries.push(`CREATE TABLE ${asName(tableName)} (
425
+ if (colDefs.length) {
426
+ queries.push(`CREATE TABLE ${asName(tableName)} (
390
427
  ${colDefs.join(", \n")}
391
428
  );`)
392
- console.error("TableConfigurator: Created table: \n" + queries[0])
393
- }
394
- }
395
- if("constraints" in tableConf && tableConf.constraints){
396
- getKeys(tableConf.constraints).map(constraintName => {
397
- queries.push(`ALTER TABLE ${asName(tableName)} ADD CONSTRAINT ${asName(constraintName)} ${tableConf.constraints![constraintName]} ;`);
398
- });
399
- }
400
- if("indexes" in tableConf && tableConf.indexes){
401
- getKeys(tableConf.indexes).map(indexName => {
402
- const { concurrently, unique, using, definition, replace } = tableConf.indexes![indexName];
403
- if(replace || typeof replace !== "boolean" && tableConf.replaceUniqueIndexes){
404
- queries.push(`DROP INDEX IF EXISTS ${asName(indexName)} ;`);
405
- }
406
- queries.push(`CREATE ${unique? "UNIQUE" : ""} ${!concurrently? "" : "CONCURRENTLY"} INDEX ${asName(indexName)} ON ${asName(tableName)} ${!using? "" : ("USING " + using)} (${definition}) ;`);
407
- });
408
- }
409
- }));
410
-
411
- if(queries.length){
412
- const q = queries.join("\n");
413
- console.log("TableConfig: \n", q)
414
- await this.db.multi(q);
429
+ console.error("TableConfigurator: Created table: \n" + queries[0])
415
430
  }
431
+ }
432
+ if ("constraints" in tableConf && tableConf.constraints) {
433
+ const constraints = await getTableConstraings(this.db, tableName);
434
+ getKeys(tableConf.constraints).map(constraintName => {
435
+ if(!constraints.some(c => c.conname === constraintName)){
436
+ queries.push(`ALTER TABLE ${asName(tableName)} ADD CONSTRAINT ${asName(constraintName)} ${tableConf.constraints![constraintName]} ;`);
437
+ }
438
+ });
439
+ }
440
+ if ("indexes" in tableConf && tableConf.indexes) {
441
+ getKeys(tableConf.indexes).map(indexName => {
442
+ const { concurrently, unique, using, definition, replace } = tableConf.indexes![indexName];
443
+ if (replace || typeof replace !== "boolean" && tableConf.replaceUniqueIndexes) {
444
+ queries.push(`DROP INDEX IF EXISTS ${asName(indexName)} ;`);
445
+ }
446
+ queries.push(`CREATE ${unique ? "UNIQUE" : ""} ${!concurrently ? "" : "CONCURRENTLY"} INDEX ${asName(indexName)} ON ${asName(tableName)} ${!using ? "" : ("USING " + using)} (${definition}) ;`);
447
+ });
448
+ }
449
+ }));
450
+
451
+ if (queries.length) {
452
+ const q = queries.join("\n");
453
+ console.log("TableConfig: \n", q)
454
+ await this.db.multi(q);
416
455
  }
456
+ }
417
457
  }
418
458
 
419
459
 
420
- async function columnExists(args: {tableName: string; colName: string; db: DB }){
421
- const { db, tableName, colName } = args;
422
- return Boolean((await db.oneOrNone(`
460
+ async function columnExists(args: { tableName: string; colName: string; db: DB }) {
461
+ const { db, tableName, colName } = args;
462
+ return Boolean((await db.oneOrNone(`
423
463
  SELECT column_name, table_name
424
464
  FROM information_schema.columns
425
465
  WHERE table_name=${asValue(tableName)} and column_name=${asValue(colName)}
426
466
  LIMIT 1;
427
467
  `))?.column_name);
468
+ }
469
+
470
+ function getTableConstraings(db: DB, tableName: string): Promise<{ oid: number; conname: string; definition: string; }[]>{
471
+ return db.any(`
472
+ SELECT con.*, pg_get_constraintdef(con.oid)
473
+ FROM pg_catalog.pg_constraint con
474
+ INNER JOIN pg_catalog.pg_class rel
475
+ ON rel.oid = con.conrelid
476
+ INNER JOIN pg_catalog.pg_namespace nsp
477
+ ON nsp.oid = connamespace
478
+ WHERE 1=1
479
+ AND nsp.nspname = current_schema
480
+ AND rel.relname = ` + "${tableName}", { tableName })
428
481
  }