@nymphjs/driver-postgresql 1.0.0-beta.12 → 1.0.0-beta.121

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,26 +1,57 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const child_process_1 = __importDefault(require("child_process"));
7
- const pg_1 = require("pg");
8
- const pg_format_1 = __importDefault(require("pg-format"));
9
- const nymph_1 = require("@nymphjs/nymph");
10
- const guid_1 = require("@nymphjs/guid");
11
- const conf_1 = require("./conf");
12
- class PostgreSQLDriver extends nymph_1.NymphDriver {
1
+ import pg from 'pg';
2
+ import format from 'pg-format';
3
+ import Cursor from 'pg-cursor';
4
+ import { NymphDriver, EntityUniqueConstraintError, InvalidParametersError, NotConfiguredError, QueryFailedError, UnableToConnectError, xor, } from '@nymphjs/nymph';
5
+ import { makeTableSuffix } from '@nymphjs/guid';
6
+ import { PostgreSQLDriverConfigDefaults as defaults, } from './conf/index.js';
7
+ /**
8
+ * The PostgreSQL Nymph database driver.
9
+ */
10
+ export default class PostgreSQLDriver extends NymphDriver {
11
+ config;
12
+ postgresqlConfig;
13
+ prefix;
14
+ connected = false;
15
+ // @ts-ignore: this is assigned in connect(), which is called by the constructor.
16
+ link;
17
+ transaction = null;
13
18
  static escape(input) {
14
- return pg_format_1.default.ident(input);
19
+ return format.ident(input);
15
20
  }
16
21
  static escapeValue(input) {
17
- return pg_format_1.default.literal(input);
22
+ return format.literal(input);
23
+ }
24
+ static escapeNullSequences(input) {
25
+ // Postgres doesn't support null bytes in `text`, and it converts strings
26
+ // in JSON to `text`, so we need to escape the escape sequences for null
27
+ // bytes.
28
+ return (input
29
+ .replace(/\uFFFD/g, () => '\uFFFD\uFFFD')
30
+ // n so that if there's already an escape, it turns into \n
31
+ // - so that it won't match a \uFFFD that got turned into \uFFFD\uFFFD
32
+ .replace(/\\u0000/g, () => 'nu\uFFFD-')
33
+ .replace(/\\x00/g, () => 'nx\uFFFD-'));
34
+ }
35
+ static unescapeNullSequences(input) {
36
+ return input
37
+ .replace(/nu\uFFFD-/g, () => '\\u0000')
38
+ .replace(/nx\uFFFD-/g, () => '\\x00')
39
+ .replace(/\uFFFD\uFFFD/g, () => '\uFFFD');
40
+ }
41
+ static escapeNulls(input) {
42
+ // Postgres doesn't support null bytes in `text`.
43
+ return input
44
+ .replace(/\uFFFD/g, () => '\uFFFD\uFFFD')
45
+ .replace(/\x00/g, () => '-\uFFFD-');
46
+ }
47
+ static unescapeNulls(input) {
48
+ return input
49
+ .replace(/-\uFFFD-/g, () => '\x00')
50
+ .replace(/\uFFFD\uFFFD/g, () => '\uFFFD');
18
51
  }
19
52
  constructor(config, link, transaction) {
20
53
  super();
21
- this.connected = false;
22
- this.transaction = null;
23
- this.config = { ...conf_1.PostgreSQLDriverConfigDefaults, ...config };
54
+ this.config = { ...defaults, ...config };
24
55
  const { host, user, password, database, port, customPoolConfig } = this.config;
25
56
  this.postgresqlConfig = customPoolConfig ?? {
26
57
  host,
@@ -41,19 +72,40 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
41
72
  this.connect();
42
73
  }
43
74
  }
75
+ /**
76
+ * This is used internally by Nymph. Don't call it yourself.
77
+ *
78
+ * @returns A clone of this instance.
79
+ */
44
80
  clone() {
45
81
  return new PostgreSQLDriver(this.config, this.link, this.transaction ?? undefined);
46
82
  }
47
- getConnection() {
48
- if (this.transaction != null && this.transaction.connection != null) {
83
+ getConnection(outsideTransaction = false) {
84
+ if (this.transaction != null &&
85
+ this.transaction.connection != null &&
86
+ !outsideTransaction) {
49
87
  return Promise.resolve(this.transaction.connection);
50
88
  }
51
- return new Promise((resolve, reject) => this.link.connect((err, client, done) => err ? reject(err) : resolve({ client, done })));
89
+ return new Promise((resolve, reject) => this.link.connect((err, client, done) => err
90
+ ? reject(err)
91
+ : client
92
+ ? resolve({ client, done })
93
+ : reject('No client returned from connect.')));
52
94
  }
95
+ /**
96
+ * Connect to the PostgreSQL database.
97
+ *
98
+ * @returns Whether this instance is connected to a PostgreSQL database.
99
+ */
53
100
  async connect() {
101
+ // If we think we're connected, try pinging the server.
54
102
  try {
55
103
  if (this.connected) {
56
- const connection = await new Promise((resolve, reject) => this.link.connect((err, client, done) => err ? reject(err) : resolve({ client, done })));
104
+ const connection = await new Promise((resolve, reject) => this.link.connect((err, client, done) => err
105
+ ? reject(err)
106
+ : client
107
+ ? resolve({ client, done })
108
+ : reject('No client returned from connect.')));
57
109
  await new Promise((resolve, reject) => connection.client.query('SELECT 1;', [], (err, res) => {
58
110
  if (err) {
59
111
  reject(err);
@@ -66,9 +118,10 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
66
118
  catch (e) {
67
119
  this.connected = false;
68
120
  }
121
+ // Connecting, selecting database
69
122
  if (!this.connected) {
70
123
  try {
71
- this.link = new pg_1.Pool(this.postgresqlConfig);
124
+ this.link = new pg.Pool(this.postgresqlConfig);
72
125
  this.connected = true;
73
126
  }
74
127
  catch (e) {
@@ -76,15 +129,20 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
76
129
  this.postgresqlConfig.user === 'nymph' &&
77
130
  this.postgresqlConfig.password === 'password' &&
78
131
  this.postgresqlConfig.database === 'nymph') {
79
- throw new nymph_1.NotConfiguredError("It seems the config hasn't been set up correctly.");
132
+ throw new NotConfiguredError("It seems the config hasn't been set up correctly.");
80
133
  }
81
134
  else {
82
- throw new nymph_1.UnableToConnectError('Could not connect: ' + e?.message);
135
+ throw new UnableToConnectError('Could not connect: ' + e?.message);
83
136
  }
84
137
  }
85
138
  }
86
139
  return this.connected;
87
140
  }
141
+ /**
142
+ * Disconnect from the PostgreSQL database.
143
+ *
144
+ * @returns Whether this instance is connected to a PostgreSQL database.
145
+ */
88
146
  async disconnect() {
89
147
  if (this.connected) {
90
148
  await new Promise((resolve) => this.link.end(() => resolve(0)));
@@ -93,87 +151,199 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
93
151
  return this.connected;
94
152
  }
95
153
  async inTransaction() {
154
+ if (this.transaction && this.transaction.count === 0) {
155
+ this.transaction = null;
156
+ }
96
157
  return !!this.transaction;
97
158
  }
159
+ /**
160
+ * Check connection status.
161
+ *
162
+ * @returns Whether this instance is connected to a PostgreSQL database.
163
+ */
98
164
  isConnected() {
99
165
  return this.connected;
100
166
  }
101
- createTables(etype = null) {
102
- if (etype != null) {
103
- this.queryRunSync(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} (
167
+ async createEntitiesTable(etype, connection) {
168
+ // Create the entity table.
169
+ await this.queryRun(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} (
104
170
  "guid" BYTEA NOT NULL,
105
171
  "tags" TEXT[],
106
172
  "cdate" DOUBLE PRECISION NOT NULL,
107
173
  "mdate" DOUBLE PRECISION NOT NULL,
174
+ "user" BYTEA,
175
+ "group" BYTEA,
176
+ "acUser" SMALLINT,
177
+ "acGroup" SMALLINT,
178
+ "acOther" SMALLINT,
179
+ "acRead" BYTEA[],
180
+ "acWrite" BYTEA[],
181
+ "acFull" BYTEA[],
108
182
  PRIMARY KEY ("guid")
109
- ) WITH ( OIDS=FALSE );`);
110
- this.queryRunSync(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`);
111
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_cdate`)};`);
112
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_cdate`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING btree ("cdate");`);
113
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_mdate`)};`);
114
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_mdate`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING btree ("mdate");`);
115
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_tags`)};`);
116
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_tags`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING gin ("tags");`);
117
- this.queryRunSync(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} (
118
- "guid" BYTEA NOT NULL,
119
- "name" TEXT NOT NULL,
120
- "value" TEXT NOT NULL,
121
- PRIMARY KEY ("guid", "name"),
122
- FOREIGN KEY ("guid")
123
- REFERENCES ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE
124
- ) WITH ( OIDS=FALSE );`);
125
- this.queryRunSync(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`);
126
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid`)};`);
127
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("guid");`);
128
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name`)};`);
129
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("name");`);
130
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name__user`)};`);
131
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name__user`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("guid") WHERE "name" = 'user'::text;`);
132
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name__group`)};`);
133
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name__group`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("guid") WHERE "name" = 'group'::text;`);
134
- this.queryRunSync(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} (
183
+ ) WITH ( OIDS=FALSE );`, { connection });
184
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`, { connection });
185
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_cdate`)};`, { connection });
186
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_cdate`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING btree ("cdate");`, { connection });
187
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_mdate`)};`, { connection });
188
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_mdate`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING btree ("mdate");`, { connection });
189
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_tags`)};`, { connection });
190
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_tags`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING gin ("tags");`, { connection });
191
+ await this.createEntitiesTilmeldIndexes(etype, connection);
192
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} SET ( autovacuum_vacuum_scale_factor = 0.05, autovacuum_analyze_scale_factor = 0.05 );`, { connection });
193
+ }
194
+ async addTilmeldColumnsAndIndexes(etype, connection) {
195
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ADD COLUMN IF NOT EXISTS "user" BYTEA,
196
+ ADD COLUMN IF NOT EXISTS "group" BYTEA,
197
+ ADD COLUMN IF NOT EXISTS "acUser" SMALLINT,
198
+ ADD COLUMN IF NOT EXISTS "acGroup" SMALLINT,
199
+ ADD COLUMN IF NOT EXISTS "acOther" SMALLINT,
200
+ ADD COLUMN IF NOT EXISTS "acRead" BYTEA[],
201
+ ADD COLUMN IF NOT EXISTS "acWrite" BYTEA[],
202
+ ADD COLUMN IF NOT EXISTS "acFull" BYTEA[];`);
203
+ await this.createEntitiesTilmeldIndexes(etype, connection);
204
+ }
205
+ async createEntitiesTilmeldIndexes(etype, connection) {
206
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_user_acUser`)};`, { connection });
207
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_user_acUser`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING btree ("user", "acUser");`, { connection });
208
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_group_acGroup`)};`, { connection });
209
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_group_acGroup`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING btree ("group", "acGroup");`, { connection });
210
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_acUser`)};`, { connection });
211
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_acUser`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING btree ("acUser");`, { connection });
212
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_acGroup`)};`, { connection });
213
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_acGroup`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING btree ("acGroup");`, { connection });
214
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_acOther`)};`, { connection });
215
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_acOther`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING btree ("acOther");`, { connection });
216
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_acRead`)};`, { connection });
217
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_acRead`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING gin ("acRead");`, { connection });
218
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_acWrite`)};`, { connection });
219
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_acWrite`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING gin ("acWrite");`, { connection });
220
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_acFull`)};`, { connection });
221
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_acFull`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING gin ("acFull");`, { connection });
222
+ }
223
+ async createDataTable(etype, connection) {
224
+ // Create the data table.
225
+ await this.queryRun(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} (
135
226
  "guid" BYTEA NOT NULL,
136
227
  "name" TEXT NOT NULL,
137
- "truthy" BOOLEAN,
228
+ "value" CHARACTER(1) NOT NULL,
229
+ "json" JSONB,
138
230
  "string" TEXT,
139
231
  "number" DOUBLE PRECISION,
232
+ "truthy" BOOLEAN,
140
233
  PRIMARY KEY ("guid", "name"),
141
234
  FOREIGN KEY ("guid")
142
235
  REFERENCES ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE
143
- ) WITH ( OIDS=FALSE );`);
144
- this.queryRunSync(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`);
145
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}_id_guid`)};`);
146
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}_id_guid`)} ON ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} USING btree ("guid");`);
147
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}_id_name`)};`);
148
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}_id_name`)} ON ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} USING btree ("name");`);
149
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}_id_guid_name_truthy`)};`);
150
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}_id_guid_name_truthy`)} ON ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} USING btree ("guid", "name") WHERE "truthy" = TRUE;`);
151
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}_id_guid_name_falsy`)};`);
152
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}_id_guid_name_falsy`)} ON ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} USING btree ("guid", "name") WHERE "truthy" <> TRUE;`);
153
- this.queryRunSync(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} (
236
+ ) WITH ( OIDS=FALSE );`, { connection });
237
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`, { connection });
238
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid`)};`, { connection });
239
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("guid");`, { connection });
240
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name`)};`, { connection });
241
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("guid", "name");`, { connection });
242
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name`)};`, { connection });
243
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("name");`, { connection });
244
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name_string`)};`, { connection });
245
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name_string`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("name", LEFT("string", 512));`, { connection });
246
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name_number`)};`, { connection });
247
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name_number`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("name", "number");`, { connection });
248
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name_number`)};`, { connection });
249
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name_number`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("guid", "name", "number");`, { connection });
250
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name_truthy`)};`, { connection });
251
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name_truthy`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("name", "truthy");`, { connection });
252
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name_truthy`)};`, { connection });
253
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name_truthy`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("guid", "name", "truthy");`, { connection });
254
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_string`)};`, { connection });
255
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_string`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING gin ("string" gin_trgm_ops);`, { connection });
256
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_json`)};`, { connection });
257
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_json`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING gin ("json");`, { connection });
258
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} SET ( autovacuum_vacuum_scale_factor = 0.05, autovacuum_analyze_scale_factor = 0.05 );`, { connection });
259
+ }
260
+ async createReferencesTable(etype, connection) {
261
+ // Create the references table.
262
+ await this.queryRun(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} (
154
263
  "guid" BYTEA NOT NULL,
155
264
  "name" TEXT NOT NULL,
156
265
  "reference" BYTEA NOT NULL,
157
266
  PRIMARY KEY ("guid", "name", "reference"),
158
267
  FOREIGN KEY ("guid")
159
268
  REFERENCES ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE
160
- ) WITH ( OIDS=FALSE );`);
161
- this.queryRunSync(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`);
162
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid`)};`);
163
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("guid");`);
164
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_name`)};`);
165
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_name`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("name");`);
166
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_reference`)};`);
167
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_reference`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("reference");`);
269
+ ) WITH ( OIDS=FALSE );`, { connection });
270
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`, { connection });
271
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid`)};`, { connection });
272
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("guid");`, { connection });
273
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_name`)};`, { connection });
274
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_name`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("name");`, { connection });
275
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_name_reference`)};`, { connection });
276
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_name_reference`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("name", "reference");`, { connection });
277
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_reference`)};`, { connection });
278
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_reference`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("reference");`, { connection });
279
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid_name_reference`)};`, { connection });
280
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid_name_reference`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("guid", "name", "reference");`, { connection });
281
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_reference_name_guid`)};`, { connection });
282
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_reference_name_guid`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("reference", "name", "guid");`, { connection });
283
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_reference_guid_name`)};`, { connection });
284
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_reference_guid_name`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("reference", "guid", "name");`, { connection });
285
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} SET ( autovacuum_vacuum_scale_factor = 0.05, autovacuum_analyze_scale_factor = 0.05 );`, { connection });
286
+ }
287
+ async createTokensTable(etype, connection) {
288
+ // Create the tokens table.
289
+ await this.queryRun(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} (
290
+ "guid" BYTEA NOT NULL,
291
+ "name" TEXT NOT NULL,
292
+ "token" INTEGER NOT NULL,
293
+ "position" INTEGER NOT NULL,
294
+ "stem" BOOLEAN NOT NULL,
295
+ PRIMARY KEY ("guid", "name", "token", "position"),
296
+ FOREIGN KEY ("guid")
297
+ REFERENCES ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE
298
+ ) WITH ( OIDS=FALSE );`, { connection });
299
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`, { connection });
300
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}_id_name_token`)};`, { connection });
301
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}_id_name_token`)} ON ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} USING btree ("name", "token");`, { connection });
302
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} SET ( autovacuum_vacuum_scale_factor = 0.05, autovacuum_analyze_scale_factor = 0.05 );`, { connection });
303
+ }
304
+ async createUniquesTable(etype, connection) {
305
+ // Create the unique strings table.
306
+ await this.queryRun(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} (
307
+ "guid" BYTEA NOT NULL,
308
+ "unique" TEXT NOT NULL UNIQUE,
309
+ PRIMARY KEY ("guid", "unique"),
310
+ FOREIGN KEY ("guid")
311
+ REFERENCES ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE
312
+ ) WITH ( OIDS=FALSE );`, { connection });
313
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`, { connection });
314
+ }
315
+ /**
316
+ * Create entity tables in the database.
317
+ *
318
+ * @param etype The entity type to create a table for. If this is blank, the default tables are created.
319
+ * @returns True on success, false on failure.
320
+ */
321
+ async createTables(etype = null) {
322
+ const connection = await this.getConnection(true);
323
+ if (etype != null) {
324
+ await this.createEntitiesTable(etype, connection);
325
+ await this.createDataTable(etype, connection);
326
+ await this.createReferencesTable(etype, connection);
327
+ await this.createTokensTable(etype, connection);
328
+ await this.createUniquesTable(etype, connection);
168
329
  }
169
330
  else {
170
- this.queryRunSync(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}uids`)} (
331
+ // Add trigram extensions.
332
+ try {
333
+ await this.queryRun(`CREATE EXTENSION pg_trgm;`, { connection });
334
+ }
335
+ catch (e) {
336
+ // Ignore errors.
337
+ }
338
+ // Create the UID table.
339
+ await this.queryRun(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}uids`)} (
171
340
  "name" TEXT NOT NULL,
172
341
  "cur_uid" BIGINT NOT NULL,
173
342
  PRIMARY KEY ("name")
174
- ) WITH ( OIDS = FALSE );`);
175
- this.queryRunSync(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}uids`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`);
343
+ ) WITH ( OIDS = FALSE );`, { connection });
344
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}uids`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`, { connection });
176
345
  }
346
+ connection.done();
177
347
  return true;
178
348
  }
179
349
  translateQuery(origQuery, origParams) {
@@ -191,158 +361,159 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
191
361
  }
192
362
  async query(runQuery, query, etypes = []) {
193
363
  try {
364
+ this.nymph.config.debugInfo('postgresql:query', query);
194
365
  return await runQuery();
195
366
  }
196
367
  catch (e) {
197
368
  const errorCode = e?.code;
198
- if (errorCode === '42P01' && this.createTables()) {
369
+ if (errorCode === '42P01' && (await this.createTables())) {
370
+ // If the tables don't exist yet, create them.
199
371
  for (let etype of etypes) {
200
- this.createTables(etype);
372
+ await this.createTables(etype);
201
373
  }
202
374
  try {
203
375
  return await runQuery();
204
376
  }
205
377
  catch (e2) {
206
- throw new nymph_1.QueryFailedError('Query failed: ' + e2?.code + ' - ' + e2?.message, query);
378
+ throw new QueryFailedError('Query failed: ' + e2?.code + ' - ' + e2?.message, query, e2?.code);
207
379
  }
208
380
  }
209
- else {
210
- throw e;
211
- }
212
- }
213
- }
214
- querySync(runQuery, query, etypes = []) {
215
- try {
216
- return runQuery();
217
- }
218
- catch (e) {
219
- const errorCode = e?.code;
220
- if (errorCode === '42P01' && this.createTables()) {
221
- for (let etype of etypes) {
222
- this.createTables(etype);
223
- }
224
- try {
225
- return runQuery();
226
- }
227
- catch (e2) {
228
- throw new nymph_1.QueryFailedError('Query failed: ' + e2?.code + ' - ' + e2?.message, query);
229
- }
381
+ else if (errorCode === '23505') {
382
+ throw new EntityUniqueConstraintError(`Unique constraint violation.`);
230
383
  }
231
384
  else {
232
- throw e;
385
+ throw new QueryFailedError('Query failed: ' + e?.code + ' - ' + e?.message, query, e?.code);
233
386
  }
234
387
  }
235
388
  }
236
- queryIter(query, { etypes = [], params = {}, } = {}) {
389
+ queryArray(query, { etypes = [], params = {}, } = {}) {
237
390
  const { query: newQuery, params: newParams } = this.translateQuery(query, params);
238
391
  return this.query(async () => {
239
- const results = await new Promise((resolve, reject) => {
392
+ const results = await new Promise(async (resolve, reject) => {
393
+ // Only do one query at a time in transations. The Postgres
394
+ // package will only support one query at a time on a given
395
+ // connection in a future release.
396
+ let myResolve = () => { };
397
+ const myPromise = new Promise((resolve) => {
398
+ myResolve = resolve;
399
+ });
400
+ if (this.transaction) {
401
+ const latestPromise = this.transaction.latestPromise;
402
+ this.transaction.latestPromise = myPromise;
403
+ await latestPromise;
404
+ }
240
405
  try {
241
- (this.transaction?.connection?.client ?? this.link)
242
- .query(newQuery, newParams)
243
- .then((results) => resolve(results), (error) => reject(error));
406
+ const results = await (this.transaction?.connection?.client ?? this.link).query(newQuery, newParams);
407
+ resolve(results);
244
408
  }
245
409
  catch (e) {
246
410
  reject(e);
247
411
  }
412
+ myResolve();
248
413
  });
249
414
  return results.rows;
250
415
  }, `${query} -- ${JSON.stringify(params)}`, etypes);
251
416
  }
252
- queryIterSync(query, { etypes = [], params = {}, } = {}) {
417
+ async queryIter(query, { etypes = [], params = {}, } = {}) {
253
418
  const { query: newQuery, params: newParams } = this.translateQuery(query, params);
254
- return this.querySync(() => {
255
- const output = child_process_1.default.spawnSync(process.argv0, [__dirname + '/runPostgresqlSync.js'], {
256
- input: JSON.stringify({
257
- postgresqlConfig: this.postgresqlConfig,
258
- query: newQuery,
259
- params: newParams,
260
- }),
261
- timeout: 30000,
262
- maxBuffer: 100 * 1024 * 1024,
263
- encoding: 'utf8',
264
- windowsHide: true,
419
+ const that = this;
420
+ return this.query(async function* () {
421
+ // Only do one query at a time in transations. The Postgres
422
+ // package will only support one query at a time on a given
423
+ // connection in a future release.
424
+ let myResolve = () => { };
425
+ const myPromise = new Promise((resolve) => {
426
+ myResolve = resolve;
265
427
  });
266
- try {
267
- return JSON.parse(output.stdout).rows;
268
- }
269
- catch (e) {
428
+ if (that.transaction) {
429
+ const latestPromise = that.transaction.latestPromise;
430
+ that.transaction.latestPromise = myPromise;
431
+ await latestPromise;
270
432
  }
271
- if (output.status === 0) {
272
- throw new Error('Unknown parse error.');
273
- }
274
- const err = JSON.parse(output.stderr);
275
- const e = new Error(err.name);
276
- for (const name in err) {
277
- e[name] = err[name];
433
+ const transaction = !!that.transaction?.connection;
434
+ const connection = await that.getConnection();
435
+ const cursor = new Cursor(newQuery, newParams);
436
+ const iter = connection.client.query(cursor);
437
+ while (true) {
438
+ const rows = await iter.read(100);
439
+ myResolve();
440
+ if (!rows.length) {
441
+ await new Promise((resolve) => {
442
+ iter.close(() => {
443
+ if (!transaction) {
444
+ connection.done();
445
+ }
446
+ resolve();
447
+ });
448
+ });
449
+ return;
450
+ }
451
+ for (let row of rows) {
452
+ yield row;
453
+ }
278
454
  }
279
455
  }, `${query} -- ${JSON.stringify(params)}`, etypes);
280
456
  }
281
457
  queryGet(query, { etypes = [], params = {}, } = {}) {
282
458
  const { query: newQuery, params: newParams } = this.translateQuery(query, params);
283
459
  return this.query(async () => {
284
- const results = await new Promise((resolve, reject) => {
460
+ const results = await new Promise(async (resolve, reject) => {
461
+ // Only do one query at a time in transations. The Postgres
462
+ // package will only support one query at a time on a given
463
+ // connection in a future release.
464
+ let myResolve = () => { };
465
+ const myPromise = new Promise((resolve) => {
466
+ myResolve = resolve;
467
+ });
468
+ if (this.transaction) {
469
+ const latestPromise = this.transaction.latestPromise;
470
+ this.transaction.latestPromise = myPromise;
471
+ await latestPromise;
472
+ }
285
473
  try {
286
- (this.transaction?.connection?.client ?? this.link)
287
- .query(newQuery, newParams)
288
- .then((results) => resolve(results), (error) => reject(error));
474
+ const results = await (this.transaction?.connection?.client ?? this.link).query(newQuery, newParams);
475
+ resolve(results);
289
476
  }
290
477
  catch (e) {
291
478
  reject(e);
292
479
  }
480
+ myResolve();
293
481
  });
294
482
  return results.rows[0];
295
483
  }, `${query} -- ${JSON.stringify(params)}`, etypes);
296
484
  }
297
- queryRun(query, { etypes = [], params = {}, } = {}) {
485
+ queryRun(query, { etypes = [], params = {}, transaction, connection, } = {}) {
298
486
  const { query: newQuery, params: newParams } = this.translateQuery(query, params);
299
487
  return this.query(async () => {
300
- const results = await new Promise((resolve, reject) => {
488
+ const results = await new Promise(async (resolve, reject) => {
489
+ // Only do one query at a time in transations. The Postgres
490
+ // package will only support one query at a time on a given
491
+ // connection in a future release.
492
+ let myResolve = () => { };
493
+ const myPromise = new Promise((resolve) => {
494
+ myResolve = resolve;
495
+ });
496
+ const trans = transaction ?? this.transaction;
497
+ if (trans) {
498
+ const latestPromise = trans.latestPromise;
499
+ trans.latestPromise = myPromise;
500
+ await latestPromise;
501
+ }
301
502
  try {
302
- (this.transaction?.connection?.client ?? this.link)
303
- .query(newQuery, newParams)
304
- .then((results) => resolve(results), (error) => reject(error));
503
+ const results = await ((connection ?? trans?.connection)?.client ?? this.link).query(newQuery, newParams);
504
+ resolve(results);
305
505
  }
306
506
  catch (e) {
307
507
  reject(e);
308
508
  }
509
+ myResolve();
309
510
  });
310
511
  return { rowCount: results.rowCount ?? 0 };
311
512
  }, `${query} -- ${JSON.stringify(params)}`, etypes);
312
513
  }
313
- queryRunSync(query, { etypes = [], params = {}, } = {}) {
314
- const { query: newQuery, params: newParams } = this.translateQuery(query, params);
315
- return this.querySync(() => {
316
- const output = child_process_1.default.spawnSync(process.argv0, [__dirname + '/runPostgresqlSync.js'], {
317
- input: JSON.stringify({
318
- postgresqlConfig: this.postgresqlConfig,
319
- query: newQuery,
320
- params: newParams,
321
- }),
322
- timeout: 30000,
323
- maxBuffer: 100 * 1024 * 1024,
324
- encoding: 'utf8',
325
- windowsHide: true,
326
- });
327
- try {
328
- const results = JSON.parse(output.stdout);
329
- return { rowCount: results.rowCount ?? 0 };
330
- }
331
- catch (e) {
332
- }
333
- if (output.status === 0) {
334
- throw new Error('Unknown parse error.');
335
- }
336
- const err = JSON.parse(output.stderr);
337
- const e = new Error(err.name);
338
- for (const name in err) {
339
- e[name] = err[name];
340
- }
341
- }, `${query} -- ${JSON.stringify(params)}`, etypes);
342
- }
343
514
  async commit(name) {
344
515
  if (name == null || typeof name !== 'string' || name.length === 0) {
345
- throw new nymph_1.InvalidParametersError('Transaction commit attempted without a name.');
516
+ throw new InvalidParametersError('Transaction commit attempted without a name.');
346
517
  }
347
518
  if (!this.transaction || this.transaction.count === 0) {
348
519
  this.transaction = null;
@@ -382,32 +553,40 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
382
553
  guid,
383
554
  },
384
555
  });
385
- await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
556
+ await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
557
+ etypes: [etype],
558
+ params: {
559
+ guid,
560
+ },
561
+ });
562
+ await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
386
563
  etypes: [etype],
387
564
  params: {
388
565
  guid,
389
566
  },
390
567
  });
391
- await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
568
+ await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
392
569
  etypes: [etype],
393
570
  params: {
394
571
  guid,
395
572
  },
396
573
  });
397
- await this.commit('nymph-delete');
398
- if (this.nymph.config.cache) {
399
- this.cleanCache(guid);
400
- }
401
- return true;
402
574
  }
403
575
  catch (e) {
576
+ this.nymph.config.debugError('postgresql', `Delete entity error: "${e}"`);
404
577
  await this.rollback('nymph-delete');
405
578
  throw e;
406
579
  }
580
+ await this.commit('nymph-delete');
581
+ // Remove any cached versions of this entity.
582
+ if (this.nymph.config.cache) {
583
+ this.cleanCache(guid);
584
+ }
585
+ return true;
407
586
  }
408
587
  async deleteUID(name) {
409
588
  if (!name) {
410
- throw new nymph_1.InvalidParametersError('Name not given for UID');
589
+ throw new InvalidParametersError('Name not given for UID');
411
590
  }
412
591
  await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}uids`)} WHERE "name"=@name;`, {
413
592
  params: {
@@ -416,79 +595,223 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
416
595
  });
417
596
  return true;
418
597
  }
419
- async exportEntities(writeLine) {
420
- writeLine('#nex2');
421
- writeLine('# Nymph Entity Exchange v2');
422
- writeLine('# http://nymph.io');
423
- writeLine('#');
424
- writeLine('# Generation Time: ' + new Date().toLocaleString());
425
- writeLine('');
426
- writeLine('#');
427
- writeLine('# UIDs');
428
- writeLine('#');
429
- writeLine('');
430
- let uids = await this.queryIter(`SELECT * FROM ${PostgreSQLDriver.escape(`${this.prefix}uids`)} ORDER BY "name";`);
431
- for (const uid of uids) {
432
- writeLine(`<${uid.name}>[${uid.cur_uid}]`);
433
- }
434
- writeLine('');
435
- writeLine('#');
436
- writeLine('# Entities');
437
- writeLine('#');
438
- writeLine('');
439
- const tables = await this.queryIter('SELECT relname FROM pg_stat_user_tables ORDER BY relname;');
598
+ async getIndexes(etype) {
599
+ const indexes = [];
600
+ for (let [scope, suffix] of [
601
+ ['data', '_json'],
602
+ ['references', '_reference_guid'],
603
+ ['tokens', '_token_position_stem'],
604
+ ]) {
605
+ const indexDefinitions = await this.queryArray(`SELECT * FROM "pg_indexes" WHERE "indexname" LIKE @pattern;`, {
606
+ params: {
607
+ pattern: `${this.prefix}${scope}_${etype}_id_custom_%${suffix}`,
608
+ },
609
+ });
610
+ for (const indexDefinition of indexDefinitions) {
611
+ indexes.push({
612
+ scope,
613
+ name: indexDefinition.indexname.substring(`${this.prefix}${scope}_${etype}_id_custom_`.length, indexDefinition.indexname.length - suffix.length),
614
+ property: (indexDefinition.indexdef.match(/WHERE\s+\(?\s*name\s*=\s*'(.*)'/) ?? [])[1] ?? '',
615
+ });
616
+ }
617
+ }
618
+ return indexes;
619
+ }
620
+ async addIndex(etype, definition) {
621
+ this.checkIndexName(definition.name);
622
+ await this.deleteIndex(etype, definition.scope, definition.name);
623
+ const connection = await this.getConnection(true);
624
+ if (definition.scope === 'data') {
625
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${definition.name}_json`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING gin ("json") WHERE "name"=${PostgreSQLDriver.escapeValue(definition.property)};`, { connection });
626
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${definition.name}_string_gin`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING gin ("string" gin_trgm_ops) WHERE "name"=${PostgreSQLDriver.escapeValue(definition.property)};`, { connection });
627
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${definition.name}_string_btree`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree (LEFT("string", 1024)) WHERE "name"=${PostgreSQLDriver.escapeValue(definition.property)};`, { connection });
628
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${definition.name}_number`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("number") WHERE "name"=${PostgreSQLDriver.escapeValue(definition.property)};`, { connection });
629
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${definition.name}_truthy`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("truthy") WHERE "name"=${PostgreSQLDriver.escapeValue(definition.property)};`, { connection });
630
+ }
631
+ else if (definition.scope === 'references') {
632
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_custom_${definition.name}_reference_guid`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("reference", "guid") WHERE "name"=${PostgreSQLDriver.escapeValue(definition.property)};`, { connection });
633
+ }
634
+ else if (definition.scope === 'tokens') {
635
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}_id_custom_${definition.name}_token_position_stem`)} ON ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} USING btree ("token", "position", "stem") WHERE "name"=${PostgreSQLDriver.escapeValue(definition.property)};`, { connection });
636
+ }
637
+ connection.done();
638
+ return true;
639
+ }
640
+ async deleteIndex(etype, scope, name) {
641
+ this.checkIndexName(name);
642
+ const connection = await this.getConnection(true);
643
+ if (scope === 'data') {
644
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${name}_json`)};`, { connection });
645
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${name}_string_gin`)};`, { connection });
646
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${name}_string_btree`)};`, { connection });
647
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${name}_number`)};`, { connection });
648
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${name}_truthy`)};`, { connection });
649
+ }
650
+ else if (scope === 'references') {
651
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_custom_${name}_reference_guid`)};`, { connection });
652
+ }
653
+ else if (scope === 'tokens') {
654
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}_id_custom_${name}_token_position_stem`)};`, { connection });
655
+ }
656
+ connection.done();
657
+ return true;
658
+ }
659
+ async getEtypes() {
660
+ const tables = await this.queryArray('SELECT "table_name" AS "table_name" FROM "information_schema"."tables" WHERE "table_catalog"=@db AND "table_schema"=\'public\' AND "table_name" LIKE @prefix;', {
661
+ params: {
662
+ db: this.config.database,
663
+ prefix: this.prefix + 'entities_' + '%',
664
+ },
665
+ });
440
666
  const etypes = [];
441
- for (const tableRow of tables) {
442
- const table = tableRow.relname;
443
- if (table.startsWith(this.prefix + 'entities_')) {
444
- etypes.push(table.substr((this.prefix + 'entities_').length));
667
+ for (const table of tables) {
668
+ etypes.push(table.table_name.substr((this.prefix + 'entities_').length));
669
+ }
670
+ return etypes;
671
+ }
672
+ async *exportDataIterator() {
673
+ if (yield {
674
+ type: 'comment',
675
+ content: `#nex2
676
+ # Nymph Entity Exchange v2
677
+ # http://nymph.io
678
+ #
679
+ # Generation Time: ${new Date().toLocaleString()}
680
+ `,
681
+ }) {
682
+ return;
683
+ }
684
+ if (yield {
685
+ type: 'comment',
686
+ content: `
687
+
688
+ #
689
+ # UIDs
690
+ #
691
+
692
+ `,
693
+ }) {
694
+ return;
695
+ }
696
+ // Export UIDs.
697
+ let uids = await this.queryIter(`SELECT * FROM ${PostgreSQLDriver.escape(`${this.prefix}uids`)} ORDER BY "name";`);
698
+ for await (const uid of uids) {
699
+ if (yield { type: 'uid', content: `<${uid.name}>[${uid.cur_uid}]\n` }) {
700
+ return;
445
701
  }
446
702
  }
703
+ if (yield {
704
+ type: 'comment',
705
+ content: `
706
+
707
+ #
708
+ # Entities
709
+ #
710
+
711
+ `,
712
+ }) {
713
+ return;
714
+ }
715
+ // Get the etypes.
716
+ const etypes = await this.getEtypes();
447
717
  for (const etype of etypes) {
448
- const dataIterator = (await this.queryIter(`SELECT encode(e."guid", 'hex') AS "guid", e."tags", e."cdate", e."mdate", d."name" AS "dname", d."value" AS "dvalue", c."string", c."number"
718
+ // Export entities.
719
+ const dataIterator = await this.queryIter(`SELECT encode(e."guid", 'hex') AS "guid", e."tags", e."cdate", e."mdate", encode(e."user", 'hex') AS "user", encode(e."group", 'hex') AS "group", e."acUser", e."acGroup", e."acOther", array(SELECT encode(n, 'hex') FROM unnest(e."acRead") AS n) as "acRead", array(SELECT encode(n, 'hex') FROM unnest(e."acWrite") AS n) as "acWrite", array(SELECT encode(n, 'hex') FROM unnest(e."acFull") AS n) as "acFull", d."name", d."value", d."json", d."string", d."number"
449
720
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} e
450
721
  LEFT JOIN ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} d ON e."guid"=d."guid"
451
- INNER JOIN ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} c ON d."guid"=c."guid" AND d."name"=c."name"
452
- ORDER BY e."guid";`))[Symbol.iterator]();
453
- let datum = dataIterator.next();
722
+ ORDER BY e."guid";`);
723
+ let datum = await dataIterator.next();
454
724
  while (!datum.done) {
455
725
  const guid = datum.value.guid;
456
- const tags = datum.value.tags.join(',');
726
+ const tags = datum.value.tags.filter((tag) => tag).join(',');
457
727
  const cdate = datum.value.cdate;
458
728
  const mdate = datum.value.mdate;
459
- writeLine(`{${guid}}<${etype}>[${tags}]`);
460
- writeLine(`\tcdate=${JSON.stringify(cdate)}`);
461
- writeLine(`\tmdate=${JSON.stringify(mdate)}`);
462
- if (datum.value.dname != null) {
729
+ const user = datum.value.user;
730
+ const group = datum.value.group;
731
+ const acUser = datum.value.acUser;
732
+ const acGroup = datum.value.acGroup;
733
+ const acOther = datum.value.acOther;
734
+ const acRead = datum.value.acRead?.filter((guid) => guid);
735
+ const acWrite = datum.value.acWrite?.filter((guid) => guid);
736
+ const acFull = datum.value.acFull?.filter((guid) => guid);
737
+ let currentEntityExport = [];
738
+ currentEntityExport.push(`{${guid}}<${etype}>[${tags}]`);
739
+ currentEntityExport.push(`\tcdate=${JSON.stringify(cdate)}`);
740
+ currentEntityExport.push(`\tmdate=${JSON.stringify(mdate)}`);
741
+ if (this.nymph.tilmeld != null) {
742
+ if (user != null) {
743
+ currentEntityExport.push(`\tuser=${JSON.stringify(['nymph_entity_reference', user, 'User'])}`);
744
+ }
745
+ if (group != null) {
746
+ currentEntityExport.push(`\tgroup=${JSON.stringify(['nymph_entity_reference', group, 'Group'])}`);
747
+ }
748
+ if (acUser != null) {
749
+ currentEntityExport.push(`\tacUser=${JSON.stringify(acUser)}`);
750
+ }
751
+ if (acGroup != null) {
752
+ currentEntityExport.push(`\tacGroup=${JSON.stringify(acGroup)}`);
753
+ }
754
+ if (acOther != null) {
755
+ currentEntityExport.push(`\tacOther=${JSON.stringify(acOther)}`);
756
+ }
757
+ if (acRead != null) {
758
+ currentEntityExport.push(`\tacRead=${JSON.stringify(acRead)}`);
759
+ }
760
+ if (acWrite != null) {
761
+ currentEntityExport.push(`\tacWrite=${JSON.stringify(acWrite)}`);
762
+ }
763
+ if (acFull != null) {
764
+ currentEntityExport.push(`\tacFull=${JSON.stringify(acFull)}`);
765
+ }
766
+ }
767
+ if (datum.value.name != null) {
768
+ // This do will keep going and adding the data until the
769
+ // next entity is reached. $row will end on the next entity.
463
770
  do {
464
- const value = datum.value.dvalue === 'N'
771
+ const value = datum.value.value === 'N'
465
772
  ? JSON.stringify(Number(datum.value.number))
466
- : datum.value.dvalue === 'S'
467
- ? JSON.stringify(datum.value.string)
468
- : datum.value.dvalue;
469
- writeLine(`\t${datum.value.dname}=${value}`);
470
- datum = dataIterator.next();
773
+ : datum.value.value === 'S'
774
+ ? JSON.stringify(PostgreSQLDriver.unescapeNulls(datum.value.string))
775
+ : datum.value.value === 'J'
776
+ ? PostgreSQLDriver.unescapeNullSequences(JSON.stringify(datum.value.json))
777
+ : datum.value.value;
778
+ currentEntityExport.push(`\t${datum.value.name}=${value}`);
779
+ datum = await dataIterator.next();
471
780
  } while (!datum.done && datum.value.guid === guid);
472
781
  }
473
782
  else {
474
- datum = dataIterator.next();
783
+ // Make sure that datum is incremented :)
784
+ datum = await dataIterator.next();
785
+ }
786
+ currentEntityExport.push('');
787
+ if (yield { type: 'entity', content: currentEntityExport.join('\n') }) {
788
+ return;
475
789
  }
476
790
  }
477
791
  }
478
- return;
479
792
  }
480
- makeEntityQuery(options, formattedSelectors, etype, count = { i: 0 }, params = {}, subquery = false, tableSuffix = '', etypes = []) {
793
+ /**
794
+ * Generate the PostgreSQL query.
795
+ * @param options The options array.
796
+ * @param formattedSelectors The formatted selector array.
797
+ * @param etype
798
+ * @param count Used to track internal params.
799
+ * @param params Used to store internal params.
800
+ * @param subquery Whether only a subquery should be returned.
801
+ * @returns The SQL query.
802
+ */
803
+ makeEntityQuery(options, formattedSelectors, etype, count = { i: 0 }, params = {}, subquery = false, tableSuffix = '', etypes = [], guidSelector = undefined, guidExplicitSelector = undefined) {
481
804
  if (typeof options.class?.alterOptions === 'function') {
482
805
  options = options.class.alterOptions(options);
483
806
  }
484
807
  const eTable = `e${tableSuffix}`;
485
808
  const dTable = `d${tableSuffix}`;
486
- const cTable = `c${tableSuffix}`;
487
809
  const fTable = `f${tableSuffix}`;
488
810
  const ieTable = `ie${tableSuffix}`;
489
811
  const countTable = `count${tableSuffix}`;
490
- const sort = options.sort ?? 'cdate';
491
- const queryParts = this.iterateSelectorsForQuery(formattedSelectors, (key, value, typeIsOr, typeIsNot) => {
812
+ const sTable = `s${tableSuffix}`;
813
+ const sort = options.sort === undefined ? 'cdate' : options.sort;
814
+ const queryParts = this.iterateSelectorsForQuery(formattedSelectors, ({ key, value, typeIsOr, typeIsNot }) => {
492
815
  const clauseNot = key.startsWith('!');
493
816
  let curQuery = '';
494
817
  for (const curValue of value) {
@@ -501,7 +824,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
501
824
  }
502
825
  const guid = `param${++count.i}`;
503
826
  curQuery +=
504
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
827
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
505
828
  ieTable +
506
829
  '."guid"=decode(@' +
507
830
  guid +
@@ -517,7 +840,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
517
840
  }
518
841
  const tag = `param${++count.i}`;
519
842
  curQuery +=
520
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
843
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
521
844
  '@' +
522
845
  tag +
523
846
  ' <@ ie."tags"';
@@ -530,17 +853,38 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
530
853
  if (curQuery) {
531
854
  curQuery += typeIsOr ? ' OR ' : ' AND ';
532
855
  }
533
- const name = `param${++count.i}`;
534
- curQuery +=
535
- ieTable +
536
- '."guid" ' +
537
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
538
- 'IN (SELECT "guid" FROM ' +
539
- PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
540
- ' WHERE "name"=@' +
541
- name +
542
- ')';
543
- params[name] = curVar;
856
+ if (curVar === 'cdate' ||
857
+ curVar === 'mdate' ||
858
+ (this.nymph.tilmeld != null &&
859
+ (curVar === 'user' ||
860
+ curVar === 'group' ||
861
+ curVar === 'acUser' ||
862
+ curVar === 'acGroup' ||
863
+ curVar === 'acOther' ||
864
+ curVar === 'acRead' ||
865
+ curVar === 'acWrite' ||
866
+ curVar === 'acFull'))) {
867
+ curQuery +=
868
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
869
+ '(' +
870
+ ieTable +
871
+ '.' +
872
+ PostgreSQLDriver.escape(curVar) +
873
+ ' IS NOT NULL)';
874
+ }
875
+ else {
876
+ const name = `param${++count.i}`;
877
+ curQuery +=
878
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
879
+ 'EXISTS (SELECT "guid" FROM ' +
880
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
881
+ ' WHERE "guid"=' +
882
+ ieTable +
883
+ '."guid" AND "name"=@' +
884
+ name +
885
+ ')';
886
+ params[name] = curVar;
887
+ }
544
888
  }
545
889
  break;
546
890
  case 'truthy':
@@ -549,30 +893,34 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
549
893
  if (curQuery) {
550
894
  curQuery += typeIsOr ? ' OR ' : ' AND ';
551
895
  }
552
- if (curVar === 'cdate') {
553
- curQuery +=
554
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
555
- '(' +
556
- ieTable +
557
- '."cdate" NOT NULL)';
558
- break;
559
- }
560
- else if (curVar === 'mdate') {
896
+ if (curVar === 'cdate' ||
897
+ curVar === 'mdate' ||
898
+ (this.nymph.tilmeld != null &&
899
+ (curVar === 'user' ||
900
+ curVar === 'group' ||
901
+ curVar === 'acUser' ||
902
+ curVar === 'acGroup' ||
903
+ curVar === 'acOther' ||
904
+ curVar === 'acRead' ||
905
+ curVar === 'acWrite' ||
906
+ curVar === 'acFull'))) {
561
907
  curQuery +=
562
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
908
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
563
909
  '(' +
564
910
  ieTable +
565
- '."mdate" NOT NULL)';
566
- break;
911
+ '.' +
912
+ PostgreSQLDriver.escape(curVar) +
913
+ ' IS NOT NULL)';
567
914
  }
568
915
  else {
569
916
  const name = `param${++count.i}`;
570
917
  curQuery +=
571
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
918
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
919
+ 'EXISTS (SELECT "guid" FROM ' +
920
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
921
+ ' WHERE "guid"=' +
572
922
  ieTable +
573
- '."guid" IN (SELECT "guid" FROM ' +
574
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
575
- ' WHERE "name"=@' +
923
+ '."guid" AND "name"=@' +
576
924
  name +
577
925
  ' AND "truthy"=TRUE)';
578
926
  params[name] = curVar;
@@ -581,35 +929,63 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
581
929
  break;
582
930
  case 'equal':
583
931
  case '!equal':
584
- if (curValue[0] === 'cdate') {
932
+ if (curValue[0] === 'cdate' ||
933
+ curValue[0] === 'mdate' ||
934
+ (this.nymph.tilmeld != null &&
935
+ (curValue[0] === 'acUser' ||
936
+ curValue[0] === 'acGroup' ||
937
+ curValue[0] === 'acOther'))) {
585
938
  if (curQuery) {
586
939
  curQuery += typeIsOr ? ' OR ' : ' AND ';
587
940
  }
588
- const cdate = `param${++count.i}`;
941
+ const value = `param${++count.i}`;
589
942
  curQuery +=
590
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
943
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
591
944
  ieTable +
592
- '."cdate"=@' +
593
- cdate;
594
- params[cdate] = isNaN(Number(curValue[1]))
945
+ '.' +
946
+ PostgreSQLDriver.escape(curValue[0]) +
947
+ '=@' +
948
+ value;
949
+ params[value] = isNaN(Number(curValue[1]))
595
950
  ? null
596
951
  : Number(curValue[1]);
597
- break;
598
952
  }
599
- else if (curValue[0] === 'mdate') {
953
+ else if (this.nymph.tilmeld != null &&
954
+ (curValue[0] === 'user' || curValue[0] === 'group')) {
600
955
  if (curQuery) {
601
956
  curQuery += typeIsOr ? ' OR ' : ' AND ';
602
957
  }
603
- const mdate = `param${++count.i}`;
958
+ const value = `param${++count.i}`;
604
959
  curQuery +=
605
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
960
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
606
961
  ieTable +
607
- '."mdate"=@' +
608
- mdate;
609
- params[mdate] = isNaN(Number(curValue[1]))
610
- ? null
611
- : Number(curValue[1]);
612
- break;
962
+ '.' +
963
+ PostgreSQLDriver.escape(curValue[0]) +
964
+ '=decode(@' +
965
+ value +
966
+ ", 'hex')";
967
+ params[value] = `${curValue[1]}`;
968
+ }
969
+ else if (this.nymph.tilmeld != null &&
970
+ (curValue[0] === 'acRead' ||
971
+ curValue[0] === 'acWrite' ||
972
+ curValue[0] === 'acFull')) {
973
+ if (curQuery) {
974
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
975
+ }
976
+ const value = `param${++count.i}`;
977
+ curQuery +=
978
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
979
+ ieTable +
980
+ '.' +
981
+ PostgreSQLDriver.escape(curValue[0]) +
982
+ '=' +
983
+ !curValue[1]?.length
984
+ ? '@' + value
985
+ : "array(SELECT decode(n, 'hex') FROM unnest(@" +
986
+ value +
987
+ '::text[]) AS n)';
988
+ params[value] = Array.isArray(curValue[1]) ? curValue[1] : '';
613
989
  }
614
990
  else if (typeof curValue[1] === 'number') {
615
991
  if (curQuery) {
@@ -618,11 +994,12 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
618
994
  const name = `param${++count.i}`;
619
995
  const value = `param${++count.i}`;
620
996
  curQuery +=
621
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
997
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
998
+ 'EXISTS (SELECT "guid" FROM ' +
999
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1000
+ ' WHERE "guid"=' +
622
1001
  ieTable +
623
- '."guid" IN (SELECT "guid" FROM ' +
624
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
625
- ' WHERE "name"=@' +
1002
+ '."guid" AND "name"=@' +
626
1003
  name +
627
1004
  ' AND "number"=@' +
628
1005
  value +
@@ -637,17 +1014,20 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
637
1014
  const name = `param${++count.i}`;
638
1015
  const value = `param${++count.i}`;
639
1016
  curQuery +=
640
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1017
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1018
+ 'EXISTS (SELECT "guid" FROM ' +
1019
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1020
+ ' WHERE "guid"=' +
641
1021
  ieTable +
642
- '."guid" IN (SELECT "guid" FROM ' +
643
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
644
- ' WHERE "name"=@' +
1022
+ '."guid" AND "name"=@' +
645
1023
  name +
646
- ' AND "string"=@' +
647
- value +
1024
+ ' AND "string"=' +
1025
+ (curValue[1].length < 512
1026
+ ? 'LEFT(@' + value + ', 512)'
1027
+ : '@' + value) +
648
1028
  ')';
649
1029
  params[name] = curValue[0];
650
- params[value] = curValue[1];
1030
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
651
1031
  }
652
1032
  else {
653
1033
  if (curQuery) {
@@ -664,138 +1044,272 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
664
1044
  const name = `param${++count.i}`;
665
1045
  const value = `param${++count.i}`;
666
1046
  curQuery +=
667
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
668
- ieTable +
669
- '."guid" IN (SELECT "guid" FROM ' +
1047
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1048
+ 'EXISTS (SELECT "guid" FROM ' +
670
1049
  PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
671
- ' WHERE "name"=@' +
1050
+ ' WHERE "guid"=' +
1051
+ ieTable +
1052
+ '."guid" AND "name"=@' +
672
1053
  name +
673
- ' AND "value"=@' +
1054
+ ' AND "json"=@' +
674
1055
  value +
675
1056
  ')';
676
1057
  params[name] = curValue[0];
677
- params[value] = svalue;
1058
+ params[value] = PostgreSQLDriver.escapeNullSequences(svalue);
678
1059
  }
679
1060
  break;
680
1061
  case 'contain':
681
1062
  case '!contain':
682
- if (curValue[0] === 'cdate') {
1063
+ if (curValue[0] === 'cdate' ||
1064
+ curValue[0] === 'mdate' ||
1065
+ (this.nymph.tilmeld != null &&
1066
+ (curValue[0] === 'acUser' ||
1067
+ curValue[0] === 'acGroup' ||
1068
+ curValue[0] === 'acOther'))) {
683
1069
  if (curQuery) {
684
1070
  curQuery += typeIsOr ? ' OR ' : ' AND ';
685
1071
  }
686
- const cdate = `param${++count.i}`;
1072
+ const value = `param${++count.i}`;
687
1073
  curQuery +=
688
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1074
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
689
1075
  ieTable +
690
- '."cdate"=' +
691
- cdate;
692
- params[cdate] = isNaN(Number(curValue[1]))
1076
+ '.' +
1077
+ PostgreSQLDriver.escape(curValue[0]) +
1078
+ '=@' +
1079
+ value;
1080
+ params[value] = isNaN(Number(curValue[1]))
693
1081
  ? null
694
1082
  : Number(curValue[1]);
695
- break;
696
1083
  }
697
- else if (curValue[0] === 'mdate') {
1084
+ else if (this.nymph.tilmeld != null &&
1085
+ (curValue[0] === 'user' || curValue[0] === 'group')) {
698
1086
  if (curQuery) {
699
1087
  curQuery += typeIsOr ? ' OR ' : ' AND ';
700
1088
  }
701
- const mdate = `param${++count.i}`;
1089
+ const value = `param${++count.i}`;
702
1090
  curQuery +=
703
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1091
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
704
1092
  ieTable +
705
- '."mdate"=' +
706
- mdate;
707
- params[mdate] = isNaN(Number(curValue[1]))
708
- ? null
709
- : Number(curValue[1]);
710
- break;
1093
+ '.' +
1094
+ PostgreSQLDriver.escape(curValue[0]) +
1095
+ '=decode(@' +
1096
+ value +
1097
+ ", 'hex')";
1098
+ params[value] = `${curValue[1]}`;
1099
+ }
1100
+ else if (this.nymph.tilmeld != null &&
1101
+ (curValue[0] === 'acRead' ||
1102
+ curValue[0] === 'acWrite' ||
1103
+ curValue[0] === 'acFull')) {
1104
+ if (curQuery) {
1105
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1106
+ }
1107
+ const value = `param${++count.i}`;
1108
+ curQuery +=
1109
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1110
+ 'decode(@' +
1111
+ value +
1112
+ ", 'hex')=ANY(" +
1113
+ ieTable +
1114
+ '.' +
1115
+ PostgreSQLDriver.escape(curValue[0]) +
1116
+ ')';
1117
+ params[value] = `${curValue[1]}`;
711
1118
  }
712
1119
  else {
713
1120
  if (curQuery) {
714
1121
  curQuery += typeIsOr ? ' OR ' : ' AND ';
715
1122
  }
716
1123
  let svalue;
717
- let stringValue;
718
1124
  if (curValue[1] instanceof Object &&
719
1125
  typeof curValue[1].toReference === 'function') {
720
1126
  svalue = JSON.stringify(curValue[1].toReference());
721
- stringValue = `${curValue[1].toReference()}`;
722
1127
  }
723
- else {
1128
+ else if (typeof curValue[1] === 'string' ||
1129
+ typeof curValue[1] === 'number') {
724
1130
  svalue = JSON.stringify(curValue[1]);
725
- stringValue = `${curValue[1]}`;
1131
+ }
1132
+ else {
1133
+ svalue = JSON.stringify([curValue[1]]);
726
1134
  }
727
1135
  const name = `param${++count.i}`;
728
1136
  const value = `param${++count.i}`;
729
- if (typeof curValue[1] === 'string') {
730
- const stringParam = `param${++count.i}`;
1137
+ curQuery +=
1138
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1139
+ 'EXISTS (SELECT "guid" FROM ' +
1140
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1141
+ ' WHERE "guid"=' +
1142
+ ieTable +
1143
+ '."guid" AND "name"=@' +
1144
+ name +
1145
+ ' AND "json" @> @' +
1146
+ value +
1147
+ ')';
1148
+ params[name] = curValue[0];
1149
+ params[value] = PostgreSQLDriver.escapeNullSequences(svalue);
1150
+ }
1151
+ break;
1152
+ case 'search':
1153
+ case '!search':
1154
+ if (curValue[0] === 'cdate' ||
1155
+ curValue[0] === 'mdate' ||
1156
+ (this.nymph.tilmeld != null &&
1157
+ (curValue[0] === 'user' ||
1158
+ curValue[0] === 'group' ||
1159
+ curValue[0] === 'acUser' ||
1160
+ curValue[0] === 'acGroup' ||
1161
+ curValue[0] === 'acOther' ||
1162
+ curValue[0] === 'acRead' ||
1163
+ curValue[0] === 'acWrite' ||
1164
+ curValue[0] === 'acFull'))) {
1165
+ if (curQuery) {
1166
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1167
+ }
1168
+ curQuery +=
1169
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') + '(FALSE)';
1170
+ }
1171
+ else {
1172
+ if (curQuery) {
1173
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1174
+ }
1175
+ const parsedFTSQuery = this.tokenizer.parseSearchQuery(curValue[1]);
1176
+ if (!parsedFTSQuery.length) {
731
1177
  curQuery +=
732
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
733
- '(' +
1178
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') + '(FALSE)';
1179
+ }
1180
+ else {
1181
+ const name = `param${++count.i}`;
1182
+ const queryPartToken = (term) => {
1183
+ const value = `param${++count.i}`;
1184
+ params[value] = term.token;
1185
+ return ('EXISTS (SELECT "guid" FROM ' +
1186
+ PostgreSQLDriver.escape(this.prefix + 'tokens_' + etype) +
1187
+ ' WHERE "guid"=' +
734
1188
  ieTable +
735
- '."guid" IN (SELECT "guid" FROM ' +
736
- PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
737
- ' WHERE "name"=@' +
1189
+ '."guid" AND "name"=@' +
738
1190
  name +
739
- ' AND position(@' +
1191
+ ' AND "token"=@' +
740
1192
  value +
741
- ' IN "value")>0) OR ' +
1193
+ (term.nostemmed ? ' AND "stem"=FALSE' : '') +
1194
+ ')');
1195
+ };
1196
+ const queryPartSeries = (series) => {
1197
+ const tokenTableSuffix = makeTableSuffix();
1198
+ const tokenParts = series.tokens.map((token, i) => {
1199
+ const value = `param${++count.i}`;
1200
+ params[value] = token.token;
1201
+ return {
1202
+ fromClause: i === 0
1203
+ ? 'FROM ' +
1204
+ PostgreSQLDriver.escape(this.prefix + 'tokens_' + etype) +
1205
+ ' t' +
1206
+ tokenTableSuffix +
1207
+ '0'
1208
+ : 'JOIN ' +
1209
+ PostgreSQLDriver.escape(this.prefix + 'tokens_' + etype) +
1210
+ ' t' +
1211
+ tokenTableSuffix +
1212
+ i +
1213
+ ' ON t' +
1214
+ tokenTableSuffix +
1215
+ i +
1216
+ '."guid" = t' +
1217
+ tokenTableSuffix +
1218
+ '0."guid" AND t' +
1219
+ tokenTableSuffix +
1220
+ i +
1221
+ '."name" = t' +
1222
+ tokenTableSuffix +
1223
+ '0."name" AND t' +
1224
+ tokenTableSuffix +
1225
+ i +
1226
+ '."position" = t' +
1227
+ tokenTableSuffix +
1228
+ '0."position" + ' +
1229
+ i,
1230
+ whereClause: 't' +
1231
+ tokenTableSuffix +
1232
+ i +
1233
+ '."token"=@' +
1234
+ value +
1235
+ (token.nostemmed
1236
+ ? ' AND t' + tokenTableSuffix + i + '."stem"=FALSE'
1237
+ : ''),
1238
+ };
1239
+ });
1240
+ return ('EXISTS (SELECT t' +
1241
+ tokenTableSuffix +
1242
+ '0."guid" ' +
1243
+ tokenParts.map((part) => part.fromClause).join(' ') +
1244
+ ' WHERE t' +
1245
+ tokenTableSuffix +
1246
+ '0."guid"=' +
742
1247
  ieTable +
743
- '."guid" IN (SELECT "guid" FROM ' +
744
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
745
- ' WHERE "name"=@' +
1248
+ '."guid" AND t' +
1249
+ tokenTableSuffix +
1250
+ '0."name"=@' +
746
1251
  name +
747
- ' AND "string"=@' +
748
- stringParam +
749
- '))';
750
- params[stringParam] = stringValue;
751
- }
752
- else {
1252
+ ' AND ' +
1253
+ tokenParts.map((part) => part.whereClause).join(' AND ') +
1254
+ ')');
1255
+ };
1256
+ const queryPartTerm = (term) => {
1257
+ if (term.type === 'series') {
1258
+ return queryPartSeries(term);
1259
+ }
1260
+ else if (term.type === 'not') {
1261
+ return 'NOT ' + queryPartTerm(term.operand);
1262
+ }
1263
+ else if (term.type === 'or') {
1264
+ let queryParts = [];
1265
+ for (let operand of term.operands) {
1266
+ queryParts.push(queryPartTerm(operand));
1267
+ }
1268
+ return '(' + queryParts.join(' OR ') + ')';
1269
+ }
1270
+ return queryPartToken(term);
1271
+ };
1272
+ // Run through the query and add terms.
1273
+ let termStrings = [];
1274
+ for (let term of parsedFTSQuery) {
1275
+ termStrings.push(queryPartTerm(term));
1276
+ }
753
1277
  curQuery +=
754
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
755
- ieTable +
756
- '."guid" IN (SELECT "guid" FROM ' +
757
- PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
758
- ' WHERE "name"=@' +
759
- name +
760
- ' AND position(@' +
761
- value +
762
- ' IN "value")>0)';
1278
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1279
+ '(' +
1280
+ termStrings.join(' AND ') +
1281
+ ')';
1282
+ params[name] = curValue[0];
763
1283
  }
764
- params[name] = curValue[0];
765
- params[value] = svalue;
766
1284
  }
767
1285
  break;
768
1286
  case 'match':
769
1287
  case '!match':
770
- if (curValue[0] === 'cdate') {
771
- if (curQuery) {
772
- curQuery += typeIsOr ? ' OR ' : ' AND ';
773
- }
774
- const cdate = `param${++count.i}`;
775
- curQuery +=
776
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
777
- '(' +
778
- ieTable +
779
- '."cdate" ~ @' +
780
- cdate +
781
- ')';
782
- params[cdate] = curValue[1];
783
- break;
784
- }
785
- else if (curValue[0] === 'mdate') {
1288
+ if (curValue[0] === 'cdate' ||
1289
+ curValue[0] === 'mdate' ||
1290
+ (this.nymph.tilmeld != null &&
1291
+ (curValue[0] === 'user' ||
1292
+ curValue[0] === 'group' ||
1293
+ curValue[0] === 'acUser' ||
1294
+ curValue[0] === 'acGroup' ||
1295
+ curValue[0] === 'acOther' ||
1296
+ curValue[0] === 'acRead' ||
1297
+ curValue[0] === 'acWrite' ||
1298
+ curValue[0] === 'acFull'))) {
786
1299
  if (curQuery) {
787
1300
  curQuery += typeIsOr ? ' OR ' : ' AND ';
788
1301
  }
789
- const mdate = `param${++count.i}`;
1302
+ const value = `param${++count.i}`;
790
1303
  curQuery +=
791
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1304
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
792
1305
  '(' +
793
1306
  ieTable +
794
- '."mdate" ~ @' +
795
- mdate +
1307
+ '.' +
1308
+ PostgreSQLDriver.escape(curValue[0]) +
1309
+ ' ~ @' +
1310
+ value +
796
1311
  ')';
797
- params[mdate] = curValue[1];
798
- break;
1312
+ params[value] = curValue[1];
799
1313
  }
800
1314
  else {
801
1315
  if (curQuery) {
@@ -804,50 +1318,47 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
804
1318
  const name = `param${++count.i}`;
805
1319
  const value = `param${++count.i}`;
806
1320
  curQuery +=
807
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1321
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1322
+ 'EXISTS (SELECT "guid" FROM ' +
1323
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1324
+ ' WHERE "guid"=' +
808
1325
  ieTable +
809
- '."guid" IN (SELECT "guid" FROM ' +
810
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
811
- ' WHERE "name"=@' +
1326
+ '."guid" AND "name"=@' +
812
1327
  name +
813
1328
  ' AND "string" ~ @' +
814
1329
  value +
815
1330
  ')';
816
1331
  params[name] = curValue[0];
817
- params[value] = curValue[1];
1332
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
818
1333
  }
819
1334
  break;
820
1335
  case 'imatch':
821
1336
  case '!imatch':
822
- if (curValue[0] === 'cdate') {
823
- if (curQuery) {
824
- curQuery += typeIsOr ? ' OR ' : ' AND ';
825
- }
826
- const cdate = `param${++count.i}`;
827
- curQuery +=
828
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
829
- '(' +
830
- ieTable +
831
- '."cdate" ~* @' +
832
- cdate +
833
- ')';
834
- params[cdate] = curValue[1];
835
- break;
836
- }
837
- else if (curValue[0] === 'mdate') {
1337
+ if (curValue[0] === 'cdate' ||
1338
+ curValue[0] === 'mdate' ||
1339
+ (this.nymph.tilmeld != null &&
1340
+ (curValue[0] === 'user' ||
1341
+ curValue[0] === 'group' ||
1342
+ curValue[0] === 'acUser' ||
1343
+ curValue[0] === 'acGroup' ||
1344
+ curValue[0] === 'acOther' ||
1345
+ curValue[0] === 'acRead' ||
1346
+ curValue[0] === 'acWrite' ||
1347
+ curValue[0] === 'acFull'))) {
838
1348
  if (curQuery) {
839
1349
  curQuery += typeIsOr ? ' OR ' : ' AND ';
840
1350
  }
841
- const mdate = `param${++count.i}`;
1351
+ const value = `param${++count.i}`;
842
1352
  curQuery +=
843
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1353
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
844
1354
  '(' +
845
1355
  ieTable +
846
- '."mdate" ~* @' +
847
- mdate +
1356
+ '.' +
1357
+ PostgreSQLDriver.escape(curValue[0]) +
1358
+ ' ~* @' +
1359
+ value +
848
1360
  ')';
849
- params[mdate] = curValue[1];
850
- break;
1361
+ params[value] = curValue[1];
851
1362
  }
852
1363
  else {
853
1364
  if (curQuery) {
@@ -856,50 +1367,47 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
856
1367
  const name = `param${++count.i}`;
857
1368
  const value = `param${++count.i}`;
858
1369
  curQuery +=
859
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1370
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1371
+ 'EXISTS (SELECT "guid" FROM ' +
1372
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1373
+ ' WHERE "guid"=' +
860
1374
  ieTable +
861
- '."guid" IN (SELECT "guid" FROM ' +
862
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
863
- ' WHERE "name"=@' +
1375
+ '."guid" AND "name"=@' +
864
1376
  name +
865
1377
  ' AND "string" ~* @' +
866
1378
  value +
867
1379
  ')';
868
1380
  params[name] = curValue[0];
869
- params[value] = curValue[1];
1381
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
870
1382
  }
871
1383
  break;
872
1384
  case 'like':
873
1385
  case '!like':
874
- if (curValue[0] === 'cdate') {
1386
+ if (curValue[0] === 'cdate' ||
1387
+ curValue[0] === 'mdate' ||
1388
+ (this.nymph.tilmeld != null &&
1389
+ (curValue[0] === 'user' ||
1390
+ curValue[0] === 'group' ||
1391
+ curValue[0] === 'acUser' ||
1392
+ curValue[0] === 'acGroup' ||
1393
+ curValue[0] === 'acOther' ||
1394
+ curValue[0] === 'acRead' ||
1395
+ curValue[0] === 'acWrite' ||
1396
+ curValue[0] === 'acFull'))) {
875
1397
  if (curQuery) {
876
1398
  curQuery += typeIsOr ? ' OR ' : ' AND ';
877
1399
  }
878
- const cdate = `param${++count.i}`;
879
- curQuery +=
880
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
881
- '(' +
882
- ieTable +
883
- '."cdate" LIKE @' +
884
- cdate +
885
- ')';
886
- params[cdate] = curValue[1];
887
- break;
888
- }
889
- else if (curValue[0] === 'mdate') {
890
- if (curQuery) {
891
- curQuery += typeIsOr ? ' OR ' : ' AND ';
892
- }
893
- const mdate = `param${++count.i}`;
1400
+ const value = `param${++count.i}`;
894
1401
  curQuery +=
895
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1402
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
896
1403
  '(' +
897
1404
  ieTable +
898
- '."mdate" LIKE @' +
899
- mdate +
1405
+ '.' +
1406
+ PostgreSQLDriver.escape(curValue[0]) +
1407
+ ' LIKE @' +
1408
+ value +
900
1409
  ')';
901
- params[mdate] = curValue[1];
902
- break;
1410
+ params[value] = curValue[1];
903
1411
  }
904
1412
  else {
905
1413
  if (curQuery) {
@@ -908,50 +1416,47 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
908
1416
  const name = `param${++count.i}`;
909
1417
  const value = `param${++count.i}`;
910
1418
  curQuery +=
911
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1419
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1420
+ 'EXISTS (SELECT "guid" FROM ' +
1421
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1422
+ ' WHERE "guid"=' +
912
1423
  ieTable +
913
- '."guid" IN (SELECT "guid" FROM ' +
914
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
915
- ' WHERE "name"=@' +
1424
+ '."guid" AND "name"=@' +
916
1425
  name +
917
1426
  ' AND "string" LIKE @' +
918
1427
  value +
919
1428
  ')';
920
1429
  params[name] = curValue[0];
921
- params[value] = curValue[1];
1430
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
922
1431
  }
923
1432
  break;
924
1433
  case 'ilike':
925
1434
  case '!ilike':
926
- if (curValue[0] === 'cdate') {
1435
+ if (curValue[0] === 'cdate' ||
1436
+ curValue[0] === 'mdate' ||
1437
+ (this.nymph.tilmeld != null &&
1438
+ (curValue[0] === 'user' ||
1439
+ curValue[0] === 'group' ||
1440
+ curValue[0] === 'acUser' ||
1441
+ curValue[0] === 'acGroup' ||
1442
+ curValue[0] === 'acOther' ||
1443
+ curValue[0] === 'acRead' ||
1444
+ curValue[0] === 'acWrite' ||
1445
+ curValue[0] === 'acFull'))) {
927
1446
  if (curQuery) {
928
1447
  curQuery += typeIsOr ? ' OR ' : ' AND ';
929
1448
  }
930
- const cdate = `param${++count.i}`;
1449
+ const value = `param${++count.i}`;
931
1450
  curQuery +=
932
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1451
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
933
1452
  '(' +
934
1453
  ieTable +
935
- '."cdate" ILIKE @' +
936
- cdate +
1454
+ '.' +
1455
+ PostgreSQLDriver.escape(curValue[0]) +
1456
+ ' ILIKE @' +
1457
+ value +
937
1458
  ')';
938
- params[cdate] = curValue[1];
939
- break;
940
- }
941
- else if (curValue[0] === 'mdate') {
942
- if (curQuery) {
943
- curQuery += typeIsOr ? ' OR ' : ' AND ';
944
- }
945
- const mdate = `param${++count.i}`;
946
- curQuery +=
947
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
948
- '(' +
949
- ieTable +
950
- '."mdate" ILIKE @' +
951
- mdate +
952
- ')';
953
- params[mdate] = curValue[1];
954
- break;
1459
+ params[value] = curValue[1];
955
1460
  }
956
1461
  else {
957
1462
  if (curQuery) {
@@ -960,50 +1465,47 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
960
1465
  const name = `param${++count.i}`;
961
1466
  const value = `param${++count.i}`;
962
1467
  curQuery +=
963
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1468
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1469
+ 'EXISTS (SELECT "guid" FROM ' +
1470
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1471
+ ' WHERE "guid"=' +
964
1472
  ieTable +
965
- '."guid" IN (SELECT "guid" FROM ' +
966
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
967
- ' WHERE "name"=@' +
1473
+ '."guid" AND "name"=@' +
968
1474
  name +
969
1475
  ' AND "string" ILIKE @' +
970
1476
  value +
971
1477
  ')';
972
1478
  params[name] = curValue[0];
973
- params[value] = curValue[1];
1479
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
974
1480
  }
975
1481
  break;
976
1482
  case 'gt':
977
1483
  case '!gt':
978
- if (curValue[0] === 'cdate') {
1484
+ if (curValue[0] === 'cdate' ||
1485
+ curValue[0] === 'mdate' ||
1486
+ (this.nymph.tilmeld != null &&
1487
+ (curValue[0] === 'user' ||
1488
+ curValue[0] === 'group' ||
1489
+ curValue[0] === 'acUser' ||
1490
+ curValue[0] === 'acGroup' ||
1491
+ curValue[0] === 'acOther' ||
1492
+ curValue[0] === 'acRead' ||
1493
+ curValue[0] === 'acWrite' ||
1494
+ curValue[0] === 'acFull'))) {
979
1495
  if (curQuery) {
980
1496
  curQuery += typeIsOr ? ' OR ' : ' AND ';
981
1497
  }
982
- const cdate = `param${++count.i}`;
983
- curQuery +=
984
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
985
- ieTable +
986
- '."cdate">@' +
987
- cdate;
988
- params[cdate] = isNaN(Number(curValue[1]))
989
- ? null
990
- : Number(curValue[1]);
991
- break;
992
- }
993
- else if (curValue[0] === 'mdate') {
994
- if (curQuery) {
995
- curQuery += typeIsOr ? ' OR ' : ' AND ';
996
- }
997
- const mdate = `param${++count.i}`;
1498
+ const value = `param${++count.i}`;
998
1499
  curQuery +=
999
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1500
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1000
1501
  ieTable +
1001
- '."mdate">@' +
1002
- mdate;
1003
- params[mdate] = isNaN(Number(curValue[1]))
1502
+ '.' +
1503
+ PostgreSQLDriver.escape(curValue[0]) +
1504
+ '>@' +
1505
+ value;
1506
+ params[value] = isNaN(Number(curValue[1]))
1004
1507
  ? null
1005
1508
  : Number(curValue[1]);
1006
- break;
1007
1509
  }
1008
1510
  else {
1009
1511
  if (curQuery) {
@@ -1012,11 +1514,12 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1012
1514
  const name = `param${++count.i}`;
1013
1515
  const value = `param${++count.i}`;
1014
1516
  curQuery +=
1015
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1517
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1518
+ 'EXISTS (SELECT "guid" FROM ' +
1519
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1520
+ ' WHERE "guid"=' +
1016
1521
  ieTable +
1017
- '."guid" IN (SELECT "guid" FROM ' +
1018
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1019
- ' WHERE "name"=@' +
1522
+ '."guid" AND "name"=@' +
1020
1523
  name +
1021
1524
  ' AND "number">@' +
1022
1525
  value +
@@ -1029,35 +1532,31 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1029
1532
  break;
1030
1533
  case 'gte':
1031
1534
  case '!gte':
1032
- if (curValue[0] === 'cdate') {
1033
- if (curQuery) {
1034
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1035
- }
1036
- const cdate = `param${++count.i}`;
1037
- curQuery +=
1038
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1039
- ieTable +
1040
- '."cdate">=@' +
1041
- cdate;
1042
- params[cdate] = isNaN(Number(curValue[1]))
1043
- ? null
1044
- : Number(curValue[1]);
1045
- break;
1046
- }
1047
- else if (curValue[0] === 'mdate') {
1535
+ if (curValue[0] === 'cdate' ||
1536
+ curValue[0] === 'mdate' ||
1537
+ (this.nymph.tilmeld != null &&
1538
+ (curValue[0] === 'user' ||
1539
+ curValue[0] === 'group' ||
1540
+ curValue[0] === 'acUser' ||
1541
+ curValue[0] === 'acGroup' ||
1542
+ curValue[0] === 'acOther' ||
1543
+ curValue[0] === 'acRead' ||
1544
+ curValue[0] === 'acWrite' ||
1545
+ curValue[0] === 'acFull'))) {
1048
1546
  if (curQuery) {
1049
1547
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1050
1548
  }
1051
- const mdate = `param${++count.i}`;
1549
+ const value = `param${++count.i}`;
1052
1550
  curQuery +=
1053
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1551
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1054
1552
  ieTable +
1055
- '."mdate">=@' +
1056
- mdate;
1057
- params[mdate] = isNaN(Number(curValue[1]))
1553
+ '.' +
1554
+ PostgreSQLDriver.escape(curValue[0]) +
1555
+ '>=@' +
1556
+ value;
1557
+ params[value] = isNaN(Number(curValue[1]))
1058
1558
  ? null
1059
1559
  : Number(curValue[1]);
1060
- break;
1061
1560
  }
1062
1561
  else {
1063
1562
  if (curQuery) {
@@ -1066,11 +1565,12 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1066
1565
  const name = `param${++count.i}`;
1067
1566
  const value = `param${++count.i}`;
1068
1567
  curQuery +=
1069
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1568
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1569
+ 'EXISTS (SELECT "guid" FROM ' +
1570
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1571
+ ' WHERE "guid"=' +
1070
1572
  ieTable +
1071
- '."guid" IN (SELECT "guid" FROM ' +
1072
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1073
- ' WHERE "name"=@' +
1573
+ '."guid" AND "name"=@' +
1074
1574
  name +
1075
1575
  ' AND "number">=@' +
1076
1576
  value +
@@ -1083,35 +1583,31 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1083
1583
  break;
1084
1584
  case 'lt':
1085
1585
  case '!lt':
1086
- if (curValue[0] === 'cdate') {
1586
+ if (curValue[0] === 'cdate' ||
1587
+ curValue[0] === 'mdate' ||
1588
+ (this.nymph.tilmeld != null &&
1589
+ (curValue[0] === 'user' ||
1590
+ curValue[0] === 'group' ||
1591
+ curValue[0] === 'acUser' ||
1592
+ curValue[0] === 'acGroup' ||
1593
+ curValue[0] === 'acOther' ||
1594
+ curValue[0] === 'acRead' ||
1595
+ curValue[0] === 'acWrite' ||
1596
+ curValue[0] === 'acFull'))) {
1087
1597
  if (curQuery) {
1088
1598
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1089
1599
  }
1090
- const cdate = `param${++count.i}`;
1091
- curQuery +=
1092
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1093
- ieTable +
1094
- '."cdate"<@' +
1095
- cdate;
1096
- params[cdate] = isNaN(Number(curValue[1]))
1097
- ? null
1098
- : Number(curValue[1]);
1099
- break;
1100
- }
1101
- else if (curValue[0] === 'mdate') {
1102
- if (curQuery) {
1103
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1104
- }
1105
- const mdate = `param${++count.i}`;
1600
+ const value = `param${++count.i}`;
1106
1601
  curQuery +=
1107
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1602
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1108
1603
  ieTable +
1109
- '."mdate"<@' +
1110
- mdate;
1111
- params[mdate] = isNaN(Number(curValue[1]))
1604
+ '.' +
1605
+ PostgreSQLDriver.escape(curValue[0]) +
1606
+ '<@' +
1607
+ value;
1608
+ params[value] = isNaN(Number(curValue[1]))
1112
1609
  ? null
1113
1610
  : Number(curValue[1]);
1114
- break;
1115
1611
  }
1116
1612
  else {
1117
1613
  if (curQuery) {
@@ -1120,11 +1616,12 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1120
1616
  const name = `param${++count.i}`;
1121
1617
  const value = `param${++count.i}`;
1122
1618
  curQuery +=
1123
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1619
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1620
+ 'EXISTS (SELECT "guid" FROM ' +
1621
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1622
+ ' WHERE "guid"=' +
1124
1623
  ieTable +
1125
- '."guid" IN (SELECT "guid" FROM ' +
1126
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1127
- ' WHERE "name"=@' +
1624
+ '."guid" AND "name"=@' +
1128
1625
  name +
1129
1626
  ' AND "number"<@' +
1130
1627
  value +
@@ -1137,35 +1634,31 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1137
1634
  break;
1138
1635
  case 'lte':
1139
1636
  case '!lte':
1140
- if (curValue[0] === 'cdate') {
1637
+ if (curValue[0] === 'cdate' ||
1638
+ curValue[0] === 'mdate' ||
1639
+ (this.nymph.tilmeld != null &&
1640
+ (curValue[0] === 'user' ||
1641
+ curValue[0] === 'group' ||
1642
+ curValue[0] === 'acUser' ||
1643
+ curValue[0] === 'acGroup' ||
1644
+ curValue[0] === 'acOther' ||
1645
+ curValue[0] === 'acRead' ||
1646
+ curValue[0] === 'acWrite' ||
1647
+ curValue[0] === 'acFull'))) {
1141
1648
  if (curQuery) {
1142
1649
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1143
1650
  }
1144
- const cdate = `param${++count.i}`;
1145
- curQuery +=
1146
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1147
- ieTable +
1148
- '."cdate"<=@' +
1149
- cdate;
1150
- params[cdate] = isNaN(Number(curValue[1]))
1151
- ? null
1152
- : Number(curValue[1]);
1153
- break;
1154
- }
1155
- else if (curValue[0] === 'mdate') {
1156
- if (curQuery) {
1157
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1158
- }
1159
- const mdate = `param${++count.i}`;
1651
+ const value = `param${++count.i}`;
1160
1652
  curQuery +=
1161
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1653
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1162
1654
  ieTable +
1163
- '."mdate"<=@' +
1164
- mdate;
1165
- params[mdate] = isNaN(Number(curValue[1]))
1655
+ '.' +
1656
+ PostgreSQLDriver.escape(curValue[0]) +
1657
+ '<=@' +
1658
+ value;
1659
+ params[value] = isNaN(Number(curValue[1]))
1166
1660
  ? null
1167
1661
  : Number(curValue[1]);
1168
- break;
1169
1662
  }
1170
1663
  else {
1171
1664
  if (curQuery) {
@@ -1174,11 +1667,12 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1174
1667
  const name = `param${++count.i}`;
1175
1668
  const value = `param${++count.i}`;
1176
1669
  curQuery +=
1177
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1670
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1671
+ 'EXISTS (SELECT "guid" FROM ' +
1672
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1673
+ ' WHERE "guid"=' +
1178
1674
  ieTable +
1179
- '."guid" IN (SELECT "guid" FROM ' +
1180
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1181
- ' WHERE "name"=@' +
1675
+ '."guid" AND "name"=@' +
1182
1676
  name +
1183
1677
  ' AND "number"<=@' +
1184
1678
  value +
@@ -1204,71 +1698,190 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1204
1698
  if (curQuery) {
1205
1699
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1206
1700
  }
1207
- const name = `param${++count.i}`;
1208
- const guid = `param${++count.i}`;
1209
- curQuery +=
1210
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1211
- ieTable +
1212
- '."guid" IN (SELECT "guid" FROM ' +
1213
- PostgreSQLDriver.escape(this.prefix + 'references_' + etype) +
1214
- ' WHERE "name"=@' +
1215
- name +
1216
- ' AND "reference"=decode(@' +
1217
- guid +
1218
- ", 'hex'))";
1219
- params[name] = curValue[0];
1220
- params[guid] = curQguid;
1701
+ if (this.nymph.tilmeld != null &&
1702
+ (curValue[0] === 'user' || curValue[0] === 'group')) {
1703
+ const guid = `param${++count.i}`;
1704
+ curQuery +=
1705
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1706
+ ieTable +
1707
+ '.' +
1708
+ PostgreSQLDriver.escape(curValue[0]) +
1709
+ '=decode(@' +
1710
+ guid +
1711
+ ", 'hex')";
1712
+ params[guid] = curQguid;
1713
+ }
1714
+ else if (this.nymph.tilmeld != null &&
1715
+ (curValue[0] === 'acRead' ||
1716
+ curValue[0] === 'acWrite' ||
1717
+ curValue[0] === 'acFull')) {
1718
+ const guid = `param${++count.i}`;
1719
+ curQuery +=
1720
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1721
+ 'decode(@' +
1722
+ guid +
1723
+ ", 'hex')=ANY(" +
1724
+ ieTable +
1725
+ '.' +
1726
+ PostgreSQLDriver.escape(curValue[0]) +
1727
+ ')';
1728
+ params[guid] = curQguid;
1729
+ }
1730
+ else {
1731
+ const name = `param${++count.i}`;
1732
+ const guid = `param${++count.i}`;
1733
+ curQuery +=
1734
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1735
+ 'EXISTS (SELECT "guid" FROM ' +
1736
+ PostgreSQLDriver.escape(this.prefix + 'references_' + etype) +
1737
+ ' WHERE "guid"=' +
1738
+ ieTable +
1739
+ '."guid" AND "name"=@' +
1740
+ name +
1741
+ ' AND "reference"=decode(@' +
1742
+ guid +
1743
+ ", 'hex'))";
1744
+ params[name] = curValue[0];
1745
+ params[guid] = curQguid;
1746
+ }
1221
1747
  break;
1222
1748
  case 'selector':
1223
1749
  case '!selector':
1224
- const subquery = this.makeEntityQuery(options, [curValue], etype, count, params, true, tableSuffix, etypes);
1750
+ const innerquery = this.makeEntityQuery({ ...options, sort: null, limit: undefined }, [curValue], etype, count, params, true, tableSuffix, etypes);
1225
1751
  if (curQuery) {
1226
1752
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1227
1753
  }
1228
1754
  curQuery +=
1229
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1755
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1230
1756
  '(' +
1231
- subquery.query +
1757
+ innerquery.query +
1232
1758
  ')';
1233
1759
  break;
1234
1760
  case 'qref':
1235
1761
  case '!qref':
1762
+ const referenceTableSuffix = makeTableSuffix();
1236
1763
  const [qrefOptions, ...qrefSelectors] = curValue[1];
1237
1764
  const QrefEntityClass = qrefOptions.class;
1238
1765
  etypes.push(QrefEntityClass.ETYPE);
1239
- const qrefQuery = this.makeEntityQuery({ ...qrefOptions, return: 'guid', class: QrefEntityClass }, qrefSelectors, QrefEntityClass.ETYPE, count, params, false, (0, guid_1.makeTableSuffix)(), etypes);
1240
1766
  if (curQuery) {
1241
1767
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1242
1768
  }
1243
- const qrefName = `param${++count.i}`;
1244
- curQuery +=
1245
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1769
+ if (this.nymph.tilmeld != null &&
1770
+ (curValue[0] === 'user' || curValue[0] === 'group')) {
1771
+ const qrefQuery = this.makeEntityQuery({
1772
+ ...qrefOptions,
1773
+ sort: qrefOptions.sort ?? null,
1774
+ return: 'guid',
1775
+ class: QrefEntityClass,
1776
+ }, qrefSelectors, QrefEntityClass.ETYPE, count, params, false, makeTableSuffix(), etypes, 'r' +
1777
+ referenceTableSuffix +
1778
+ '.' +
1779
+ PostgreSQLDriver.escape(curValue[0]));
1780
+ curQuery +=
1781
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1782
+ 'EXISTS (SELECT "guid" FROM ' +
1783
+ PostgreSQLDriver.escape(this.prefix + 'entities_' + etype) +
1784
+ ' r' +
1785
+ referenceTableSuffix +
1786
+ ' WHERE r' +
1787
+ referenceTableSuffix +
1788
+ '."guid"=' +
1789
+ ieTable +
1790
+ '."guid" AND EXISTS (' +
1791
+ qrefQuery.query +
1792
+ '))';
1793
+ }
1794
+ else if (this.nymph.tilmeld != null &&
1795
+ (curValue[0] === 'acRead' ||
1796
+ curValue[0] === 'acWrite' ||
1797
+ curValue[0] === 'acFull')) {
1798
+ const qrefQuery = this.makeEntityQuery({
1799
+ ...qrefOptions,
1800
+ sort: qrefOptions.sort ?? null,
1801
+ return: 'guid',
1802
+ class: QrefEntityClass,
1803
+ }, qrefSelectors, QrefEntityClass.ETYPE, count, params, false, makeTableSuffix(), etypes, undefined, (guid) => guid +
1804
+ '=ANY(' +
1246
1805
  ieTable +
1247
- '."guid" IN (SELECT "guid" FROM ' +
1248
- PostgreSQLDriver.escape(this.prefix + 'references_' + etype) +
1249
- ' WHERE "name"=@' +
1250
- qrefName +
1251
- ' AND "reference" IN (' +
1252
- qrefQuery.query +
1253
- '))';
1254
- params[qrefName] = curValue[0];
1806
+ '.' +
1807
+ PostgreSQLDriver.escape(curValue[0]) +
1808
+ ')');
1809
+ curQuery +=
1810
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1811
+ 'EXISTS (' +
1812
+ qrefQuery.query +
1813
+ ')';
1814
+ }
1815
+ else {
1816
+ const qrefQuery = this.makeEntityQuery({
1817
+ ...qrefOptions,
1818
+ sort: qrefOptions.sort ?? null,
1819
+ return: 'guid',
1820
+ class: QrefEntityClass,
1821
+ }, qrefSelectors, QrefEntityClass.ETYPE, count, params, false, makeTableSuffix(), etypes, 'r' + referenceTableSuffix + '."reference"');
1822
+ const qrefName = `param${++count.i}`;
1823
+ curQuery +=
1824
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1825
+ 'EXISTS (SELECT "guid" FROM ' +
1826
+ PostgreSQLDriver.escape(this.prefix + 'references_' + etype) +
1827
+ ' r' +
1828
+ referenceTableSuffix +
1829
+ ' WHERE r' +
1830
+ referenceTableSuffix +
1831
+ '."guid"=' +
1832
+ ieTable +
1833
+ '."guid" AND r' +
1834
+ referenceTableSuffix +
1835
+ '."name"=@' +
1836
+ qrefName +
1837
+ ' AND EXISTS (' +
1838
+ qrefQuery.query +
1839
+ '))';
1840
+ params[qrefName] = curValue[0];
1841
+ }
1255
1842
  break;
1256
1843
  }
1257
1844
  }
1258
1845
  return curQuery;
1259
1846
  });
1260
1847
  let sortBy;
1261
- switch (sort) {
1262
- case 'mdate':
1263
- sortBy = '"mdate"';
1264
- break;
1265
- case 'cdate':
1266
- default:
1267
- sortBy = '"cdate"';
1268
- break;
1269
- }
1270
- if (options.reverse) {
1271
- sortBy += ' DESC';
1848
+ let sortByInner;
1849
+ let sortJoin = '';
1850
+ let sortJoinInner = '';
1851
+ const order = options.reverse ? ' DESC' : '';
1852
+ if (sort == null) {
1853
+ sortBy = '';
1854
+ sortByInner = '';
1855
+ }
1856
+ else {
1857
+ switch (sort) {
1858
+ case 'mdate':
1859
+ sortBy = `ORDER BY ${eTable}."mdate"${order}`;
1860
+ sortByInner = `ORDER BY ${ieTable}."mdate"${order}`;
1861
+ break;
1862
+ case 'cdate':
1863
+ sortBy = `ORDER BY ${eTable}."cdate"${order}`;
1864
+ sortByInner = `ORDER BY ${ieTable}."cdate"${order}`;
1865
+ break;
1866
+ default:
1867
+ const name = `param${++count.i}`;
1868
+ sortJoin = `LEFT JOIN (
1869
+ SELECT "guid", "string", "number"
1870
+ FROM ${PostgreSQLDriver.escape(this.prefix + 'data_' + etype)}
1871
+ WHERE "name"=@${name}
1872
+ ORDER BY "number"${order}, "string"${order}
1873
+ ) ${sTable} ON ${eTable}."guid"=${sTable}."guid"`;
1874
+ sortJoinInner = `LEFT JOIN (
1875
+ SELECT "guid", "string", "number"
1876
+ FROM ${PostgreSQLDriver.escape(this.prefix + 'data_' + etype)}
1877
+ WHERE "name"=@${name}
1878
+ ORDER BY "number"${order}, "string"${order}
1879
+ ) ${sTable} ON ${ieTable}."guid"=${sTable}."guid"`;
1880
+ sortBy = `ORDER BY ${sTable}."number"${order}, ${sTable}."string"${order}`;
1881
+ sortByInner = sortBy;
1882
+ params[name] = sort;
1883
+ break;
1884
+ }
1272
1885
  }
1273
1886
  let query;
1274
1887
  if (queryParts.length) {
@@ -1285,18 +1898,23 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1285
1898
  offset = ` OFFSET ${Math.floor(isNaN(Number(options.offset)) ? 0 : Number(options.offset))}`;
1286
1899
  }
1287
1900
  const whereClause = queryParts.join(') AND (');
1901
+ const guidClause = guidExplicitSelector
1902
+ ? `${guidExplicitSelector(`${ieTable}."guid"`)} AND `
1903
+ : guidSelector
1904
+ ? `${ieTable}."guid"=${guidSelector} AND `
1905
+ : '';
1288
1906
  if (options.return === 'count') {
1289
1907
  if (limit || offset) {
1290
1908
  query = `SELECT COUNT(${countTable}."guid") AS "count" FROM (
1291
- SELECT COUNT(${ieTable}."guid") AS "guid"
1909
+ SELECT ${ieTable}."guid" AS "guid"
1292
1910
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}
1293
- WHERE (${whereClause})${limit}${offset}
1911
+ WHERE ${guidClause}(${whereClause})${limit}${offset}
1294
1912
  ) ${countTable}`;
1295
1913
  }
1296
1914
  else {
1297
1915
  query = `SELECT COUNT(${ieTable}."guid") AS "count"
1298
1916
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}
1299
- WHERE (${whereClause})`;
1917
+ WHERE ${guidClause}(${whereClause})`;
1300
1918
  }
1301
1919
  }
1302
1920
  else if (options.return === 'guid') {
@@ -1305,8 +1923,9 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1305
1923
  : `${ieTable}."guid"`;
1306
1924
  query = `SELECT ${guidColumn} AS "guid"
1307
1925
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}
1308
- WHERE (${whereClause})
1309
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}`;
1926
+ ${sortJoinInner}
1927
+ WHERE ${guidClause}(${whereClause})
1928
+ ${sortByInner ? sortByInner + ', ' : 'ORDER BY '}${ieTable}."guid"${limit}${offset}`;
1310
1929
  }
1311
1930
  else {
1312
1931
  query = `SELECT
@@ -1314,20 +1933,30 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1314
1933
  ${eTable}."tags",
1315
1934
  ${eTable}."cdate",
1316
1935
  ${eTable}."mdate",
1936
+ encode(${eTable}."user", 'hex') AS "user",
1937
+ encode(${eTable}."group", 'hex') AS "group",
1938
+ ${eTable}."acUser",
1939
+ ${eTable}."acGroup",
1940
+ ${eTable}."acOther",
1941
+ array(SELECT encode(n, 'hex') FROM unnest(${eTable}."acRead") AS n) as "acRead",
1942
+ array(SELECT encode(n, 'hex') FROM unnest(${eTable}."acWrite") AS n) as "acWrite",
1943
+ array(SELECT encode(n, 'hex') FROM unnest(${eTable}."acFull") AS n) as "acFull",
1317
1944
  ${dTable}."name",
1318
1945
  ${dTable}."value",
1319
- ${cTable}."string",
1320
- ${cTable}."number"
1946
+ ${dTable}."json",
1947
+ ${dTable}."string",
1948
+ ${dTable}."number"
1321
1949
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${eTable}
1322
1950
  LEFT JOIN ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} ${dTable} ON ${eTable}."guid"=${dTable}."guid"
1323
- INNER JOIN ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} ${cTable} ON ${dTable}."guid"=${cTable}."guid" AND ${dTable}."name"=${cTable}."name"
1951
+ ${sortJoin}
1324
1952
  INNER JOIN (
1325
1953
  SELECT ${ieTable}."guid"
1326
1954
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}
1327
- WHERE (${whereClause})
1328
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}
1955
+ ${sortJoinInner}
1956
+ WHERE ${guidClause}(${whereClause})
1957
+ ${sortByInner}${limit}${offset}
1329
1958
  ) ${fTable} ON ${eTable}."guid"=${fTable}."guid"
1330
- ORDER BY ${eTable}.${sortBy}`;
1959
+ ${sortBy ? sortBy + ', ' : 'ORDER BY '}${eTable}."guid"`;
1331
1960
  }
1332
1961
  }
1333
1962
  }
@@ -1344,16 +1973,21 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1344
1973
  if ('offset' in options) {
1345
1974
  offset = ` OFFSET ${Math.floor(isNaN(Number(options.offset)) ? 0 : Number(options.offset))}`;
1346
1975
  }
1976
+ const guidClause = guidExplicitSelector
1977
+ ? ` WHERE ${guidExplicitSelector(`${ieTable}."guid"`)}`
1978
+ : guidSelector
1979
+ ? ` WHERE ${ieTable}."guid"=${guidSelector}`
1980
+ : '';
1347
1981
  if (options.return === 'count') {
1348
1982
  if (limit || offset) {
1349
1983
  query = `SELECT COUNT(${countTable}."guid") AS "count" FROM (
1350
- SELECT COUNT(${ieTable}."guid") AS "guid"
1351
- FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}${limit}${offset}
1984
+ SELECT ${ieTable}."guid" AS "guid"
1985
+ FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}${guidClause}${limit}${offset}
1352
1986
  ) ${countTable}`;
1353
1987
  }
1354
1988
  else {
1355
1989
  query = `SELECT COUNT(${ieTable}."guid") AS "count"
1356
- FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}`;
1990
+ FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}${guidClause}`;
1357
1991
  }
1358
1992
  }
1359
1993
  else if (options.return === 'guid') {
@@ -1362,7 +1996,9 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1362
1996
  : `${ieTable}."guid"`;
1363
1997
  query = `SELECT ${guidColumn} AS "guid"
1364
1998
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}
1365
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}`;
1999
+ ${sortJoinInner}
2000
+ ${guidClause}
2001
+ ${sortByInner ? sortByInner + ', ' : 'ORDER BY '}${ieTable}."guid"${limit}${offset}`;
1366
2002
  }
1367
2003
  else {
1368
2004
  if (limit || offset) {
@@ -1371,19 +2007,30 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1371
2007
  ${eTable}."tags",
1372
2008
  ${eTable}."cdate",
1373
2009
  ${eTable}."mdate",
2010
+ encode(${eTable}."user", 'hex') AS "user",
2011
+ encode(${eTable}."group", 'hex') AS "group",
2012
+ ${eTable}."acUser",
2013
+ ${eTable}."acGroup",
2014
+ ${eTable}."acOther",
2015
+ array(SELECT encode(n, 'hex') FROM unnest(${eTable}."acRead") AS n) as "acRead",
2016
+ array(SELECT encode(n, 'hex') FROM unnest(${eTable}."acWrite") AS n) as "acWrite",
2017
+ array(SELECT encode(n, 'hex') FROM unnest(${eTable}."acFull") AS n) as "acFull",
1374
2018
  ${dTable}."name",
1375
2019
  ${dTable}."value",
1376
- ${cTable}."string",
1377
- ${cTable}."number"
2020
+ ${dTable}."json",
2021
+ ${dTable}."string",
2022
+ ${dTable}."number"
1378
2023
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${eTable}
1379
2024
  LEFT JOIN ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} ${dTable} ON ${eTable}."guid"=${dTable}."guid"
1380
- INNER JOIN ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} ${cTable} ON ${dTable}."guid"=${cTable}."guid" AND ${dTable}."name"=${cTable}."name"
2025
+ ${sortJoin}
1381
2026
  INNER JOIN (
1382
2027
  SELECT ${ieTable}."guid"
1383
2028
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}
1384
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}
2029
+ ${sortJoinInner}
2030
+ ${guidClause}
2031
+ ${sortByInner}${limit}${offset}
1385
2032
  ) ${fTable} ON ${eTable}."guid"=${fTable}."guid"
1386
- ORDER BY ${eTable}.${sortBy}`;
2033
+ ${sortBy ? sortBy + ', ' : 'ORDER BY '}${eTable}."guid"`;
1387
2034
  }
1388
2035
  else {
1389
2036
  query = `SELECT
@@ -1391,14 +2038,24 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1391
2038
  ${eTable}."tags",
1392
2039
  ${eTable}."cdate",
1393
2040
  ${eTable}."mdate",
2041
+ encode(${eTable}."user", 'hex') AS "user",
2042
+ encode(${eTable}."group", 'hex') AS "group",
2043
+ ${eTable}."acUser",
2044
+ ${eTable}."acGroup",
2045
+ ${eTable}."acOther",
2046
+ array(SELECT encode(n, 'hex') FROM unnest(${eTable}."acRead") AS n) as "acRead",
2047
+ array(SELECT encode(n, 'hex') FROM unnest(${eTable}."acWrite") AS n) as "acWrite",
2048
+ array(SELECT encode(n, 'hex') FROM unnest(${eTable}."acFull") AS n) as "acFull",
1394
2049
  ${dTable}."name",
1395
2050
  ${dTable}."value",
1396
- ${cTable}."string",
1397
- ${cTable}."number"
2051
+ ${dTable}."json",
2052
+ ${dTable}."string",
2053
+ ${dTable}."number"
1398
2054
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${eTable}
1399
2055
  LEFT JOIN ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} ${dTable} ON ${eTable}."guid"=${dTable}."guid"
1400
- INNER JOIN ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} ${cTable} ON ${dTable}."guid"=${cTable}."guid" AND ${dTable}."name"=${cTable}."name"
1401
- ORDER BY ${eTable}.${sortBy}`;
2056
+ ${sortJoin}
2057
+ ${guidExplicitSelector ? `WHERE ${guidExplicitSelector(`${eTable}."guid"`)}` : guidSelector ? `WHERE ${eTable}."guid"=${guidSelector}` : ''}
2058
+ ${sortBy ? sortBy + ', ' : 'ORDER BY '}${eTable}."guid"`;
1402
2059
  }
1403
2060
  }
1404
2061
  }
@@ -1414,33 +2071,38 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1414
2071
  }
1415
2072
  performQuery(options, formattedSelectors, etype) {
1416
2073
  const { query, params, etypes } = this.makeEntityQuery(options, formattedSelectors, etype);
1417
- const result = this.queryIter(query, { etypes, params }).then((val) => val[Symbol.iterator]());
1418
- return {
1419
- result,
1420
- };
1421
- }
1422
- performQuerySync(options, formattedSelectors, etype) {
1423
- const { query, params, etypes } = this.makeEntityQuery(options, formattedSelectors, etype);
1424
- const result = (this.queryIterSync(query, { etypes, params }) || [])[Symbol.iterator]();
2074
+ const result = this.queryArray(query, { etypes, params }).then((val) => val[Symbol.iterator]());
1425
2075
  return {
1426
2076
  result,
1427
2077
  };
1428
2078
  }
1429
2079
  async getEntities(options = {}, ...selectors) {
1430
- const { result: resultPromise, process } = this.getEntitesRowLike(options, selectors, (options, formattedSelectors, etype) => this.performQuery(options, formattedSelectors, etype), () => {
2080
+ const { result: resultPromise, process } = this.getEntitiesRowLike(
2081
+ // @ts-ignore: options is correct here.
2082
+ options, selectors, ({ options, selectors, etype }) => this.performQuery(options, selectors, etype), () => {
1431
2083
  const next = result.next();
1432
2084
  return next.done ? null : next.value;
1433
2085
  }, () => undefined, (row) => Number(row.count), (row) => row.guid, (row) => ({
1434
- tags: row.tags,
1435
- cdate: isNaN(Number(row.cdate)) ? null : Number(row.cdate),
1436
- mdate: isNaN(Number(row.mdate)) ? null : Number(row.mdate),
2086
+ tags: row.tags.filter((tag) => tag),
2087
+ cdate: isNaN(Number(row.cdate)) ? Date.now() : Number(row.cdate),
2088
+ mdate: isNaN(Number(row.mdate)) ? Date.now() : Number(row.mdate),
2089
+ user: row.user,
2090
+ group: row.group,
2091
+ acUser: row.acUser,
2092
+ acGroup: row.acGroup,
2093
+ acOther: row.acOther,
2094
+ acRead: row.acRead?.filter((guid) => guid) ?? [],
2095
+ acWrite: row.acWrite?.filter((guid) => guid) ?? [],
2096
+ acFull: row.acFull?.filter((guid) => guid) ?? [],
1437
2097
  }), (row) => ({
1438
2098
  name: row.name,
1439
2099
  svalue: row.value === 'N'
1440
2100
  ? JSON.stringify(Number(row.number))
1441
2101
  : row.value === 'S'
1442
- ? JSON.stringify(row.string)
1443
- : row.value,
2102
+ ? JSON.stringify(PostgreSQLDriver.unescapeNulls(row.string))
2103
+ : row.value === 'J'
2104
+ ? PostgreSQLDriver.unescapeNullSequences(JSON.stringify(row.json))
2105
+ : row.value,
1444
2106
  }));
1445
2107
  const result = await resultPromise;
1446
2108
  const value = process();
@@ -1449,31 +2111,9 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1449
2111
  }
1450
2112
  return value;
1451
2113
  }
1452
- getEntitiesSync(options = {}, ...selectors) {
1453
- const { result, process } = this.getEntitesRowLike(options, selectors, (options, formattedSelectors, etype) => this.performQuerySync(options, formattedSelectors, etype), () => {
1454
- const next = result.next();
1455
- return next.done ? null : next.value;
1456
- }, () => undefined, (row) => Number(row.count), (row) => row.guid, (row) => ({
1457
- tags: row.tags,
1458
- cdate: isNaN(Number(row.cdate)) ? null : Number(row.cdate),
1459
- mdate: isNaN(Number(row.mdate)) ? null : Number(row.mdate),
1460
- }), (row) => ({
1461
- name: row.name,
1462
- svalue: row.value === 'N'
1463
- ? JSON.stringify(Number(row.number))
1464
- : row.value === 'S'
1465
- ? JSON.stringify(row.string)
1466
- : row.value,
1467
- }));
1468
- const value = process();
1469
- if (value instanceof Error) {
1470
- throw value;
1471
- }
1472
- return value;
1473
- }
1474
2114
  async getUID(name) {
1475
2115
  if (name == null) {
1476
- throw new nymph_1.InvalidParametersError('Name not given for UID.');
2116
+ throw new InvalidParametersError('Name not given for UID.');
1477
2117
  }
1478
2118
  const result = await this.queryGet(`SELECT "cur_uid" FROM ${PostgreSQLDriver.escape(`${this.prefix}uids`)} WHERE "name"=@name;`, {
1479
2119
  params: {
@@ -1482,50 +2122,75 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1482
2122
  });
1483
2123
  return result?.cur_uid == null ? null : Number(result.cur_uid);
1484
2124
  }
1485
- async import(filename) {
2125
+ async importEntity(entity) {
2126
+ return await this.importEntityInternal(entity);
2127
+ }
2128
+ async importEntityTokens(entity) {
2129
+ return await this.importEntityInternal(entity, { only: 'tokens' });
2130
+ }
2131
+ async importEntityTilmeldAC(entity) {
2132
+ return await this.importEntityInternal(entity, { only: 'tilmeldAC' });
2133
+ }
2134
+ async importEntityInternal({ guid, cdate, mdate, tags, sdata, etype, }, { only = undefined } = {}) {
1486
2135
  try {
1487
- const result = await this.importFromFile(filename, async (guid, tags, sdata, etype) => {
1488
- await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
2136
+ let promises = [];
2137
+ if (only == null) {
2138
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1489
2139
  etypes: [etype],
1490
2140
  params: {
1491
2141
  guid,
1492
2142
  },
1493
- });
1494
- await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid", "tags", "cdate", "mdate") VALUES (decode(@guid, 'hex'), @tags, @cdate, @mdate);`, {
2143
+ }));
2144
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1495
2145
  etypes: [etype],
1496
2146
  params: {
1497
2147
  guid,
1498
- tags,
1499
- cdate: isNaN(Number(JSON.parse(sdata.cdate)))
1500
- ? null
1501
- : Number(JSON.parse(sdata.cdate)),
1502
- mdate: isNaN(Number(JSON.parse(sdata.mdate)))
1503
- ? null
1504
- : Number(JSON.parse(sdata.mdate)),
1505
2148
  },
1506
- });
1507
- const promises = [];
1508
- promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
2149
+ }));
2150
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1509
2151
  etypes: [etype],
1510
2152
  params: {
1511
2153
  guid,
1512
2154
  },
1513
2155
  }));
1514
- promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
2156
+ }
2157
+ if (only == null || only === 'tokens') {
2158
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1515
2159
  etypes: [etype],
1516
2160
  params: {
1517
2161
  guid,
1518
2162
  },
1519
2163
  }));
1520
- promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
2164
+ }
2165
+ if (only == null) {
2166
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1521
2167
  etypes: [etype],
1522
2168
  params: {
1523
2169
  guid,
1524
2170
  },
1525
2171
  }));
1526
- await Promise.all(promises);
1527
- delete sdata.cdate;
1528
- delete sdata.mdate;
2172
+ }
2173
+ await Promise.all(promises);
2174
+ promises = [];
2175
+ if (only == null) {
2176
+ let { user, group, acUser, acGroup, acOther, acRead, acWrite, acFull } = this.removeAndReturnACValues(etype, {}, sdata);
2177
+ await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid", "tags", "cdate", "mdate", "user", "group", "acUser", "acGroup", "acOther", "acRead", "acWrite", "acFull") VALUES (decode(@guid, 'hex'), @tags, @cdate, @mdate, ${user == null ? '@user' : "decode(@user, 'hex')"}, ${group == null ? '@group' : "decode(@group, 'hex')"}, @acUser, @acGroup, @acOther, ${!acRead?.length ? '@acRead' : "array(SELECT decode(n, 'hex') FROM unnest(@acRead::text[]) AS n)"}, ${!acWrite?.length ? '@acWrite' : "array(SELECT decode(n, 'hex') FROM unnest(@acWrite::text[]) AS n)"}, ${!acFull?.length ? '@acFull' : "array(SELECT decode(n, 'hex') FROM unnest(@acFull::text[]) AS n)"});`, {
2178
+ etypes: [etype],
2179
+ params: {
2180
+ guid,
2181
+ tags,
2182
+ cdate: isNaN(cdate) ? null : cdate,
2183
+ mdate: isNaN(mdate) ? null : mdate,
2184
+ user,
2185
+ group,
2186
+ acUser,
2187
+ acGroup,
2188
+ acOther,
2189
+ acRead,
2190
+ acWrite,
2191
+ acFull,
2192
+ },
2193
+ });
1529
2194
  for (const name in sdata) {
1530
2195
  const value = sdata[name];
1531
2196
  const uvalue = JSON.parse(value);
@@ -1536,24 +2201,22 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1536
2201
  ? 'N'
1537
2202
  : typeof uvalue === 'string'
1538
2203
  ? 'S'
1539
- : value;
1540
- const promises = [];
1541
- promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} ("guid", "name", "value") VALUES (decode(@guid, 'hex'), @name, @storageValue);`, {
2204
+ : 'J';
2205
+ const jsonValue = storageValue === 'J'
2206
+ ? PostgreSQLDriver.escapeNullSequences(value)
2207
+ : null;
2208
+ promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} ("guid", "name", "value", "json", "string", "number", "truthy") VALUES (decode(@guid, 'hex'), @name, @storageValue, @jsonValue, @string, @number, @truthy);`, {
1542
2209
  etypes: [etype],
1543
2210
  params: {
1544
2211
  guid,
1545
2212
  name,
1546
2213
  storageValue,
1547
- },
1548
- }));
1549
- promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} ("guid", "name", "truthy", "string", "number") VALUES (decode(@guid, 'hex'), @name, @truthy, @string, @number);`, {
1550
- etypes: [etype],
1551
- params: {
1552
- guid,
1553
- name,
1554
- truthy: !!uvalue,
1555
- string: `${uvalue}`,
2214
+ jsonValue,
2215
+ string: storageValue === 'J'
2216
+ ? null
2217
+ : PostgreSQLDriver.escapeNulls(`${uvalue}`),
1556
2218
  number: isNaN(Number(uvalue)) ? null : Number(uvalue),
2219
+ truthy: !!uvalue,
1557
2220
  },
1558
2221
  }));
1559
2222
  const references = this.findReferences(value);
@@ -1568,43 +2231,130 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1568
2231
  }));
1569
2232
  }
1570
2233
  }
1571
- await Promise.all(promises);
1572
- }, async (name, curUid) => {
1573
- await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}uids`)} WHERE "name"=@name;`, {
1574
- params: {
1575
- name,
1576
- },
1577
- });
1578
- await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}uids`)} ("name", "cur_uid") VALUES (@name, @curUid);`, {
2234
+ }
2235
+ if (only === 'tilmeldAC') {
2236
+ let { user, group, acUser, acGroup, acOther, acRead, acWrite, acFull } = this.removeAndReturnACValues(etype, {}, sdata);
2237
+ promises.push(this.queryRun(`UPDATE ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} SET "user"=${user == null ? '@user' : "decode(@user, 'hex')"}, "group"=${group == null ? '@group' : "decode(@group, 'hex')"}, "acUser"=@acUser, "acGroup"=@acGroup, "acOther"=@acOther, "acRead"=${!acRead?.length ? '@acRead' : "array(SELECT decode(n, 'hex') FROM unnest(@acRead::text[]) AS n)"}, "acWrite"=${!acWrite?.length ? '@acWrite' : "array(SELECT decode(n, 'hex') FROM unnest(@acWrite::text[]) AS n)"}, "acFull"=${!acFull?.length ? '@acFull' : "array(SELECT decode(n, 'hex') FROM unnest(@acFull::text[]) AS n)"} WHERE "guid"=decode(@guid, 'hex');`, {
2238
+ etypes: [etype],
1579
2239
  params: {
1580
- name,
1581
- curUid,
2240
+ user,
2241
+ group,
2242
+ acUser,
2243
+ acGroup,
2244
+ acOther,
2245
+ acRead,
2246
+ acWrite,
2247
+ acFull,
2248
+ guid,
1582
2249
  },
2250
+ }));
2251
+ }
2252
+ const EntityClass = this.nymph.getEntityClassByEtype(etype);
2253
+ if (only == null || only === 'tokens') {
2254
+ for (let name in sdata) {
2255
+ let tokenString = null;
2256
+ try {
2257
+ tokenString = EntityClass.getFTSText(name, JSON.parse(sdata[name]));
2258
+ }
2259
+ catch (e) {
2260
+ // Ignore error.
2261
+ }
2262
+ if (tokenString != null) {
2263
+ const tokens = this.tokenizer.tokenize(tokenString);
2264
+ while (tokens.length) {
2265
+ const currentTokens = tokens.splice(0, 100);
2266
+ const params = {
2267
+ guid,
2268
+ name,
2269
+ };
2270
+ const values = [];
2271
+ for (let i = 0; i < currentTokens.length; i++) {
2272
+ const token = currentTokens[i];
2273
+ params['token' + i] = token.token;
2274
+ params['position' + i] = token.position;
2275
+ params['stem' + i] = token.stem;
2276
+ values.push("(decode(@guid, 'hex'), @name, @token" +
2277
+ i +
2278
+ ', @position' +
2279
+ i +
2280
+ ', @stem' +
2281
+ i +
2282
+ ')');
2283
+ }
2284
+ promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} ("guid", "name", "token", "position", "stem") VALUES ${values.join(', ')};`, {
2285
+ etypes: [etype],
2286
+ params,
2287
+ }));
2288
+ }
2289
+ }
2290
+ }
2291
+ }
2292
+ if (only == null) {
2293
+ const uniques = await EntityClass.getUniques({
2294
+ guid,
2295
+ cdate,
2296
+ mdate,
2297
+ tags,
2298
+ data: {},
2299
+ sdata,
1583
2300
  });
1584
- }, async () => {
1585
- await this.internalTransaction('nymph-import');
1586
- }, async () => {
1587
- await this.commit('nymph-import');
2301
+ for (const unique of uniques) {
2302
+ promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} ("guid", "unique") VALUES (decode(@guid, 'hex'), @unique);`, {
2303
+ etypes: [etype],
2304
+ params: {
2305
+ guid,
2306
+ unique,
2307
+ },
2308
+ }).catch((e) => {
2309
+ if (e instanceof EntityUniqueConstraintError) {
2310
+ this.nymph.config.debugError('postgresql', `Import entity unique constraint violation for GUID "${guid}" on etype "${etype}": "${unique}"`);
2311
+ }
2312
+ return e;
2313
+ }));
2314
+ }
2315
+ }
2316
+ await Promise.all(promises);
2317
+ }
2318
+ catch (e) {
2319
+ this.nymph.config.debugError('postgresql', `Import entity error: "${e}"`);
2320
+ throw e;
2321
+ }
2322
+ }
2323
+ async importUID({ name, value }) {
2324
+ try {
2325
+ await this.internalTransaction(`nymph-import-uid-${name}`);
2326
+ await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}uids`)} WHERE "name"=@name;`, {
2327
+ params: {
2328
+ name,
2329
+ },
1588
2330
  });
1589
- return result;
2331
+ await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}uids`)} ("name", "cur_uid") VALUES (@name, @value);`, {
2332
+ params: {
2333
+ name,
2334
+ value,
2335
+ },
2336
+ });
2337
+ await this.commit(`nymph-import-uid-${name}`);
1590
2338
  }
1591
2339
  catch (e) {
1592
- await this.rollback('nymph-import');
1593
- return false;
2340
+ this.nymph.config.debugError('postgresql', `Import UID error: "${e}"`);
2341
+ await this.rollback(`nymph-import-uid-${name}`);
2342
+ throw e;
1594
2343
  }
1595
2344
  }
1596
2345
  async newUID(name) {
1597
2346
  if (name == null) {
1598
- throw new nymph_1.InvalidParametersError('Name not given for UID.');
2347
+ throw new InvalidParametersError('Name not given for UID.');
1599
2348
  }
1600
2349
  await this.internalTransaction('nymph-newuid');
2350
+ let curUid = undefined;
1601
2351
  try {
1602
2352
  const lock = await this.queryGet(`SELECT "cur_uid" FROM ${PostgreSQLDriver.escape(`${this.prefix}uids`)} WHERE "name"=@name FOR UPDATE;`, {
1603
2353
  params: {
1604
2354
  name,
1605
2355
  },
1606
2356
  });
1607
- let curUid = lock?.cur_uid == null ? undefined : Number(lock.cur_uid);
2357
+ curUid = lock?.cur_uid == null ? undefined : Number(lock.cur_uid);
1608
2358
  if (curUid == null) {
1609
2359
  curUid = 1;
1610
2360
  await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}uids`)} ("name", "cur_uid") VALUES (@name, @curUid);`, {
@@ -1623,17 +2373,18 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1623
2373
  },
1624
2374
  });
1625
2375
  }
1626
- await this.commit('nymph-newuid');
1627
- return curUid;
1628
2376
  }
1629
2377
  catch (e) {
2378
+ this.nymph.config.debugError('postgresql', `New UID error: "${e}"`);
1630
2379
  await this.rollback('nymph-newuid');
1631
2380
  throw e;
1632
2381
  }
2382
+ await this.commit('nymph-newuid');
2383
+ return curUid;
1633
2384
  }
1634
2385
  async renameUID(oldName, newName) {
1635
2386
  if (oldName == null || newName == null) {
1636
- throw new nymph_1.InvalidParametersError('Name not given for UID.');
2387
+ throw new InvalidParametersError('Name not given for UID.');
1637
2388
  }
1638
2389
  await this.queryRun(`UPDATE ${PostgreSQLDriver.escape(`${this.prefix}uids`)} SET "name"=@newName WHERE "name"=@oldName;`, {
1639
2390
  params: {
@@ -1645,7 +2396,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1645
2396
  }
1646
2397
  async rollback(name) {
1647
2398
  if (name == null || typeof name !== 'string' || name.length === 0) {
1648
- throw new nymph_1.InvalidParametersError('Transaction rollback attempted without a name.');
2399
+ throw new InvalidParametersError('Transaction rollback attempted without a name.');
1649
2400
  }
1650
2401
  if (!this.transaction || this.transaction.count === 0) {
1651
2402
  this.transaction = null;
@@ -1662,7 +2413,8 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1662
2413
  return true;
1663
2414
  }
1664
2415
  async saveEntity(entity) {
1665
- const insertData = async (guid, data, sdata, etype) => {
2416
+ const insertData = async (guid, data, sdata, uniques, etype) => {
2417
+ const EntityClass = this.nymph.getEntityClassByEtype(etype);
1666
2418
  const runInsertQuery = async (name, value, svalue) => {
1667
2419
  if (value === undefined) {
1668
2420
  return;
@@ -1671,24 +2423,23 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1671
2423
  ? 'N'
1672
2424
  : typeof value === 'string'
1673
2425
  ? 'S'
1674
- : svalue;
2426
+ : 'J';
2427
+ const jsonValue = storageValue === 'J'
2428
+ ? PostgreSQLDriver.escapeNullSequences(svalue)
2429
+ : null;
1675
2430
  const promises = [];
1676
- promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} ("guid", "name", "value") VALUES (decode(@guid, 'hex'), @name, @storageValue);`, {
2431
+ promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} ("guid", "name", "value", "json", "string", "number", "truthy") VALUES (decode(@guid, 'hex'), @name, @storageValue, @jsonValue, @string, @number, @truthy);`, {
1677
2432
  etypes: [etype],
1678
2433
  params: {
1679
2434
  guid,
1680
2435
  name,
1681
2436
  storageValue,
1682
- },
1683
- }));
1684
- promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} ("guid", "name", "truthy", "string", "number") VALUES (decode(@guid, 'hex'), @name, @truthy, @string, @number);`, {
1685
- etypes: [etype],
1686
- params: {
1687
- guid,
1688
- name,
1689
- truthy: !!value,
1690
- string: `${value}`,
2437
+ jsonValue,
2438
+ string: storageValue === 'J'
2439
+ ? null
2440
+ : PostgreSQLDriver.escapeNulls(`${value}`),
1691
2441
  number: isNaN(Number(value)) ? null : Number(value),
2442
+ truthy: !!value,
1692
2443
  },
1693
2444
  }));
1694
2445
  const references = this.findReferences(svalue);
@@ -1702,8 +2453,60 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1702
2453
  },
1703
2454
  }));
1704
2455
  }
2456
+ let tokenString = null;
2457
+ try {
2458
+ tokenString = EntityClass.getFTSText(name, value);
2459
+ }
2460
+ catch (e) {
2461
+ // Ignore error.
2462
+ }
2463
+ if (tokenString != null) {
2464
+ const tokens = this.tokenizer.tokenize(tokenString);
2465
+ while (tokens.length) {
2466
+ const currentTokens = tokens.splice(0, 100);
2467
+ const params = {
2468
+ guid,
2469
+ name,
2470
+ };
2471
+ const values = [];
2472
+ for (let i = 0; i < currentTokens.length; i++) {
2473
+ const token = currentTokens[i];
2474
+ params['token' + i] = token.token;
2475
+ params['position' + i] = token.position;
2476
+ params['stem' + i] = token.stem;
2477
+ values.push("(decode(@guid, 'hex'), @name, @token" +
2478
+ i +
2479
+ ', @position' +
2480
+ i +
2481
+ ', @stem' +
2482
+ i +
2483
+ ')');
2484
+ }
2485
+ promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} ("guid", "name", "token", "position", "stem") VALUES ${values.join(', ')};`, {
2486
+ etypes: [etype],
2487
+ params,
2488
+ }));
2489
+ }
2490
+ }
1705
2491
  await Promise.all(promises);
1706
2492
  };
2493
+ for (const unique of uniques) {
2494
+ try {
2495
+ await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} ("guid", "unique") VALUES (decode(@guid, 'hex'), @unique);`, {
2496
+ etypes: [etype],
2497
+ params: {
2498
+ guid,
2499
+ unique,
2500
+ },
2501
+ });
2502
+ }
2503
+ catch (e) {
2504
+ if (e instanceof EntityUniqueConstraintError) {
2505
+ this.nymph.config.debugError('postgresql', `Save entity unique constraint violation for GUID "${guid}" on etype "${etype}": "${unique}"`);
2506
+ }
2507
+ throw e;
2508
+ }
2509
+ }
1707
2510
  for (const name in data) {
1708
2511
  await runInsertQuery(name, data[name], JSON.stringify(data[name]));
1709
2512
  }
@@ -1711,19 +2514,38 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1711
2514
  await runInsertQuery(name, JSON.parse(sdata[name]), sdata[name]);
1712
2515
  }
1713
2516
  };
2517
+ let inTransaction = false;
1714
2518
  try {
1715
- const result = await this.saveEntityRowLike(entity, async (_entity, guid, tags, data, sdata, cdate, etype) => {
1716
- await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid", "tags", "cdate", "mdate") VALUES (decode(@guid, 'hex'), @tags, @cdate, @cdate);`, {
2519
+ const result = await this.saveEntityRowLike(entity, async ({ guid, tags, data, sdata, uniques, cdate, etype }) => {
2520
+ if (Object.keys(data).length === 0 &&
2521
+ Object.keys(sdata).length === 0) {
2522
+ return false;
2523
+ }
2524
+ let { user, group, acUser, acGroup, acOther, acRead, acWrite, acFull, } = this.removeAndReturnACValues(etype, data, sdata);
2525
+ await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid", "tags", "cdate", "mdate", "user", "group", "acUser", "acGroup", "acOther", "acRead", "acWrite", "acFull") VALUES (decode(@guid, 'hex'), @tags, @cdate, @cdate, ${user == null ? '@user' : "decode(@user, 'hex')"}, ${group == null ? '@group' : "decode(@group, 'hex')"}, @acUser, @acGroup, @acOther, ${!acRead?.length ? '@acRead' : "array(SELECT decode(n, 'hex') FROM unnest(@acRead::text[]) AS n)"}, ${!acWrite?.length ? '@acWrite' : "array(SELECT decode(n, 'hex') FROM unnest(@acWrite::text[]) AS n)"}, ${!acFull?.length ? '@acFull' : "array(SELECT decode(n, 'hex') FROM unnest(@acFull::text[]) AS n)"});`, {
1717
2526
  etypes: [etype],
1718
2527
  params: {
1719
2528
  guid,
1720
2529
  tags,
1721
2530
  cdate,
2531
+ user,
2532
+ group,
2533
+ acUser,
2534
+ acGroup,
2535
+ acOther,
2536
+ acRead,
2537
+ acWrite,
2538
+ acFull,
1722
2539
  },
1723
2540
  });
1724
- await insertData(guid, data, sdata, etype);
2541
+ await insertData(guid, data, sdata, uniques, etype);
1725
2542
  return true;
1726
- }, async (entity, guid, tags, data, sdata, mdate, etype) => {
2543
+ }, async ({ entity, guid, tags, data, sdata, uniques, mdate, etype }) => {
2544
+ if (Object.keys(data).length === 0 &&
2545
+ Object.keys(sdata).length === 0) {
2546
+ return false;
2547
+ }
2548
+ let { user, group, acUser, acGroup, acOther, acRead, acWrite, acFull, } = this.removeAndReturnACValues(etype, data, sdata);
1727
2549
  const promises = [];
1728
2550
  promises.push(this.queryRun(`SELECT 1 FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`, {
1729
2551
  etypes: [etype],
@@ -1737,24 +2559,38 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1737
2559
  guid,
1738
2560
  },
1739
2561
  }));
1740
- promises.push(this.queryRun(`SELECT 1 FROM ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`, {
2562
+ promises.push(this.queryRun(`SELECT 1 FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`, {
1741
2563
  etypes: [etype],
1742
2564
  params: {
1743
2565
  guid,
1744
2566
  },
1745
2567
  }));
1746
- promises.push(this.queryRun(`SELECT 1 FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`, {
2568
+ promises.push(this.queryRun(`SELECT 1 FROM ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`, {
2569
+ etypes: [etype],
2570
+ params: {
2571
+ guid,
2572
+ },
2573
+ }));
2574
+ promises.push(this.queryRun(`SELECT 1 FROM ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`, {
1747
2575
  etypes: [etype],
1748
2576
  params: {
1749
2577
  guid,
1750
2578
  },
1751
2579
  }));
1752
2580
  await Promise.all(promises);
1753
- const info = await this.queryRun(`UPDATE ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} SET "tags"=@tags, "mdate"=@mdate WHERE "guid"=decode(@guid, 'hex') AND "mdate" <= @emdate;`, {
2581
+ const info = await this.queryRun(`UPDATE ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} SET "tags"=@tags, "mdate"=@mdate, "user"=${user == null ? '@user' : "decode(@user, 'hex')"}, "group"=${group == null ? '@group' : "decode(@group, 'hex')"}, "acUser"=@acUser, "acGroup"=@acGroup, "acOther"=@acOther, "acRead"=${!acRead?.length ? '@acRead' : "array(SELECT decode(n, 'hex') FROM unnest(@acRead::text[]) AS n)"}, "acWrite"=${!acWrite?.length ? '@acWrite' : "array(SELECT decode(n, 'hex') FROM unnest(@acWrite::text[]) AS n)"}, "acFull"=${!acFull?.length ? '@acFull' : "array(SELECT decode(n, 'hex') FROM unnest(@acFull::text[]) AS n)"} WHERE "guid"=decode(@guid, 'hex') AND "mdate" <= @emdate;`, {
1754
2582
  etypes: [etype],
1755
2583
  params: {
1756
2584
  tags,
1757
2585
  mdate,
2586
+ user,
2587
+ group,
2588
+ acUser,
2589
+ acGroup,
2590
+ acOther,
2591
+ acRead,
2592
+ acWrite,
2593
+ acFull,
1758
2594
  guid,
1759
2595
  emdate: isNaN(Number(entity.mdate)) ? 0 : Number(entity.mdate),
1760
2596
  },
@@ -1768,44 +2604,57 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1768
2604
  guid,
1769
2605
  },
1770
2606
  }));
1771
- promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
2607
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1772
2608
  etypes: [etype],
1773
2609
  params: {
1774
2610
  guid,
1775
2611
  },
1776
2612
  }));
1777
- promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
2613
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
2614
+ etypes: [etype],
2615
+ params: {
2616
+ guid,
2617
+ },
2618
+ }));
2619
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1778
2620
  etypes: [etype],
1779
2621
  params: {
1780
2622
  guid,
1781
2623
  },
1782
2624
  }));
1783
2625
  await Promise.all(promises);
1784
- await insertData(guid, data, sdata, etype);
2626
+ await insertData(guid, data, sdata, uniques, etype);
1785
2627
  success = true;
1786
2628
  }
1787
2629
  return success;
1788
2630
  }, async () => {
1789
2631
  await this.internalTransaction('nymph-save');
2632
+ inTransaction = true;
1790
2633
  }, async (success) => {
1791
- if (success) {
1792
- await this.commit('nymph-save');
1793
- }
1794
- else {
1795
- await this.rollback('nymph-save');
2634
+ if (inTransaction) {
2635
+ inTransaction = false;
2636
+ if (success) {
2637
+ await this.commit('nymph-save');
2638
+ }
2639
+ else {
2640
+ await this.rollback('nymph-save');
2641
+ }
1796
2642
  }
1797
2643
  return success;
1798
2644
  });
1799
2645
  return result;
1800
2646
  }
1801
2647
  catch (e) {
1802
- await this.rollback('nymph-save');
2648
+ this.nymph.config.debugError('postgresql', `Save entity error: "${e}"`);
2649
+ if (inTransaction) {
2650
+ await this.rollback('nymph-save');
2651
+ }
1803
2652
  throw e;
1804
2653
  }
1805
2654
  }
1806
2655
  async setUID(name, curUid) {
1807
2656
  if (name == null) {
1808
- throw new nymph_1.InvalidParametersError('Name not given for UID.');
2657
+ throw new InvalidParametersError('Name not given for UID.');
1809
2658
  }
1810
2659
  await this.internalTransaction('nymph-setuid');
1811
2660
  try {
@@ -1821,39 +2670,161 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1821
2670
  curUid,
1822
2671
  },
1823
2672
  });
1824
- await this.commit('nymph-setuid');
1825
- return true;
1826
2673
  }
1827
2674
  catch (e) {
1828
2675
  await this.rollback('nymph-setuid');
1829
2676
  throw e;
1830
2677
  }
2678
+ await this.commit('nymph-setuid');
2679
+ return true;
1831
2680
  }
1832
- async internalTransaction(name) {
2681
+ async internalTransaction(name, setOnSelf = true) {
1833
2682
  if (name == null || typeof name !== 'string' || name.length === 0) {
1834
- throw new nymph_1.InvalidParametersError('Transaction start attempted without a name.');
2683
+ throw new InvalidParametersError('Transaction start attempted without a name.');
1835
2684
  }
1836
- if (!this.transaction || this.transaction.count === 0) {
1837
- this.transaction = {
2685
+ let transaction = this.transaction;
2686
+ if (!transaction) {
2687
+ // Lock to one connection.
2688
+ transaction = {
1838
2689
  count: 0,
1839
2690
  connection: await this.getConnection(),
2691
+ latestPromise: Promise.resolve(),
1840
2692
  };
1841
- await this.queryRun('BEGIN;');
2693
+ if (setOnSelf) {
2694
+ this.transaction = transaction;
2695
+ }
2696
+ }
2697
+ if (transaction.count === 0) {
2698
+ // We're not in a transaction yet, so start one.
2699
+ await this.queryRun('BEGIN;', { transaction });
1842
2700
  }
1843
- await this.queryRun(`SAVEPOINT ${PostgreSQLDriver.escape(name)};`);
1844
- this.transaction.count++;
1845
- return this.transaction;
2701
+ transaction.count++;
2702
+ await this.queryRun(`SAVEPOINT ${PostgreSQLDriver.escape(name)};`, {
2703
+ transaction,
2704
+ });
2705
+ return transaction;
1846
2706
  }
1847
2707
  async startTransaction(name) {
1848
- const inTransaction = this.inTransaction();
1849
- const transaction = await this.internalTransaction(name);
1850
- if (!inTransaction) {
1851
- this.transaction = null;
1852
- }
2708
+ const transaction = await this.internalTransaction(name, false);
1853
2709
  const nymph = this.nymph.clone();
1854
2710
  nymph.driver.transaction = transaction;
1855
2711
  return nymph;
1856
2712
  }
2713
+ async removeTilmeldOldRows(etype, connection) {
2714
+ await this.internalTransaction('nymph-remove-tilmeld-rows');
2715
+ try {
2716
+ for (let name of [
2717
+ 'user',
2718
+ 'group',
2719
+ 'acUser',
2720
+ 'acGroup',
2721
+ 'acOther',
2722
+ 'acRead',
2723
+ 'acWrite',
2724
+ 'acFull',
2725
+ ]) {
2726
+ await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} WHERE "name"=@name;`, {
2727
+ etypes: [etype],
2728
+ params: {
2729
+ name,
2730
+ },
2731
+ connection,
2732
+ });
2733
+ await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "name"=@name;`, {
2734
+ etypes: [etype],
2735
+ params: {
2736
+ name,
2737
+ },
2738
+ connection,
2739
+ });
2740
+ await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} WHERE "name"=@name;`, {
2741
+ etypes: [etype],
2742
+ params: {
2743
+ name,
2744
+ },
2745
+ connection,
2746
+ });
2747
+ }
2748
+ }
2749
+ catch (e) {
2750
+ this.nymph.config.debugError('postgresql', `Remove tilmeld rows error: "${e}"`);
2751
+ await this.rollback('nymph-remove-tilmeld-rows');
2752
+ throw e;
2753
+ }
2754
+ await this.commit('nymph-remove-tilmeld-rows');
2755
+ return true;
2756
+ }
2757
+ async needsMigration() {
2758
+ const table = await this.queryGet('SELECT "table_name" AS "table_name" FROM "information_schema"."tables" WHERE "table_catalog"=@db AND "table_schema"=\'public\' AND "table_name" LIKE @prefix LIMIT 1;', {
2759
+ params: {
2760
+ db: this.config.database,
2761
+ prefix: this.prefix + 'data_' + '%',
2762
+ },
2763
+ });
2764
+ if (table?.table_name) {
2765
+ const result = await this.queryGet('SELECT 1 AS "exists" FROM "information_schema"."columns" WHERE "table_catalog"=@db AND "table_schema"=\'public\' AND "table_name"=@table AND "column_name"=\'json\';', {
2766
+ params: {
2767
+ db: this.config.database,
2768
+ table: table.table_name,
2769
+ },
2770
+ });
2771
+ if (!result?.exists) {
2772
+ return 'json';
2773
+ }
2774
+ }
2775
+ const table2 = await this.queryGet('SELECT "table_name" AS "table_name" FROM "information_schema"."tables" WHERE "table_catalog"=@db AND "table_schema"=\'public\' AND "table_name" LIKE @tokenTable LIMIT 1;', {
2776
+ params: {
2777
+ db: this.config.database,
2778
+ tokenTable: this.prefix + 'tokens_' + '%',
2779
+ },
2780
+ });
2781
+ if (!table2 || !table2.table_name) {
2782
+ return 'tokens';
2783
+ }
2784
+ const table3 = await this.queryGet('SELECT "table_name" AS "table_name" FROM "information_schema"."tables" WHERE "table_catalog"=@db AND "table_schema"=\'public\' AND "table_name" LIKE @prefix LIMIT 1;', {
2785
+ params: {
2786
+ db: this.config.database,
2787
+ prefix: this.prefix + 'entities_' + '%',
2788
+ },
2789
+ });
2790
+ if (table3?.table_name) {
2791
+ const result = await this.queryGet('SELECT 1 AS "exists" FROM "information_schema"."columns" WHERE "table_catalog"=@db AND "table_schema"=\'public\' AND "table_name"=@table AND "column_name"=\'user\';', {
2792
+ params: {
2793
+ db: this.config.database,
2794
+ table: table3.table_name,
2795
+ },
2796
+ });
2797
+ if (!result?.exists) {
2798
+ return 'tilmeldColumns';
2799
+ }
2800
+ }
2801
+ return false;
2802
+ }
2803
+ async liveMigration(migrationType) {
2804
+ if (migrationType === 'tokenTables') {
2805
+ const etypes = await this.getEtypes();
2806
+ const connection = await this.getConnection(true);
2807
+ for (let etype of etypes) {
2808
+ await this.createTokensTable(etype, connection);
2809
+ }
2810
+ connection.done();
2811
+ }
2812
+ else if (migrationType === 'tilmeldColumns') {
2813
+ const etypes = await this.getEtypes();
2814
+ const connection = await this.getConnection(true);
2815
+ for (let etype of etypes) {
2816
+ await this.addTilmeldColumnsAndIndexes(etype, connection);
2817
+ }
2818
+ connection.done();
2819
+ }
2820
+ else if (migrationType === 'tilmeldRemoveOldRows') {
2821
+ const etypes = await this.getEtypes();
2822
+ const connection = await this.getConnection(true);
2823
+ for (let etype of etypes) {
2824
+ await this.removeTilmeldOldRows(etype, connection);
2825
+ }
2826
+ connection.done();
2827
+ }
2828
+ }
1857
2829
  }
1858
- exports.default = PostgreSQLDriver;
1859
2830
  //# sourceMappingURL=PostgreSQLDriver.js.map