prostgles-server 2.0.228 → 2.0.231

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.
Files changed (39) hide show
  1. package/dist/DBSchemaBuilder.d.ts.map +1 -1
  2. package/dist/DBSchemaBuilder.js +10 -1
  3. package/dist/DBSchemaBuilder.js.map +1 -1
  4. package/dist/FileManager.d.ts +8 -1
  5. package/dist/FileManager.d.ts.map +1 -1
  6. package/dist/FileManager.js +29 -19
  7. package/dist/FileManager.js.map +1 -1
  8. package/dist/TableConfig.d.ts +5 -1
  9. package/dist/TableConfig.d.ts.map +1 -1
  10. package/dist/TableConfig.js +27 -23
  11. package/dist/TableConfig.js.map +1 -1
  12. package/dist/validation.d.ts +50 -0
  13. package/dist/validation.d.ts.map +1 -0
  14. package/dist/validation.js +140 -0
  15. package/dist/validation.js.map +1 -0
  16. package/lib/DBSchemaBuilder.d.ts.map +1 -1
  17. package/lib/DBSchemaBuilder.js +10 -1
  18. package/lib/DBSchemaBuilder.ts +10 -1
  19. package/lib/FileManager.d.ts +8 -1
  20. package/lib/FileManager.d.ts.map +1 -1
  21. package/lib/FileManager.js +29 -19
  22. package/lib/FileManager.ts +35 -20
  23. package/lib/TableConfig.d.ts +5 -1
  24. package/lib/TableConfig.d.ts.map +1 -1
  25. package/lib/TableConfig.js +27 -23
  26. package/lib/TableConfig.ts +36 -26
  27. package/lib/validation.d.ts +50 -0
  28. package/lib/validation.d.ts.map +1 -0
  29. package/lib/validation.js +139 -0
  30. package/lib/validation.ts +163 -0
  31. package/package.json +2 -1
  32. package/tests/client/PID.txt +1 -1
  33. package/tests/isomorphic_queries.d.ts.map +1 -1
  34. package/tests/isomorphic_queries.js +25 -0
  35. package/tests/isomorphic_queries.ts +28 -1
  36. package/tests/server/DBoGenerated.d.ts +139 -122
  37. package/tests/server/index.js +13 -0
  38. package/tests/server/index.ts +13 -0
  39. package/tests/server/package-lock.json +3 -1
@@ -2,6 +2,7 @@ import { getKeys, asName, AnyObject, TableInfo, ALLOWED_EXTENSION, ALLOWED_CONT
2
2
  import { isPlainObject, JoinInfo } from "./DboBuilder";
3
3
  import { DB, DBHandlerServer, Joins, Prostgles } from "./Prostgles";
4
4
  import { asValue } from "./PubSubManager";
5
+ import { getPGCheckConstraint, ValidationSchema } from "./validation";
5
6
 
6
7
  type ColExtraInfo = {
7
8
  min?: string | number;
@@ -85,6 +86,10 @@ type TextColumn = TextColDef & {
85
86
  lowerCased?: boolean;
86
87
  }
87
88
 
89
+ type JSONBColumnDef = TextColDef & {
90
+ jsonbSchema: ValidationSchema;
91
+ }
92
+
88
93
  /**
89
94
  * Allows referencing media to this table.
90
95
  * Requires this table to have a primary key AND a valid fileTable config
@@ -138,7 +143,7 @@ type NamedJoinColumn = {
138
143
  joinDef: JoinDef[];
139
144
  }
140
145
 
141
- type ColumnConfig<LANG_IDS = { en: 1 }> = NamedJoinColumn | MediaColumn | (BaseColumn<LANG_IDS> & (SQLDefColumn | ReferencedColumn | TextColumn))
146
+ type ColumnConfig<LANG_IDS = { en: 1 }> = NamedJoinColumn | MediaColumn | (BaseColumn<LANG_IDS> & (SQLDefColumn | ReferencedColumn | TextColumn | JSONBColumnDef))
142
147
 
143
148
  type TableDefinition<LANG_IDS> = {
144
149
  columns?: {
@@ -351,19 +356,19 @@ export default class TableConfigurator<LANG_IDS = { en: 1 }> {
351
356
  queries = [];
352
357
 
353
358
  /* Create referenced columns */
354
- await Promise.all(Object.keys(this.config).map(async tableName => {
359
+ await Promise.all(getKeys(this.config).map(async tableName => {
355
360
  const tableConf = this.config![tableName];
356
361
  if ("columns" in tableConf) {
357
362
  const getColDef = (name: string, colConf: ColumnConfig): string => {
358
363
  const colNameEsc = asName(name);
359
- const getTextDef = (colConf: TextColDef) => {
364
+ const getColDef = (colConf: TextColDef, pgType: "TEXT" | "JSONB") => {
360
365
  const { nullable, defaultValue } = colConf;
361
- return ` TEXT ${!nullable ? " NOT NULL " : ""} ${defaultValue ? ` DEFAULT ${asValue(defaultValue)} ` : ""}`
366
+ return `${pgType} ${!nullable ? " NOT NULL " : ""} ${defaultValue ? ` DEFAULT ${asValue(defaultValue)} ` : ""}`
362
367
  }
363
368
  if ("references" in colConf && colConf.references) {
364
369
 
365
370
  const { tableName: lookupTable, columnName: lookupCol = "id" } = colConf.references;
366
- return ` ${colNameEsc} ${getTextDef(colConf.references)} REFERENCES ${lookupTable} (${lookupCol}) `;
371
+ return ` ${colNameEsc} ${getColDef(colConf.references, "TEXT")} REFERENCES ${lookupTable} (${lookupCol}) `;
367
372
 
368
373
  } else if ("sqlDefinition" in colConf && colConf.sqlDefinition) {
369
374
 
@@ -380,46 +385,51 @@ export default class TableConfigurator<LANG_IDS = { en: 1 }> {
380
385
  if (cArr.length) {
381
386
  checks = `CHECK (${cArr.join(" AND ")})`
382
387
  }
383
- return ` ${colNameEsc} ${getTextDef(colConf)} ${checks}`;
388
+ return ` ${colNameEsc} ${getColDef(colConf, "TEXT")} ${checks}`;
389
+
390
+ } else if ("jsonbSchema" in colConf && colConf.jsonbSchema) {
391
+
392
+ return ` ${colNameEsc} ${getColDef(colConf, "JSONB")} CHECK(${getPGCheckConstraint({ schema: colConf.jsonbSchema, escapedFieldName: colNameEsc })})`;
393
+
384
394
  } else {
385
395
  throw "Unknown column config: " + JSON.stringify(colConf);
386
396
  }
387
397
  }
388
398
 
389
- const colDefs: string[] = [];
399
+ const colCreateLines: string[] = [];
400
+ const tableHandler = this.dbo[tableName];
390
401
  if (tableConf.columns) {
391
402
  getKeys(tableConf?.columns).filter(c => !("joinDef" in tableConf.columns![c])).map(colName => {
392
403
  const colConf = tableConf.columns![colName];
393
404
 
394
- if (!this.dbo[tableName]) {
395
- colDefs.push(getColDef(colName, colConf))
396
- } else if (!colDefs.length && !this.dbo[tableName].columns?.find(c => colName === c.name)) {
405
+ /* Add columns to create statement */
406
+ if (!tableHandler) {
407
+ colCreateLines.push(getColDef(colName, colConf));
397
408
 
409
+ } else if (tableHandler && !tableHandler.columns?.find(c => colName === c.name)) {
410
+
411
+
412
+ queries.push(`
413
+ ALTER TABLE ${asName(tableName)}
414
+ ADD COLUMN ${getColDef(colName, colConf)};
415
+ `)
398
416
  if ("references" in colConf && colConf.references) {
399
417
 
400
418
  const { tableName: lookupTable, } = colConf.references;
401
- queries.push(`
402
- ALTER TABLE ${asName(tableName)}
403
- ADD COLUMN ${getColDef(colName, colConf)};
404
- `)
405
419
  console.log(`TableConfigurator: ${tableName}(${colName})` + " referenced lookup table " + lookupTable);
406
-
407
- } else if ("sqlDefinition" in colConf && colConf.sqlDefinition) {
408
-
409
- queries.push(`
410
- ALTER TABLE ${asName(tableName)}
411
- ADD COLUMN ${getColDef(colName, colConf)};
412
- `)
413
- console.log(`TableConfigurator: created/added column ${tableName}(${colName}) ` + colConf.sqlDefinition)
420
+ } else {
421
+ console.log(`TableConfigurator: created/added column ${tableName}(${colName}) `)
414
422
  }
415
423
  }
416
424
  });
417
425
  }
418
426
 
419
- if (colDefs.length) {
420
- queries.push(`CREATE TABLE ${asName(tableName)} (
421
- ${colDefs.join(", \n")}
422
- );`)
427
+ if (colCreateLines.length) {
428
+ queries.push([
429
+ `CREATE TABLE ${asName(tableName)} (`,
430
+ colCreateLines.join(", \n"),
431
+ `);`
432
+ ].join("\n"))
423
433
  console.log("TableConfigurator: Created table: \n" + queries[0])
424
434
  }
425
435
  }
@@ -0,0 +1,50 @@
1
+ declare type FieldType = ({
2
+ type: "number" | "boolean" | "integer" | "string" | "number[]" | "boolean[]" | "integer[]" | "string[]" | ValidationSchema;
3
+ } | {
4
+ oneOf: readonly any[];
5
+ } | {
6
+ oneOfTypes: readonly ValidationSchema[];
7
+ }) & {
8
+ optional?: boolean;
9
+ nullable?: boolean;
10
+ };
11
+ declare type GetType<T extends FieldType> = T extends {
12
+ type: ValidationSchema;
13
+ } ? SchemaObject<T["type"]> : T extends {
14
+ type: "number";
15
+ } ? number : T extends {
16
+ type: "boolean";
17
+ } ? boolean : T extends {
18
+ type: "integer";
19
+ } ? number : T extends {
20
+ type: "string";
21
+ } ? string : T extends {
22
+ type: "number[]";
23
+ } ? number[] : T extends {
24
+ type: "boolean[]";
25
+ } ? boolean[] : T extends {
26
+ type: "integer[]";
27
+ } ? number[] : T extends {
28
+ type: "string[]";
29
+ } ? string[] : T extends {
30
+ oneOf: readonly any[];
31
+ } ? T["oneOf"][number] :
32
+ /** This needs fixing */
33
+ T extends {
34
+ oneOfTypes: readonly ValidationSchema[];
35
+ } ? SchemaObject<T["oneOfTypes"][number]> : any;
36
+ export declare type ValidationSchema = Record<string, FieldType>;
37
+ export declare type SchemaObject<S extends ValidationSchema> = ({
38
+ [K in keyof S as S[K]["optional"] extends true ? K : never]?: GetType<S[K]>;
39
+ } & {
40
+ [K in keyof S as S[K]["optional"] extends true ? never : K]: GetType<S[K]>;
41
+ });
42
+ export declare function validate<T>(obj: T, key: keyof T, validation: FieldType): boolean;
43
+ export declare function validateSchema<S extends ValidationSchema>(schema: S, obj: SchemaObject<S>, objName?: string, optional?: boolean): void;
44
+ export declare function getPGCheckConstraint(args: {
45
+ escapedFieldName: string;
46
+ schema: ValidationSchema;
47
+ }): string;
48
+ export declare function getSchemaTSTypes(schema: ValidationSchema, leading?: string, isOneOf?: boolean): string;
49
+ export {};
50
+ //# sourceMappingURL=validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["validation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getSchemaTSTypes = exports.getPGCheckConstraint = exports.validateSchema = exports.validate = void 0;
4
+ const prostgles_types_1 = require("prostgles-types");
5
+ const PubSubManager_1 = require("./PubSubManager");
6
+ /** tests */
7
+ const s = {
8
+ a: { type: "boolean" },
9
+ c: { type: { c1: { type: "string" } } },
10
+ o: { oneOfTypes: [
11
+ { z: { type: "integer" } },
12
+ { z1: { type: "integer" } }
13
+ ] }
14
+ };
15
+ const ss = {
16
+ a: true,
17
+ c: {
18
+ c1: ""
19
+ },
20
+ o: { z: 1, z1: 23 }
21
+ };
22
+ function validate(obj, key, validation) {
23
+ let err = `The provided value for ${JSON.stringify(key)} is of invalid type. Expecting `;
24
+ const val = obj[key];
25
+ if ("type" in validation && validation.type) {
26
+ if (typeof validation.type !== "string") {
27
+ (0, prostgles_types_1.getKeys)(validation.type).forEach(subKey => {
28
+ validate(val, subKey, validation.type[subKey]);
29
+ });
30
+ }
31
+ err += validation.type;
32
+ if (validation.type === "boolean" && typeof val !== validation.type)
33
+ throw new Error(err);
34
+ if (validation.type === "string" && typeof val !== validation.type)
35
+ throw new Error(err);
36
+ if (validation.type === "number" && !Number.isFinite(val))
37
+ throw new Error(err);
38
+ if (validation.type === "integer" && !Number.isInteger(val))
39
+ throw new Error(err);
40
+ }
41
+ else if ("oneOf" in validation && validation.oneOf) {
42
+ err += `on of: ${validation.oneOf}`;
43
+ if (!validation.oneOf.includes(val))
44
+ throw new Error(err);
45
+ }
46
+ return true;
47
+ }
48
+ exports.validate = validate;
49
+ function validateSchema(schema, obj, objName, optional = false) {
50
+ if ((!schema || (0, prostgles_types_1.isEmpty)(schema)) && !optional)
51
+ throw new Error(`Expecting ${objName} to be defined`);
52
+ (0, prostgles_types_1.getKeys)(schema).forEach(k => validate(obj, k, schema[k]));
53
+ }
54
+ exports.validateSchema = validateSchema;
55
+ function getPGCheckConstraint(args) {
56
+ const { schema: s, escapedFieldName } = args;
57
+ const jsToPGtypes = {
58
+ "number": "::NUMERIC",
59
+ "boolean": "::BOOLEAN",
60
+ "string": "" // already a string
61
+ };
62
+ const kChecks = (k) => {
63
+ const t = s[k];
64
+ const checks = [];
65
+ const valAsJson = `${escapedFieldName}->${(0, PubSubManager_1.asValue)(k)}`;
66
+ const valAsText = `${escapedFieldName}->>${(0, PubSubManager_1.asValue)(k)}`;
67
+ if (t.nullable)
68
+ checks.push(`${valAsJson} IS NULL`);
69
+ if (t.optional)
70
+ checks.push(`${escapedFieldName} ? ${(0, PubSubManager_1.asValue)(k)} = FALSE`);
71
+ if ("oneOfTypes" in t) {
72
+ checks.push(`(${t.oneOfTypes.map(subType => getPGCheckConstraint({ escapedFieldName: valAsJson, schema: subType })).join(" OR ")})`);
73
+ }
74
+ else if ("oneOf" in t) {
75
+ if (!t.oneOf.length || t.oneOf.some(v => v === undefined || !["number", "boolean", "string", null].includes(typeof v))) {
76
+ throw new Error(`Invalid ValidationSchema for property: ${k} of field ${escapedFieldName}: oneOf cannot be empty AND can only contain: numbers, text, boolean, null`);
77
+ }
78
+ const oneOfHasNull = t.oneOf.includes(null);
79
+ if (oneOfHasNull)
80
+ checks.push(`${valAsText} IS NULL`);
81
+ const oneOf = t.oneOf.filter(o => o !== null);
82
+ oneOf.forEach(o => {
83
+ (0, PubSubManager_1.asValue)(o.toString());
84
+ checks.push(`${valAsText}${jsToPGtypes[typeof o]} = ${(0, PubSubManager_1.asValue)(o)}`);
85
+ });
86
+ }
87
+ else if ("type" in t) {
88
+ if (typeof t.type === "string") {
89
+ const correctType = t.type.replace("integer", "number");
90
+ if (t.type.endsWith("[]")) {
91
+ /** Must add custom functions to type check each array element */
92
+ checks.push(`
93
+ jsonb_typeof(${valAsJson}) = 'array' AND
94
+ ( jsonb_array_length(${valAsJson}) = 0 OR jsonb_typeof(jsonb_array_element(${valAsJson}, 1)) = ${(0, PubSubManager_1.asValue)(correctType.slice(0, -2))} )`);
95
+ }
96
+ else {
97
+ checks.push(`jsonb_typeof(${valAsJson}) = ${(0, PubSubManager_1.asValue)(correctType)} `);
98
+ }
99
+ }
100
+ else {
101
+ checks.push("( " + getPGCheckConstraint({ escapedFieldName: valAsJson, schema: t.type }) + " )");
102
+ }
103
+ }
104
+ return checks.join(" OR ");
105
+ };
106
+ return (0, prostgles_types_1.getKeys)(s).map(k => "(" + kChecks(k) + ")").join(" AND ");
107
+ }
108
+ exports.getPGCheckConstraint = getPGCheckConstraint;
109
+ function getSchemaTSTypes(schema, leading = "", isOneOf = false) {
110
+ const getFieldType = (def) => {
111
+ if ("type" in def) {
112
+ if (typeof def.type === "string") {
113
+ const correctType = def.type.replace("integer", "number");
114
+ return correctType;
115
+ }
116
+ else {
117
+ return getSchemaTSTypes(def.type);
118
+ }
119
+ }
120
+ else if ("oneOf" in def) {
121
+ return def.oneOf.map(v => v).join(" | ");
122
+ }
123
+ else if ("oneOfTypes" in def) {
124
+ return def.oneOfTypes.map(v => `\n${leading} | ` + getSchemaTSTypes(v, "", true)).join("");
125
+ }
126
+ else
127
+ throw "Unexpected getSchemaTSTypes";
128
+ };
129
+ let spacing = isOneOf ? " " : " ";
130
+ let res = `${leading}{ \n` + (0, prostgles_types_1.getKeys)(schema).map(k => {
131
+ const def = schema[k];
132
+ return `${leading}${spacing}${k}${def.optional ? "?" : ""}: ${def.nullable ? " null | " : ""} ` + getFieldType(def) + ";";
133
+ }).join("\n") + ` \n${leading}}${isOneOf ? "" : ";"}`;
134
+ /** Keep single line */
135
+ if (isOneOf)
136
+ res = res.split("\n").join("");
137
+ return res;
138
+ }
139
+ exports.getSchemaTSTypes = getSchemaTSTypes;
@@ -0,0 +1,163 @@
1
+ import { asName, getKeys, isEmpty, isObject } from "prostgles-types";
2
+ import { asValue } from "./PubSubManager";
3
+
4
+ type FieldType = ({
5
+ type:
6
+ | "number" | "boolean" | "integer" | "string"
7
+ | "number[]" | "boolean[]" | "integer[]" | "string[]"
8
+ | ValidationSchema;
9
+
10
+ } | {
11
+ oneOf: readonly any[];
12
+ } | {
13
+ oneOfTypes: readonly ValidationSchema[];
14
+ }) & {
15
+ optional?: boolean;
16
+ nullable?: boolean;
17
+ };
18
+
19
+ type GetType<T extends FieldType> =
20
+ | T extends { type: ValidationSchema }? SchemaObject<T["type"]> :
21
+ | T extends { type: "number" }? number:
22
+ | T extends { type: "boolean" }? boolean:
23
+ | T extends { type: "integer" }? number:
24
+ | T extends { type: "string" }? string:
25
+ | T extends { type: "number[]" }? number[]:
26
+ | T extends { type: "boolean[]" }? boolean[]:
27
+ | T extends { type: "integer[]" }? number[]:
28
+ | T extends { type: "string[]" }? string[]:
29
+ | T extends { oneOf: readonly any[] }? T["oneOf"][number] :
30
+
31
+ /** This needs fixing */
32
+ | T extends { oneOfTypes: readonly ValidationSchema[] }? SchemaObject<T["oneOfTypes"][number]> :
33
+ any;
34
+
35
+ export type ValidationSchema = Record<string, FieldType>;
36
+ export type SchemaObject<S extends ValidationSchema> = ({
37
+ [K in keyof S as S[K]["optional"] extends true? K : never]?: GetType<S[K]>
38
+ } & {
39
+ [K in keyof S as S[K]["optional"] extends true? never : K]: GetType<S[K]>
40
+ });
41
+
42
+ /** tests */
43
+ const s = {
44
+ a: { type: "boolean" },
45
+ c: { type: { c1: { type: "string" } } },
46
+ o: { oneOfTypes: [
47
+ { z: { type: "integer" } },
48
+ { z1: { type: "integer" } }
49
+ ] }
50
+ } as const;
51
+ const ss: SchemaObject<typeof s> = {
52
+ a: true,
53
+ c: {
54
+ c1: ""
55
+ },
56
+ o: { z: 1, z1: 23 }
57
+ }
58
+
59
+ export function validate<T>(obj: T, key: keyof T, validation: FieldType): boolean {
60
+ let err = `The provided value for ${JSON.stringify(key)} is of invalid type. Expecting `;
61
+ const val = obj[key];
62
+ if("type" in validation && validation.type){
63
+ if(typeof validation.type !== "string"){
64
+ getKeys(validation.type).forEach(subKey => {
65
+ validate(val, subKey as any, (validation.type as ValidationSchema)[subKey])
66
+ });
67
+ }
68
+ err += validation.type;
69
+ if(validation.type === "boolean" && typeof val !== validation.type) throw new Error(err)
70
+ if(validation.type === "string" && typeof val !== validation.type) throw new Error(err)
71
+ if(validation.type === "number" && !Number.isFinite(val)) throw new Error(err)
72
+ if(validation.type === "integer" && !Number.isInteger(val)) throw new Error(err)
73
+ } else if("oneOf" in validation && validation.oneOf){
74
+ err += `on of: ${validation.oneOf}`;
75
+ if(!validation.oneOf.includes(val)) throw new Error(err)
76
+ }
77
+ return true
78
+ }
79
+
80
+ export function validateSchema<S extends ValidationSchema>(schema: S, obj: SchemaObject<S>, objName?: string, optional = false){
81
+ if((!schema || isEmpty(schema)) && !optional) throw new Error(`Expecting ${objName} to be defined`);
82
+ getKeys(schema).forEach(k => validate(obj as any, k, schema[k]));
83
+ }
84
+
85
+ export function getPGCheckConstraint(args: { escapedFieldName: string; schema: ValidationSchema }): string {
86
+ const { schema: s, escapedFieldName } = args;
87
+
88
+ const jsToPGtypes = {
89
+ "number": "::NUMERIC",
90
+ "boolean": "::BOOLEAN",
91
+ "string": "" // already a string
92
+ }
93
+
94
+ const kChecks = (k: string) => {
95
+ const t = s[k];
96
+ const checks: string[] = [];
97
+ const valAsJson = `${escapedFieldName}->${asValue(k)}`;
98
+ const valAsText = `${escapedFieldName}->>${asValue(k)}`;
99
+ if(t.nullable) checks.push(`${valAsJson} IS NULL`);
100
+ if(t.optional) checks.push(`${escapedFieldName} ? ${asValue(k)} = FALSE`);
101
+
102
+ if("oneOfTypes" in t){
103
+ checks.push(`(${t.oneOfTypes.map(subType => getPGCheckConstraint({ escapedFieldName: valAsJson, schema: subType })).join(" OR ")})`)
104
+ } else if("oneOf" in t){
105
+ if(!t.oneOf.length || t.oneOf.some(v => v === undefined || !["number", "boolean", "string", null].includes(typeof v))) {
106
+ throw new Error(`Invalid ValidationSchema for property: ${k} of field ${escapedFieldName}: oneOf cannot be empty AND can only contain: numbers, text, boolean, null`);
107
+ }
108
+ const oneOfHasNull = t.oneOf.includes(null);
109
+ if(oneOfHasNull) checks.push(`${valAsText} IS NULL`);
110
+ const oneOf = t.oneOf.filter(o => o !== null);
111
+ oneOf.forEach(o => {
112
+ asValue(o.toString())
113
+ checks.push(`${valAsText}${(jsToPGtypes as any)[typeof o]} = ${asValue(o)}`);
114
+ })
115
+ } else if("type" in t){
116
+ if(typeof t.type === "string") {
117
+ const correctType = t.type.replace("integer", "number")
118
+ if(t.type.endsWith("[]")){
119
+ /** Must add custom functions to type check each array element */
120
+ checks.push(`
121
+ jsonb_typeof(${valAsJson}) = 'array' AND
122
+ ( jsonb_array_length(${valAsJson}) = 0 OR jsonb_typeof(jsonb_array_element(${valAsJson}, 1)) = ${asValue(correctType.slice(0, -2))} )`)
123
+ } else {
124
+ checks.push(`jsonb_typeof(${valAsJson}) = ${asValue(correctType)} `)
125
+ }
126
+ } else {
127
+ checks.push("( " + getPGCheckConstraint({ escapedFieldName: valAsJson, schema: t.type }) + " )")
128
+ }
129
+ }
130
+
131
+ return checks.join(" OR ")
132
+ }
133
+
134
+ return getKeys(s).map(k => "(" + kChecks(k) + ")").join(" AND ");
135
+ }
136
+
137
+ export function getSchemaTSTypes(schema: ValidationSchema, leading = "", isOneOf = false): string {
138
+ const getFieldType = (def: FieldType) => {
139
+ if("type" in def){
140
+ if(typeof def.type === "string"){
141
+ const correctType = def.type.replace("integer", "number")
142
+ return correctType
143
+ } else {
144
+ return getSchemaTSTypes(def.type)
145
+ }
146
+ } else if("oneOf" in def){
147
+ return def.oneOf.map(v => v).join(" | ")
148
+ } else if("oneOfTypes" in def){
149
+ return def.oneOfTypes.map(v => `\n${leading} | ` + getSchemaTSTypes(v, "", true)).join("")
150
+ } else throw "Unexpected getSchemaTSTypes"
151
+ }
152
+
153
+ let spacing = isOneOf? " " : " ";
154
+
155
+ let res = `${leading}{ \n` + getKeys(schema).map(k => {
156
+ const def = schema[k];
157
+ return `${leading}${spacing}${k}${def.optional? "?" : ""}: ${def.nullable? " null | " : ""} ` + getFieldType(def) + ";";
158
+ }).join("\n") + ` \n${leading}}${isOneOf? "" : ";"}`;
159
+
160
+ /** Keep single line */
161
+ if(isOneOf) res = res.split("\n").join("")
162
+ return res;
163
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prostgles-server",
3
- "version": "2.0.228",
3
+ "version": "2.0.231",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -28,6 +28,7 @@
28
28
  "@types/express": "^4.17.13",
29
29
  "aws-sdk": "^2.1168.0",
30
30
  "bluebird": "^3.7.2",
31
+ "check-disk-space": "^3.3.1",
31
32
  "file-type": "^17.1.3",
32
33
  "pg-promise": "^10.11.1",
33
34
  "prostgles-types": "^1.5.169",
@@ -1 +1 @@
1
- 165333
1
+ 8122
@@ -1 +1 @@
1
- {"version":3,"file":"isomorphic_queries.d.ts","sourceRoot":"","sources":["isomorphic_queries.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAC,MAAM,mBAAmB,CAAC;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAGtD,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,EAAE,GAAG,CAAC,EAAE,QAAQ,iBAYzE;AACD,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,KAAK,GAAG,EAAE,GAAG,CAAC,EAAE,QAAQ,oBAW7F;AAED,wBAA8B,UAAU,CAAC,EAAE,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,OAAO,CAAC,eAAe,CAAC,iBAgyB/F"}
1
+ {"version":3,"file":"isomorphic_queries.d.ts","sourceRoot":"","sources":["isomorphic_queries.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAC,MAAM,mBAAmB,CAAC;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAGtD,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,EAAE,GAAG,CAAC,EAAE,QAAQ,iBAYzE;AACD,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,KAAK,GAAG,EAAE,GAAG,CAAC,EAAE,QAAQ,oBAW7F;AAED,wBAA8B,UAAU,CAAC,EAAE,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,OAAO,CAAC,eAAe,CAAC,iBA2zB/F"}
@@ -544,6 +544,31 @@ async function isomorphic(db) {
544
544
  const newF = await db.media.findOne({ id: original.id });
545
545
  assert_1.strict.equal(newF.original_name, newFile.name);
546
546
  });
547
+ await tryRun("jsonbSchema validation", async () => {
548
+ /**
549
+ *
550
+ tjson: {
551
+ json: { jsonbSchema: {
552
+ a: { type: "boolean" },
553
+ arr: { oneOf: ["1", "2", "3"] },
554
+ arr2: { type: "integer[]" },
555
+ o: { oneOfTypes: [{ o1: { type: "integer" } }, { o2: { type: "boolean" } }], optional: true },
556
+ }
557
+ }
558
+ },
559
+ */
560
+ const json = { a: true, arr: "2" };
561
+ const fo = await db.tjson.insert({ json }, { returning: "*" });
562
+ assert_1.strict.deepStrictEqual(fo.json, json);
563
+ await db.tjson.insert({ json: { o: { o1: 2, o2: true }, a: true, arr: 1 } });
564
+ try {
565
+ await db.tjson.insert({ json: { a: true, arr: "22" } });
566
+ throw "Should have failed";
567
+ }
568
+ catch (e) {
569
+ // Perfect
570
+ }
571
+ });
547
572
  await tryRun("Exists filter example", async () => {
548
573
  const fo = await db.items.findOne(), f = await db.items.find();
549
574
  assert_1.strict.deepStrictEqual(fo, { h: null, id: 1, name: 'a' }, "findOne query failed");
@@ -623,7 +623,34 @@ export default async function isomorphic(db: Partial<DBHandlerServer> | Partial<
623
623
  const newF = await db.media.findOne({ id: original.id });
624
624
 
625
625
  assert.equal(newF.original_name, newFile.name)
626
- })
626
+ });
627
+
628
+ await tryRun("jsonbSchema validation", async () => {
629
+
630
+ /**
631
+ *
632
+ tjson: {
633
+ json: { jsonbSchema: {
634
+ a: { type: "boolean" },
635
+ arr: { oneOf: ["1", "2", "3"] },
636
+ arr2: { type: "integer[]" },
637
+ o: { oneOfTypes: [{ o1: { type: "integer" } }, { o2: { type: "boolean" } }], optional: true },
638
+ }
639
+ }
640
+ },
641
+ */
642
+
643
+ const json = {a: true, arr: "2"}
644
+ const fo = await db.tjson.insert({ json }, { returning: "*"});
645
+ assert.deepStrictEqual(fo.json, json);
646
+ await db.tjson.insert({ json: {o: { o1: 2, o2: true }, a: true, arr: 1 } })
647
+ try {
648
+ await db.tjson.insert({ json: { a: true, arr: "22"} });
649
+ throw "Should have failed"
650
+ } catch(e){
651
+ // Perfect
652
+ }
653
+ });
627
654
 
628
655
  await tryRun("Exists filter example", async () => {
629
656