@nymphjs/driver-postgresql 1.0.0-beta.11 → 1.0.0-beta.111

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,49 +361,32 @@ 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
392
  const results = await new Promise((resolve, reject) => {
@@ -249,32 +402,30 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
249
402
  return results.rows;
250
403
  }, `${query} -- ${JSON.stringify(params)}`, etypes);
251
404
  }
252
- queryIterSync(query, { etypes = [], params = {}, } = {}) {
405
+ async queryIter(query, { etypes = [], params = {}, } = {}) {
253
406
  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,
265
- });
266
- try {
267
- return JSON.parse(output.stdout).rows;
268
- }
269
- catch (e) {
270
- }
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];
407
+ const that = this;
408
+ return this.query(async function* () {
409
+ const transaction = !!that.transaction?.connection;
410
+ const connection = await that.getConnection();
411
+ const cursor = new Cursor(newQuery, newParams);
412
+ const iter = connection.client.query(cursor);
413
+ while (true) {
414
+ const rows = await iter.read(100);
415
+ if (!rows.length) {
416
+ await new Promise((resolve) => {
417
+ iter.close(() => {
418
+ if (!transaction) {
419
+ connection.done();
420
+ }
421
+ resolve();
422
+ });
423
+ });
424
+ return;
425
+ }
426
+ for (let row of rows) {
427
+ yield row;
428
+ }
278
429
  }
279
430
  }, `${query} -- ${JSON.stringify(params)}`, etypes);
280
431
  }
@@ -294,12 +445,13 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
294
445
  return results.rows[0];
295
446
  }, `${query} -- ${JSON.stringify(params)}`, etypes);
296
447
  }
297
- queryRun(query, { etypes = [], params = {}, } = {}) {
448
+ queryRun(query, { etypes = [], params = {}, connection, } = {}) {
298
449
  const { query: newQuery, params: newParams } = this.translateQuery(query, params);
299
450
  return this.query(async () => {
300
451
  const results = await new Promise((resolve, reject) => {
301
452
  try {
302
- (this.transaction?.connection?.client ?? this.link)
453
+ ((connection ?? this.transaction?.connection)?.client ??
454
+ this.link)
303
455
  .query(newQuery, newParams)
304
456
  .then((results) => resolve(results), (error) => reject(error));
305
457
  }
@@ -310,39 +462,9 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
310
462
  return { rowCount: results.rowCount ?? 0 };
311
463
  }, `${query} -- ${JSON.stringify(params)}`, etypes);
312
464
  }
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
465
  async commit(name) {
344
466
  if (name == null || typeof name !== 'string' || name.length === 0) {
345
- throw new nymph_1.InvalidParametersError('Transaction commit attempted without a name.');
467
+ throw new InvalidParametersError('Transaction commit attempted without a name.');
346
468
  }
347
469
  if (!this.transaction || this.transaction.count === 0) {
348
470
  this.transaction = null;
@@ -382,32 +504,40 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
382
504
  guid,
383
505
  },
384
506
  });
385
- await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
507
+ await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
386
508
  etypes: [etype],
387
509
  params: {
388
510
  guid,
389
511
  },
390
512
  });
391
- await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
513
+ await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
514
+ etypes: [etype],
515
+ params: {
516
+ guid,
517
+ },
518
+ });
519
+ await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
392
520
  etypes: [etype],
393
521
  params: {
394
522
  guid,
395
523
  },
396
524
  });
397
- await this.commit('nymph-delete');
398
- if (this.nymph.config.cache) {
399
- this.cleanCache(guid);
400
- }
401
- return true;
402
525
  }
403
526
  catch (e) {
527
+ this.nymph.config.debugError('postgresql', `Delete entity error: "${e}"`);
404
528
  await this.rollback('nymph-delete');
405
529
  throw e;
406
530
  }
531
+ await this.commit('nymph-delete');
532
+ // Remove any cached versions of this entity.
533
+ if (this.nymph.config.cache) {
534
+ this.cleanCache(guid);
535
+ }
536
+ return true;
407
537
  }
408
538
  async deleteUID(name) {
409
539
  if (!name) {
410
- throw new nymph_1.InvalidParametersError('Name not given for UID');
540
+ throw new InvalidParametersError('Name not given for UID');
411
541
  }
412
542
  await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}uids`)} WHERE "name"=@name;`, {
413
543
  params: {
@@ -416,79 +546,223 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
416
546
  });
417
547
  return true;
418
548
  }
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;');
549
+ async getIndexes(etype) {
550
+ const indexes = [];
551
+ for (let [scope, suffix] of [
552
+ ['data', '_json'],
553
+ ['references', '_reference_guid'],
554
+ ['tokens', '_token_position_stem'],
555
+ ]) {
556
+ const indexDefinitions = await this.queryArray(`SELECT * FROM "pg_indexes" WHERE "indexname" LIKE @pattern;`, {
557
+ params: {
558
+ pattern: `${this.prefix}${scope}_${etype}_id_custom_%${suffix}`,
559
+ },
560
+ });
561
+ for (const indexDefinition of indexDefinitions) {
562
+ indexes.push({
563
+ scope,
564
+ name: indexDefinition.indexname.substring(`${this.prefix}${scope}_${etype}_id_custom_`.length, indexDefinition.indexname.length - suffix.length),
565
+ property: (indexDefinition.indexdef.match(/WHERE\s+\(?\s*name\s*=\s*'(.*)'/) ?? [])[1] ?? '',
566
+ });
567
+ }
568
+ }
569
+ return indexes;
570
+ }
571
+ async addIndex(etype, definition) {
572
+ this.checkIndexName(definition.name);
573
+ await this.deleteIndex(etype, definition.scope, definition.name);
574
+ const connection = await this.getConnection(true);
575
+ if (definition.scope === 'data') {
576
+ 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 });
577
+ 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 });
578
+ 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 });
579
+ 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 });
580
+ 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 });
581
+ }
582
+ else if (definition.scope === 'references') {
583
+ 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 });
584
+ }
585
+ else if (definition.scope === 'tokens') {
586
+ 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 });
587
+ }
588
+ connection.done();
589
+ return true;
590
+ }
591
+ async deleteIndex(etype, scope, name) {
592
+ this.checkIndexName(name);
593
+ const connection = await this.getConnection(true);
594
+ if (scope === 'data') {
595
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${name}_json`)};`, { connection });
596
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${name}_string_gin`)};`, { connection });
597
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${name}_string_btree`)};`, { connection });
598
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${name}_number`)};`, { connection });
599
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_custom_${name}_truthy`)};`, { connection });
600
+ }
601
+ else if (scope === 'references') {
602
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_custom_${name}_reference_guid`)};`, { connection });
603
+ }
604
+ else if (scope === 'tokens') {
605
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}_id_custom_${name}_token_position_stem`)};`, { connection });
606
+ }
607
+ connection.done();
608
+ return true;
609
+ }
610
+ async getEtypes() {
611
+ 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;', {
612
+ params: {
613
+ db: this.config.database,
614
+ prefix: this.prefix + 'entities_' + '%',
615
+ },
616
+ });
440
617
  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));
618
+ for (const table of tables) {
619
+ etypes.push(table.table_name.substr((this.prefix + 'entities_').length));
620
+ }
621
+ return etypes;
622
+ }
623
+ async *exportDataIterator() {
624
+ if (yield {
625
+ type: 'comment',
626
+ content: `#nex2
627
+ # Nymph Entity Exchange v2
628
+ # http://nymph.io
629
+ #
630
+ # Generation Time: ${new Date().toLocaleString()}
631
+ `,
632
+ }) {
633
+ return;
634
+ }
635
+ if (yield {
636
+ type: 'comment',
637
+ content: `
638
+
639
+ #
640
+ # UIDs
641
+ #
642
+
643
+ `,
644
+ }) {
645
+ return;
646
+ }
647
+ // Export UIDs.
648
+ let uids = await this.queryIter(`SELECT * FROM ${PostgreSQLDriver.escape(`${this.prefix}uids`)} ORDER BY "name";`);
649
+ for await (const uid of uids) {
650
+ if (yield { type: 'uid', content: `<${uid.name}>[${uid.cur_uid}]\n` }) {
651
+ return;
445
652
  }
446
653
  }
654
+ if (yield {
655
+ type: 'comment',
656
+ content: `
657
+
658
+ #
659
+ # Entities
660
+ #
661
+
662
+ `,
663
+ }) {
664
+ return;
665
+ }
666
+ // Get the etypes.
667
+ const etypes = await this.getEtypes();
447
668
  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"
669
+ // Export entities.
670
+ 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
671
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} e
450
672
  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();
673
+ ORDER BY e."guid";`);
674
+ let datum = await dataIterator.next();
454
675
  while (!datum.done) {
455
676
  const guid = datum.value.guid;
456
- const tags = datum.value.tags.join(',');
677
+ const tags = datum.value.tags.filter((tag) => tag).join(',');
457
678
  const cdate = datum.value.cdate;
458
679
  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) {
680
+ const user = datum.value.user;
681
+ const group = datum.value.group;
682
+ const acUser = datum.value.acUser;
683
+ const acGroup = datum.value.acGroup;
684
+ const acOther = datum.value.acOther;
685
+ const acRead = datum.value.acRead?.filter((guid) => guid);
686
+ const acWrite = datum.value.acWrite?.filter((guid) => guid);
687
+ const acFull = datum.value.acFull?.filter((guid) => guid);
688
+ let currentEntityExport = [];
689
+ currentEntityExport.push(`{${guid}}<${etype}>[${tags}]`);
690
+ currentEntityExport.push(`\tcdate=${JSON.stringify(cdate)}`);
691
+ currentEntityExport.push(`\tmdate=${JSON.stringify(mdate)}`);
692
+ if (this.nymph.tilmeld != null) {
693
+ if (user != null) {
694
+ currentEntityExport.push(`\tuser=${JSON.stringify(['nymph_entity_reference', user, 'User'])}`);
695
+ }
696
+ if (group != null) {
697
+ currentEntityExport.push(`\tgroup=${JSON.stringify(['nymph_entity_reference', group, 'Group'])}`);
698
+ }
699
+ if (acUser != null) {
700
+ currentEntityExport.push(`\tacUser=${JSON.stringify(acUser)}`);
701
+ }
702
+ if (acGroup != null) {
703
+ currentEntityExport.push(`\tacGroup=${JSON.stringify(acGroup)}`);
704
+ }
705
+ if (acOther != null) {
706
+ currentEntityExport.push(`\tacOther=${JSON.stringify(acOther)}`);
707
+ }
708
+ if (acRead != null) {
709
+ currentEntityExport.push(`\tacRead=${JSON.stringify(acRead)}`);
710
+ }
711
+ if (acWrite != null) {
712
+ currentEntityExport.push(`\tacWrite=${JSON.stringify(acWrite)}`);
713
+ }
714
+ if (acFull != null) {
715
+ currentEntityExport.push(`\tacFull=${JSON.stringify(acFull)}`);
716
+ }
717
+ }
718
+ if (datum.value.name != null) {
719
+ // This do will keep going and adding the data until the
720
+ // next entity is reached. $row will end on the next entity.
463
721
  do {
464
- const value = datum.value.dvalue === 'N'
722
+ const value = datum.value.value === 'N'
465
723
  ? 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();
724
+ : datum.value.value === 'S'
725
+ ? JSON.stringify(PostgreSQLDriver.unescapeNulls(datum.value.string))
726
+ : datum.value.value === 'J'
727
+ ? PostgreSQLDriver.unescapeNullSequences(JSON.stringify(datum.value.json))
728
+ : datum.value.value;
729
+ currentEntityExport.push(`\t${datum.value.name}=${value}`);
730
+ datum = await dataIterator.next();
471
731
  } while (!datum.done && datum.value.guid === guid);
472
732
  }
473
733
  else {
474
- datum = dataIterator.next();
734
+ // Make sure that datum is incremented :)
735
+ datum = await dataIterator.next();
736
+ }
737
+ currentEntityExport.push('');
738
+ if (yield { type: 'entity', content: currentEntityExport.join('\n') }) {
739
+ return;
475
740
  }
476
741
  }
477
742
  }
478
- return;
479
743
  }
480
- makeEntityQuery(options, formattedSelectors, etype, count = { i: 0 }, params = {}, subquery = false, tableSuffix = '', etypes = []) {
744
+ /**
745
+ * Generate the PostgreSQL query.
746
+ * @param options The options array.
747
+ * @param formattedSelectors The formatted selector array.
748
+ * @param etype
749
+ * @param count Used to track internal params.
750
+ * @param params Used to store internal params.
751
+ * @param subquery Whether only a subquery should be returned.
752
+ * @returns The SQL query.
753
+ */
754
+ makeEntityQuery(options, formattedSelectors, etype, count = { i: 0 }, params = {}, subquery = false, tableSuffix = '', etypes = [], guidSelector = undefined, guidExplicitSelector = undefined) {
481
755
  if (typeof options.class?.alterOptions === 'function') {
482
756
  options = options.class.alterOptions(options);
483
757
  }
484
758
  const eTable = `e${tableSuffix}`;
485
759
  const dTable = `d${tableSuffix}`;
486
- const cTable = `c${tableSuffix}`;
487
760
  const fTable = `f${tableSuffix}`;
488
761
  const ieTable = `ie${tableSuffix}`;
489
762
  const countTable = `count${tableSuffix}`;
490
- const sort = options.sort ?? 'cdate';
491
- const queryParts = this.iterateSelectorsForQuery(formattedSelectors, (key, value, typeIsOr, typeIsNot) => {
763
+ const sTable = `s${tableSuffix}`;
764
+ const sort = options.sort === undefined ? 'cdate' : options.sort;
765
+ const queryParts = this.iterateSelectorsForQuery(formattedSelectors, ({ key, value, typeIsOr, typeIsNot }) => {
492
766
  const clauseNot = key.startsWith('!');
493
767
  let curQuery = '';
494
768
  for (const curValue of value) {
@@ -501,7 +775,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
501
775
  }
502
776
  const guid = `param${++count.i}`;
503
777
  curQuery +=
504
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
778
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
505
779
  ieTable +
506
780
  '."guid"=decode(@' +
507
781
  guid +
@@ -517,7 +791,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
517
791
  }
518
792
  const tag = `param${++count.i}`;
519
793
  curQuery +=
520
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
794
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
521
795
  '@' +
522
796
  tag +
523
797
  ' <@ ie."tags"';
@@ -530,17 +804,38 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
530
804
  if (curQuery) {
531
805
  curQuery += typeIsOr ? ' OR ' : ' AND ';
532
806
  }
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;
807
+ if (curVar === 'cdate' ||
808
+ curVar === 'mdate' ||
809
+ (this.nymph.tilmeld != null &&
810
+ (curVar === 'user' ||
811
+ curVar === 'group' ||
812
+ curVar === 'acUser' ||
813
+ curVar === 'acGroup' ||
814
+ curVar === 'acOther' ||
815
+ curVar === 'acRead' ||
816
+ curVar === 'acWrite' ||
817
+ curVar === 'acFull'))) {
818
+ curQuery +=
819
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
820
+ '(' +
821
+ ieTable +
822
+ '.' +
823
+ PostgreSQLDriver.escape(curVar) +
824
+ ' IS NOT NULL)';
825
+ }
826
+ else {
827
+ const name = `param${++count.i}`;
828
+ curQuery +=
829
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
830
+ 'EXISTS (SELECT "guid" FROM ' +
831
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
832
+ ' WHERE "guid"=' +
833
+ ieTable +
834
+ '."guid" AND "name"=@' +
835
+ name +
836
+ ')';
837
+ params[name] = curVar;
838
+ }
544
839
  }
545
840
  break;
546
841
  case 'truthy':
@@ -549,30 +844,34 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
549
844
  if (curQuery) {
550
845
  curQuery += typeIsOr ? ' OR ' : ' AND ';
551
846
  }
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') {
847
+ if (curVar === 'cdate' ||
848
+ curVar === 'mdate' ||
849
+ (this.nymph.tilmeld != null &&
850
+ (curVar === 'user' ||
851
+ curVar === 'group' ||
852
+ curVar === 'acUser' ||
853
+ curVar === 'acGroup' ||
854
+ curVar === 'acOther' ||
855
+ curVar === 'acRead' ||
856
+ curVar === 'acWrite' ||
857
+ curVar === 'acFull'))) {
561
858
  curQuery +=
562
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
859
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
563
860
  '(' +
564
861
  ieTable +
565
- '."mdate" NOT NULL)';
566
- break;
862
+ '.' +
863
+ PostgreSQLDriver.escape(curVar) +
864
+ ' IS NOT NULL)';
567
865
  }
568
866
  else {
569
867
  const name = `param${++count.i}`;
570
868
  curQuery +=
571
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
869
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
870
+ 'EXISTS (SELECT "guid" FROM ' +
871
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
872
+ ' WHERE "guid"=' +
572
873
  ieTable +
573
- '."guid" IN (SELECT "guid" FROM ' +
574
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
575
- ' WHERE "name"=@' +
874
+ '."guid" AND "name"=@' +
576
875
  name +
577
876
  ' AND "truthy"=TRUE)';
578
877
  params[name] = curVar;
@@ -581,35 +880,63 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
581
880
  break;
582
881
  case 'equal':
583
882
  case '!equal':
584
- if (curValue[0] === 'cdate') {
883
+ if (curValue[0] === 'cdate' ||
884
+ curValue[0] === 'mdate' ||
885
+ (this.nymph.tilmeld != null &&
886
+ (curValue[0] === 'acUser' ||
887
+ curValue[0] === 'acGroup' ||
888
+ curValue[0] === 'acOther'))) {
585
889
  if (curQuery) {
586
890
  curQuery += typeIsOr ? ' OR ' : ' AND ';
587
891
  }
588
- const cdate = `param${++count.i}`;
892
+ const value = `param${++count.i}`;
589
893
  curQuery +=
590
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
894
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
591
895
  ieTable +
592
- '."cdate"=@' +
593
- cdate;
594
- params[cdate] = isNaN(Number(curValue[1]))
896
+ '.' +
897
+ PostgreSQLDriver.escape(curValue[0]) +
898
+ '=@' +
899
+ value;
900
+ params[value] = isNaN(Number(curValue[1]))
595
901
  ? null
596
902
  : Number(curValue[1]);
597
- break;
598
903
  }
599
- else if (curValue[0] === 'mdate') {
904
+ else if (this.nymph.tilmeld != null &&
905
+ (curValue[0] === 'user' || curValue[0] === 'group')) {
600
906
  if (curQuery) {
601
907
  curQuery += typeIsOr ? ' OR ' : ' AND ';
602
908
  }
603
- const mdate = `param${++count.i}`;
909
+ const value = `param${++count.i}`;
604
910
  curQuery +=
605
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
911
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
606
912
  ieTable +
607
- '."mdate"=@' +
608
- mdate;
609
- params[mdate] = isNaN(Number(curValue[1]))
610
- ? null
611
- : Number(curValue[1]);
612
- break;
913
+ '.' +
914
+ PostgreSQLDriver.escape(curValue[0]) +
915
+ '=decode(@' +
916
+ value +
917
+ ", 'hex')";
918
+ params[value] = `${curValue[1]}`;
919
+ }
920
+ else if (this.nymph.tilmeld != null &&
921
+ (curValue[0] === 'acRead' ||
922
+ curValue[0] === 'acWrite' ||
923
+ curValue[0] === 'acFull')) {
924
+ if (curQuery) {
925
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
926
+ }
927
+ const value = `param${++count.i}`;
928
+ curQuery +=
929
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
930
+ ieTable +
931
+ '.' +
932
+ PostgreSQLDriver.escape(curValue[0]) +
933
+ '=' +
934
+ !curValue[1]?.length
935
+ ? '@' + value
936
+ : "array(SELECT decode(n, 'hex') FROM unnest(@" +
937
+ value +
938
+ '::text[]) AS n)';
939
+ params[value] = Array.isArray(curValue[1]) ? curValue[1] : '';
613
940
  }
614
941
  else if (typeof curValue[1] === 'number') {
615
942
  if (curQuery) {
@@ -618,11 +945,12 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
618
945
  const name = `param${++count.i}`;
619
946
  const value = `param${++count.i}`;
620
947
  curQuery +=
621
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
948
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
949
+ 'EXISTS (SELECT "guid" FROM ' +
950
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
951
+ ' WHERE "guid"=' +
622
952
  ieTable +
623
- '."guid" IN (SELECT "guid" FROM ' +
624
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
625
- ' WHERE "name"=@' +
953
+ '."guid" AND "name"=@' +
626
954
  name +
627
955
  ' AND "number"=@' +
628
956
  value +
@@ -637,17 +965,20 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
637
965
  const name = `param${++count.i}`;
638
966
  const value = `param${++count.i}`;
639
967
  curQuery +=
640
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
968
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
969
+ 'EXISTS (SELECT "guid" FROM ' +
970
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
971
+ ' WHERE "guid"=' +
641
972
  ieTable +
642
- '."guid" IN (SELECT "guid" FROM ' +
643
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
644
- ' WHERE "name"=@' +
973
+ '."guid" AND "name"=@' +
645
974
  name +
646
- ' AND "string"=@' +
647
- value +
975
+ ' AND "string"=' +
976
+ (curValue[1].length < 512
977
+ ? 'LEFT(@' + value + ', 512)'
978
+ : '@' + value) +
648
979
  ')';
649
980
  params[name] = curValue[0];
650
- params[value] = curValue[1];
981
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
651
982
  }
652
983
  else {
653
984
  if (curQuery) {
@@ -664,138 +995,272 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
664
995
  const name = `param${++count.i}`;
665
996
  const value = `param${++count.i}`;
666
997
  curQuery +=
667
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
668
- ieTable +
669
- '."guid" IN (SELECT "guid" FROM ' +
998
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
999
+ 'EXISTS (SELECT "guid" FROM ' +
670
1000
  PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
671
- ' WHERE "name"=@' +
1001
+ ' WHERE "guid"=' +
1002
+ ieTable +
1003
+ '."guid" AND "name"=@' +
672
1004
  name +
673
- ' AND "value"=@' +
1005
+ ' AND "json"=@' +
674
1006
  value +
675
1007
  ')';
676
1008
  params[name] = curValue[0];
677
- params[value] = svalue;
1009
+ params[value] = PostgreSQLDriver.escapeNullSequences(svalue);
678
1010
  }
679
1011
  break;
680
1012
  case 'contain':
681
1013
  case '!contain':
682
- if (curValue[0] === 'cdate') {
1014
+ if (curValue[0] === 'cdate' ||
1015
+ curValue[0] === 'mdate' ||
1016
+ (this.nymph.tilmeld != null &&
1017
+ (curValue[0] === 'acUser' ||
1018
+ curValue[0] === 'acGroup' ||
1019
+ curValue[0] === 'acOther'))) {
683
1020
  if (curQuery) {
684
1021
  curQuery += typeIsOr ? ' OR ' : ' AND ';
685
1022
  }
686
- const cdate = `param${++count.i}`;
1023
+ const value = `param${++count.i}`;
687
1024
  curQuery +=
688
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1025
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
689
1026
  ieTable +
690
- '."cdate"=' +
691
- cdate;
692
- params[cdate] = isNaN(Number(curValue[1]))
1027
+ '.' +
1028
+ PostgreSQLDriver.escape(curValue[0]) +
1029
+ '=@' +
1030
+ value;
1031
+ params[value] = isNaN(Number(curValue[1]))
693
1032
  ? null
694
1033
  : Number(curValue[1]);
695
- break;
696
1034
  }
697
- else if (curValue[0] === 'mdate') {
1035
+ else if (this.nymph.tilmeld != null &&
1036
+ (curValue[0] === 'user' || curValue[0] === 'group')) {
698
1037
  if (curQuery) {
699
1038
  curQuery += typeIsOr ? ' OR ' : ' AND ';
700
1039
  }
701
- const mdate = `param${++count.i}`;
1040
+ const value = `param${++count.i}`;
702
1041
  curQuery +=
703
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1042
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
704
1043
  ieTable +
705
- '."mdate"=' +
706
- mdate;
707
- params[mdate] = isNaN(Number(curValue[1]))
708
- ? null
709
- : Number(curValue[1]);
710
- break;
1044
+ '.' +
1045
+ PostgreSQLDriver.escape(curValue[0]) +
1046
+ '=decode(@' +
1047
+ value +
1048
+ ", 'hex')";
1049
+ params[value] = `${curValue[1]}`;
1050
+ }
1051
+ else if (this.nymph.tilmeld != null &&
1052
+ (curValue[0] === 'acRead' ||
1053
+ curValue[0] === 'acWrite' ||
1054
+ curValue[0] === 'acFull')) {
1055
+ if (curQuery) {
1056
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1057
+ }
1058
+ const value = `param${++count.i}`;
1059
+ curQuery +=
1060
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1061
+ 'decode(@' +
1062
+ value +
1063
+ ", 'hex')=ANY(" +
1064
+ ieTable +
1065
+ '.' +
1066
+ PostgreSQLDriver.escape(curValue[0]) +
1067
+ ')';
1068
+ params[value] = `${curValue[1]}`;
711
1069
  }
712
1070
  else {
713
1071
  if (curQuery) {
714
1072
  curQuery += typeIsOr ? ' OR ' : ' AND ';
715
1073
  }
716
1074
  let svalue;
717
- let stringValue;
718
1075
  if (curValue[1] instanceof Object &&
719
1076
  typeof curValue[1].toReference === 'function') {
720
1077
  svalue = JSON.stringify(curValue[1].toReference());
721
- stringValue = `${curValue[1].toReference()}`;
722
1078
  }
723
- else {
1079
+ else if (typeof curValue[1] === 'string' ||
1080
+ typeof curValue[1] === 'number') {
724
1081
  svalue = JSON.stringify(curValue[1]);
725
- stringValue = `${curValue[1]}`;
1082
+ }
1083
+ else {
1084
+ svalue = JSON.stringify([curValue[1]]);
726
1085
  }
727
1086
  const name = `param${++count.i}`;
728
1087
  const value = `param${++count.i}`;
729
- if (typeof curValue[1] === 'string') {
730
- const stringParam = `param${++count.i}`;
1088
+ curQuery +=
1089
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1090
+ 'EXISTS (SELECT "guid" FROM ' +
1091
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1092
+ ' WHERE "guid"=' +
1093
+ ieTable +
1094
+ '."guid" AND "name"=@' +
1095
+ name +
1096
+ ' AND "json" @> @' +
1097
+ value +
1098
+ ')';
1099
+ params[name] = curValue[0];
1100
+ params[value] = PostgreSQLDriver.escapeNullSequences(svalue);
1101
+ }
1102
+ break;
1103
+ case 'search':
1104
+ case '!search':
1105
+ if (curValue[0] === 'cdate' ||
1106
+ curValue[0] === 'mdate' ||
1107
+ (this.nymph.tilmeld != null &&
1108
+ (curValue[0] === 'user' ||
1109
+ curValue[0] === 'group' ||
1110
+ curValue[0] === 'acUser' ||
1111
+ curValue[0] === 'acGroup' ||
1112
+ curValue[0] === 'acOther' ||
1113
+ curValue[0] === 'acRead' ||
1114
+ curValue[0] === 'acWrite' ||
1115
+ curValue[0] === 'acFull'))) {
1116
+ if (curQuery) {
1117
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1118
+ }
1119
+ curQuery +=
1120
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') + '(FALSE)';
1121
+ }
1122
+ else {
1123
+ if (curQuery) {
1124
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1125
+ }
1126
+ const parsedFTSQuery = this.tokenizer.parseSearchQuery(curValue[1]);
1127
+ if (!parsedFTSQuery.length) {
731
1128
  curQuery +=
732
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
733
- '(' +
1129
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') + '(FALSE)';
1130
+ }
1131
+ else {
1132
+ const name = `param${++count.i}`;
1133
+ const queryPartToken = (term) => {
1134
+ const value = `param${++count.i}`;
1135
+ params[value] = term.token;
1136
+ return ('EXISTS (SELECT "guid" FROM ' +
1137
+ PostgreSQLDriver.escape(this.prefix + 'tokens_' + etype) +
1138
+ ' WHERE "guid"=' +
734
1139
  ieTable +
735
- '."guid" IN (SELECT "guid" FROM ' +
736
- PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
737
- ' WHERE "name"=@' +
1140
+ '."guid" AND "name"=@' +
738
1141
  name +
739
- ' AND position(@' +
1142
+ ' AND "token"=@' +
740
1143
  value +
741
- ' IN "value")>0) OR ' +
1144
+ (term.nostemmed ? ' AND "stem"=FALSE' : '') +
1145
+ ')');
1146
+ };
1147
+ const queryPartSeries = (series) => {
1148
+ const tokenTableSuffix = makeTableSuffix();
1149
+ const tokenParts = series.tokens.map((token, i) => {
1150
+ const value = `param${++count.i}`;
1151
+ params[value] = token.token;
1152
+ return {
1153
+ fromClause: i === 0
1154
+ ? 'FROM ' +
1155
+ PostgreSQLDriver.escape(this.prefix + 'tokens_' + etype) +
1156
+ ' t' +
1157
+ tokenTableSuffix +
1158
+ '0'
1159
+ : 'JOIN ' +
1160
+ PostgreSQLDriver.escape(this.prefix + 'tokens_' + etype) +
1161
+ ' t' +
1162
+ tokenTableSuffix +
1163
+ i +
1164
+ ' ON t' +
1165
+ tokenTableSuffix +
1166
+ i +
1167
+ '."guid" = t' +
1168
+ tokenTableSuffix +
1169
+ '0."guid" AND t' +
1170
+ tokenTableSuffix +
1171
+ i +
1172
+ '."name" = t' +
1173
+ tokenTableSuffix +
1174
+ '0."name" AND t' +
1175
+ tokenTableSuffix +
1176
+ i +
1177
+ '."position" = t' +
1178
+ tokenTableSuffix +
1179
+ '0."position" + ' +
1180
+ i,
1181
+ whereClause: 't' +
1182
+ tokenTableSuffix +
1183
+ i +
1184
+ '."token"=@' +
1185
+ value +
1186
+ (token.nostemmed
1187
+ ? ' AND t' + tokenTableSuffix + i + '."stem"=FALSE'
1188
+ : ''),
1189
+ };
1190
+ });
1191
+ return ('EXISTS (SELECT t' +
1192
+ tokenTableSuffix +
1193
+ '0."guid" ' +
1194
+ tokenParts.map((part) => part.fromClause).join(' ') +
1195
+ ' WHERE t' +
1196
+ tokenTableSuffix +
1197
+ '0."guid"=' +
742
1198
  ieTable +
743
- '."guid" IN (SELECT "guid" FROM ' +
744
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
745
- ' WHERE "name"=@' +
1199
+ '."guid" AND t' +
1200
+ tokenTableSuffix +
1201
+ '0."name"=@' +
746
1202
  name +
747
- ' AND "string"=@' +
748
- stringParam +
749
- '))';
750
- params[stringParam] = stringValue;
751
- }
752
- else {
1203
+ ' AND ' +
1204
+ tokenParts.map((part) => part.whereClause).join(' AND ') +
1205
+ ')');
1206
+ };
1207
+ const queryPartTerm = (term) => {
1208
+ if (term.type === 'series') {
1209
+ return queryPartSeries(term);
1210
+ }
1211
+ else if (term.type === 'not') {
1212
+ return 'NOT ' + queryPartTerm(term.operand);
1213
+ }
1214
+ else if (term.type === 'or') {
1215
+ let queryParts = [];
1216
+ for (let operand of term.operands) {
1217
+ queryParts.push(queryPartTerm(operand));
1218
+ }
1219
+ return '(' + queryParts.join(' OR ') + ')';
1220
+ }
1221
+ return queryPartToken(term);
1222
+ };
1223
+ // Run through the query and add terms.
1224
+ let termStrings = [];
1225
+ for (let term of parsedFTSQuery) {
1226
+ termStrings.push(queryPartTerm(term));
1227
+ }
753
1228
  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)';
1229
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1230
+ '(' +
1231
+ termStrings.join(' AND ') +
1232
+ ')';
1233
+ params[name] = curValue[0];
763
1234
  }
764
- params[name] = curValue[0];
765
- params[value] = svalue;
766
1235
  }
767
1236
  break;
768
1237
  case 'match':
769
1238
  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') {
1239
+ if (curValue[0] === 'cdate' ||
1240
+ curValue[0] === 'mdate' ||
1241
+ (this.nymph.tilmeld != null &&
1242
+ (curValue[0] === 'user' ||
1243
+ curValue[0] === 'group' ||
1244
+ curValue[0] === 'acUser' ||
1245
+ curValue[0] === 'acGroup' ||
1246
+ curValue[0] === 'acOther' ||
1247
+ curValue[0] === 'acRead' ||
1248
+ curValue[0] === 'acWrite' ||
1249
+ curValue[0] === 'acFull'))) {
786
1250
  if (curQuery) {
787
1251
  curQuery += typeIsOr ? ' OR ' : ' AND ';
788
1252
  }
789
- const mdate = `param${++count.i}`;
1253
+ const value = `param${++count.i}`;
790
1254
  curQuery +=
791
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1255
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
792
1256
  '(' +
793
1257
  ieTable +
794
- '."mdate" ~ @' +
795
- mdate +
1258
+ '.' +
1259
+ PostgreSQLDriver.escape(curValue[0]) +
1260
+ ' ~ @' +
1261
+ value +
796
1262
  ')';
797
- params[mdate] = curValue[1];
798
- break;
1263
+ params[value] = curValue[1];
799
1264
  }
800
1265
  else {
801
1266
  if (curQuery) {
@@ -804,50 +1269,47 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
804
1269
  const name = `param${++count.i}`;
805
1270
  const value = `param${++count.i}`;
806
1271
  curQuery +=
807
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1272
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1273
+ 'EXISTS (SELECT "guid" FROM ' +
1274
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1275
+ ' WHERE "guid"=' +
808
1276
  ieTable +
809
- '."guid" IN (SELECT "guid" FROM ' +
810
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
811
- ' WHERE "name"=@' +
1277
+ '."guid" AND "name"=@' +
812
1278
  name +
813
1279
  ' AND "string" ~ @' +
814
1280
  value +
815
1281
  ')';
816
1282
  params[name] = curValue[0];
817
- params[value] = curValue[1];
1283
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
818
1284
  }
819
1285
  break;
820
1286
  case 'imatch':
821
1287
  case '!imatch':
822
- if (curValue[0] === 'cdate') {
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'))) {
823
1299
  if (curQuery) {
824
1300
  curQuery += typeIsOr ? ' OR ' : ' AND ';
825
1301
  }
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') {
838
- if (curQuery) {
839
- curQuery += typeIsOr ? ' OR ' : ' AND ';
840
- }
841
- const mdate = `param${++count.i}`;
1302
+ const value = `param${++count.i}`;
842
1303
  curQuery +=
843
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1304
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
844
1305
  '(' +
845
1306
  ieTable +
846
- '."mdate" ~* @' +
847
- mdate +
1307
+ '.' +
1308
+ PostgreSQLDriver.escape(curValue[0]) +
1309
+ ' ~* @' +
1310
+ value +
848
1311
  ')';
849
- params[mdate] = curValue[1];
850
- break;
1312
+ params[value] = curValue[1];
851
1313
  }
852
1314
  else {
853
1315
  if (curQuery) {
@@ -856,50 +1318,47 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
856
1318
  const name = `param${++count.i}`;
857
1319
  const value = `param${++count.i}`;
858
1320
  curQuery +=
859
- ((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"=' +
860
1325
  ieTable +
861
- '."guid" IN (SELECT "guid" FROM ' +
862
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
863
- ' WHERE "name"=@' +
1326
+ '."guid" AND "name"=@' +
864
1327
  name +
865
1328
  ' AND "string" ~* @' +
866
1329
  value +
867
1330
  ')';
868
1331
  params[name] = curValue[0];
869
- params[value] = curValue[1];
1332
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
870
1333
  }
871
1334
  break;
872
1335
  case 'like':
873
1336
  case '!like':
874
- if (curValue[0] === 'cdate') {
875
- if (curQuery) {
876
- curQuery += typeIsOr ? ' OR ' : ' AND ';
877
- }
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') {
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'))) {
890
1348
  if (curQuery) {
891
1349
  curQuery += typeIsOr ? ' OR ' : ' AND ';
892
1350
  }
893
- const mdate = `param${++count.i}`;
1351
+ const value = `param${++count.i}`;
894
1352
  curQuery +=
895
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1353
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
896
1354
  '(' +
897
1355
  ieTable +
898
- '."mdate" LIKE @' +
899
- mdate +
1356
+ '.' +
1357
+ PostgreSQLDriver.escape(curValue[0]) +
1358
+ ' LIKE @' +
1359
+ value +
900
1360
  ')';
901
- params[mdate] = curValue[1];
902
- break;
1361
+ params[value] = curValue[1];
903
1362
  }
904
1363
  else {
905
1364
  if (curQuery) {
@@ -908,102 +1367,96 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
908
1367
  const name = `param${++count.i}`;
909
1368
  const value = `param${++count.i}`;
910
1369
  curQuery +=
911
- ((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"=' +
912
1374
  ieTable +
913
- '."guid" IN (SELECT "guid" FROM ' +
914
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
915
- ' WHERE "name"=@' +
1375
+ '."guid" AND "name"=@' +
916
1376
  name +
917
1377
  ' AND "string" LIKE @' +
918
1378
  value +
919
1379
  ')';
920
1380
  params[name] = curValue[0];
921
- params[value] = curValue[1];
1381
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
922
1382
  }
923
1383
  break;
924
1384
  case 'ilike':
925
1385
  case '!ilike':
926
- 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'))) {
927
1397
  if (curQuery) {
928
1398
  curQuery += typeIsOr ? ' OR ' : ' AND ';
929
1399
  }
930
- const cdate = `param${++count.i}`;
1400
+ const value = `param${++count.i}`;
931
1401
  curQuery +=
932
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1402
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
933
1403
  '(' +
934
1404
  ieTable +
935
- '."cdate" ILIKE @' +
936
- cdate +
1405
+ '.' +
1406
+ PostgreSQLDriver.escape(curValue[0]) +
1407
+ ' ILIKE @' +
1408
+ value +
937
1409
  ')';
938
- params[cdate] = curValue[1];
939
- break;
1410
+ params[value] = curValue[1];
940
1411
  }
941
- else if (curValue[0] === 'mdate') {
1412
+ else {
942
1413
  if (curQuery) {
943
1414
  curQuery += typeIsOr ? ' OR ' : ' AND ';
944
1415
  }
945
- const mdate = `param${++count.i}`;
1416
+ const name = `param${++count.i}`;
1417
+ const value = `param${++count.i}`;
946
1418
  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;
955
- }
956
- else {
957
- if (curQuery) {
958
- curQuery += typeIsOr ? ' OR ' : ' AND ';
959
- }
960
- const name = `param${++count.i}`;
961
- const value = `param${++count.i}`;
962
- curQuery +=
963
- ((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"=' +
964
1423
  ieTable +
965
- '."guid" IN (SELECT "guid" FROM ' +
966
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
967
- ' WHERE "name"=@' +
1424
+ '."guid" AND "name"=@' +
968
1425
  name +
969
1426
  ' AND "string" ILIKE @' +
970
1427
  value +
971
1428
  ')';
972
1429
  params[name] = curValue[0];
973
- params[value] = curValue[1];
1430
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
974
1431
  }
975
1432
  break;
976
1433
  case 'gt':
977
1434
  case '!gt':
978
- if (curValue[0] === 'cdate') {
979
- if (curQuery) {
980
- curQuery += typeIsOr ? ' OR ' : ' AND ';
981
- }
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') {
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'))) {
994
1446
  if (curQuery) {
995
1447
  curQuery += typeIsOr ? ' OR ' : ' AND ';
996
1448
  }
997
- const mdate = `param${++count.i}`;
1449
+ const value = `param${++count.i}`;
998
1450
  curQuery +=
999
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1451
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1000
1452
  ieTable +
1001
- '."mdate">@' +
1002
- mdate;
1003
- params[mdate] = isNaN(Number(curValue[1]))
1453
+ '.' +
1454
+ PostgreSQLDriver.escape(curValue[0]) +
1455
+ '>@' +
1456
+ value;
1457
+ params[value] = isNaN(Number(curValue[1]))
1004
1458
  ? null
1005
1459
  : Number(curValue[1]);
1006
- break;
1007
1460
  }
1008
1461
  else {
1009
1462
  if (curQuery) {
@@ -1012,11 +1465,12 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1012
1465
  const name = `param${++count.i}`;
1013
1466
  const value = `param${++count.i}`;
1014
1467
  curQuery +=
1015
- ((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"=' +
1016
1472
  ieTable +
1017
- '."guid" IN (SELECT "guid" FROM ' +
1018
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1019
- ' WHERE "name"=@' +
1473
+ '."guid" AND "name"=@' +
1020
1474
  name +
1021
1475
  ' AND "number">@' +
1022
1476
  value +
@@ -1029,35 +1483,31 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1029
1483
  break;
1030
1484
  case 'gte':
1031
1485
  case '!gte':
1032
- if (curValue[0] === 'cdate') {
1486
+ if (curValue[0] === 'cdate' ||
1487
+ curValue[0] === 'mdate' ||
1488
+ (this.nymph.tilmeld != null &&
1489
+ (curValue[0] === 'user' ||
1490
+ curValue[0] === 'group' ||
1491
+ curValue[0] === 'acUser' ||
1492
+ curValue[0] === 'acGroup' ||
1493
+ curValue[0] === 'acOther' ||
1494
+ curValue[0] === 'acRead' ||
1495
+ curValue[0] === 'acWrite' ||
1496
+ curValue[0] === 'acFull'))) {
1033
1497
  if (curQuery) {
1034
1498
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1035
1499
  }
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') {
1048
- if (curQuery) {
1049
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1050
- }
1051
- const mdate = `param${++count.i}`;
1500
+ const value = `param${++count.i}`;
1052
1501
  curQuery +=
1053
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1502
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1054
1503
  ieTable +
1055
- '."mdate">=@' +
1056
- mdate;
1057
- params[mdate] = isNaN(Number(curValue[1]))
1504
+ '.' +
1505
+ PostgreSQLDriver.escape(curValue[0]) +
1506
+ '>=@' +
1507
+ value;
1508
+ params[value] = isNaN(Number(curValue[1]))
1058
1509
  ? null
1059
1510
  : Number(curValue[1]);
1060
- break;
1061
1511
  }
1062
1512
  else {
1063
1513
  if (curQuery) {
@@ -1066,11 +1516,12 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1066
1516
  const name = `param${++count.i}`;
1067
1517
  const value = `param${++count.i}`;
1068
1518
  curQuery +=
1069
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1519
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1520
+ 'EXISTS (SELECT "guid" FROM ' +
1521
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1522
+ ' WHERE "guid"=' +
1070
1523
  ieTable +
1071
- '."guid" IN (SELECT "guid" FROM ' +
1072
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1073
- ' WHERE "name"=@' +
1524
+ '."guid" AND "name"=@' +
1074
1525
  name +
1075
1526
  ' AND "number">=@' +
1076
1527
  value +
@@ -1083,35 +1534,31 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1083
1534
  break;
1084
1535
  case 'lt':
1085
1536
  case '!lt':
1086
- if (curValue[0] === 'cdate') {
1087
- if (curQuery) {
1088
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1089
- }
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') {
1537
+ if (curValue[0] === 'cdate' ||
1538
+ curValue[0] === 'mdate' ||
1539
+ (this.nymph.tilmeld != null &&
1540
+ (curValue[0] === 'user' ||
1541
+ curValue[0] === 'group' ||
1542
+ curValue[0] === 'acUser' ||
1543
+ curValue[0] === 'acGroup' ||
1544
+ curValue[0] === 'acOther' ||
1545
+ curValue[0] === 'acRead' ||
1546
+ curValue[0] === 'acWrite' ||
1547
+ curValue[0] === 'acFull'))) {
1102
1548
  if (curQuery) {
1103
1549
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1104
1550
  }
1105
- const mdate = `param${++count.i}`;
1551
+ const value = `param${++count.i}`;
1106
1552
  curQuery +=
1107
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1553
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1108
1554
  ieTable +
1109
- '."mdate"<@' +
1110
- mdate;
1111
- params[mdate] = isNaN(Number(curValue[1]))
1555
+ '.' +
1556
+ PostgreSQLDriver.escape(curValue[0]) +
1557
+ '<@' +
1558
+ value;
1559
+ params[value] = isNaN(Number(curValue[1]))
1112
1560
  ? null
1113
1561
  : Number(curValue[1]);
1114
- break;
1115
1562
  }
1116
1563
  else {
1117
1564
  if (curQuery) {
@@ -1120,11 +1567,12 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1120
1567
  const name = `param${++count.i}`;
1121
1568
  const value = `param${++count.i}`;
1122
1569
  curQuery +=
1123
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1570
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1571
+ 'EXISTS (SELECT "guid" FROM ' +
1572
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1573
+ ' WHERE "guid"=' +
1124
1574
  ieTable +
1125
- '."guid" IN (SELECT "guid" FROM ' +
1126
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1127
- ' WHERE "name"=@' +
1575
+ '."guid" AND "name"=@' +
1128
1576
  name +
1129
1577
  ' AND "number"<@' +
1130
1578
  value +
@@ -1137,35 +1585,31 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1137
1585
  break;
1138
1586
  case 'lte':
1139
1587
  case '!lte':
1140
- if (curValue[0] === 'cdate') {
1588
+ if (curValue[0] === 'cdate' ||
1589
+ curValue[0] === 'mdate' ||
1590
+ (this.nymph.tilmeld != null &&
1591
+ (curValue[0] === 'user' ||
1592
+ curValue[0] === 'group' ||
1593
+ curValue[0] === 'acUser' ||
1594
+ curValue[0] === 'acGroup' ||
1595
+ curValue[0] === 'acOther' ||
1596
+ curValue[0] === 'acRead' ||
1597
+ curValue[0] === 'acWrite' ||
1598
+ curValue[0] === 'acFull'))) {
1141
1599
  if (curQuery) {
1142
1600
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1143
1601
  }
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}`;
1602
+ const value = `param${++count.i}`;
1160
1603
  curQuery +=
1161
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1604
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1162
1605
  ieTable +
1163
- '."mdate"<=@' +
1164
- mdate;
1165
- params[mdate] = isNaN(Number(curValue[1]))
1606
+ '.' +
1607
+ PostgreSQLDriver.escape(curValue[0]) +
1608
+ '<=@' +
1609
+ value;
1610
+ params[value] = isNaN(Number(curValue[1]))
1166
1611
  ? null
1167
1612
  : Number(curValue[1]);
1168
- break;
1169
1613
  }
1170
1614
  else {
1171
1615
  if (curQuery) {
@@ -1174,11 +1618,12 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1174
1618
  const name = `param${++count.i}`;
1175
1619
  const value = `param${++count.i}`;
1176
1620
  curQuery +=
1177
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1621
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1622
+ 'EXISTS (SELECT "guid" FROM ' +
1623
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1624
+ ' WHERE "guid"=' +
1178
1625
  ieTable +
1179
- '."guid" IN (SELECT "guid" FROM ' +
1180
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1181
- ' WHERE "name"=@' +
1626
+ '."guid" AND "name"=@' +
1182
1627
  name +
1183
1628
  ' AND "number"<=@' +
1184
1629
  value +
@@ -1204,71 +1649,190 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1204
1649
  if (curQuery) {
1205
1650
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1206
1651
  }
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;
1652
+ if (this.nymph.tilmeld != null &&
1653
+ (curValue[0] === 'user' || curValue[0] === 'group')) {
1654
+ const guid = `param${++count.i}`;
1655
+ curQuery +=
1656
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1657
+ ieTable +
1658
+ '.' +
1659
+ PostgreSQLDriver.escape(curValue[0]) +
1660
+ '=decode(@' +
1661
+ guid +
1662
+ ", 'hex')";
1663
+ params[guid] = curQguid;
1664
+ }
1665
+ else if (this.nymph.tilmeld != null &&
1666
+ (curValue[0] === 'acRead' ||
1667
+ curValue[0] === 'acWrite' ||
1668
+ curValue[0] === 'acFull')) {
1669
+ const guid = `param${++count.i}`;
1670
+ curQuery +=
1671
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1672
+ 'decode(@' +
1673
+ guid +
1674
+ ", 'hex')=ANY(" +
1675
+ ieTable +
1676
+ '.' +
1677
+ PostgreSQLDriver.escape(curValue[0]) +
1678
+ ')';
1679
+ params[guid] = curQguid;
1680
+ }
1681
+ else {
1682
+ const name = `param${++count.i}`;
1683
+ const guid = `param${++count.i}`;
1684
+ curQuery +=
1685
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1686
+ 'EXISTS (SELECT "guid" FROM ' +
1687
+ PostgreSQLDriver.escape(this.prefix + 'references_' + etype) +
1688
+ ' WHERE "guid"=' +
1689
+ ieTable +
1690
+ '."guid" AND "name"=@' +
1691
+ name +
1692
+ ' AND "reference"=decode(@' +
1693
+ guid +
1694
+ ", 'hex'))";
1695
+ params[name] = curValue[0];
1696
+ params[guid] = curQguid;
1697
+ }
1221
1698
  break;
1222
1699
  case 'selector':
1223
1700
  case '!selector':
1224
- const subquery = this.makeEntityQuery(options, [curValue], etype, count, params, true, tableSuffix, etypes);
1701
+ const innerquery = this.makeEntityQuery({ ...options, sort: null, limit: undefined }, [curValue], etype, count, params, true, tableSuffix, etypes);
1225
1702
  if (curQuery) {
1226
1703
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1227
1704
  }
1228
1705
  curQuery +=
1229
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1706
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1230
1707
  '(' +
1231
- subquery.query +
1708
+ innerquery.query +
1232
1709
  ')';
1233
1710
  break;
1234
1711
  case 'qref':
1235
1712
  case '!qref':
1713
+ const referenceTableSuffix = makeTableSuffix();
1236
1714
  const [qrefOptions, ...qrefSelectors] = curValue[1];
1237
1715
  const QrefEntityClass = qrefOptions.class;
1238
1716
  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
1717
  if (curQuery) {
1241
1718
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1242
1719
  }
1243
- const qrefName = `param${++count.i}`;
1244
- curQuery +=
1245
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1720
+ if (this.nymph.tilmeld != null &&
1721
+ (curValue[0] === 'user' || curValue[0] === 'group')) {
1722
+ const qrefQuery = this.makeEntityQuery({
1723
+ ...qrefOptions,
1724
+ sort: qrefOptions.sort ?? null,
1725
+ return: 'guid',
1726
+ class: QrefEntityClass,
1727
+ }, qrefSelectors, QrefEntityClass.ETYPE, count, params, false, makeTableSuffix(), etypes, 'r' +
1728
+ referenceTableSuffix +
1729
+ '.' +
1730
+ PostgreSQLDriver.escape(curValue[0]));
1731
+ curQuery +=
1732
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1733
+ 'EXISTS (SELECT "guid" FROM ' +
1734
+ PostgreSQLDriver.escape(this.prefix + 'entities_' + etype) +
1735
+ ' r' +
1736
+ referenceTableSuffix +
1737
+ ' WHERE r' +
1738
+ referenceTableSuffix +
1739
+ '."guid"=' +
1740
+ ieTable +
1741
+ '."guid" AND EXISTS (' +
1742
+ qrefQuery.query +
1743
+ '))';
1744
+ }
1745
+ else if (this.nymph.tilmeld != null &&
1746
+ (curValue[0] === 'acRead' ||
1747
+ curValue[0] === 'acWrite' ||
1748
+ curValue[0] === 'acFull')) {
1749
+ const qrefQuery = this.makeEntityQuery({
1750
+ ...qrefOptions,
1751
+ sort: qrefOptions.sort ?? null,
1752
+ return: 'guid',
1753
+ class: QrefEntityClass,
1754
+ }, qrefSelectors, QrefEntityClass.ETYPE, count, params, false, makeTableSuffix(), etypes, undefined, (guid) => guid +
1755
+ '=ANY(' +
1246
1756
  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];
1757
+ '.' +
1758
+ PostgreSQLDriver.escape(curValue[0]) +
1759
+ ')');
1760
+ curQuery +=
1761
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1762
+ 'EXISTS (' +
1763
+ qrefQuery.query +
1764
+ ')';
1765
+ }
1766
+ else {
1767
+ const qrefQuery = this.makeEntityQuery({
1768
+ ...qrefOptions,
1769
+ sort: qrefOptions.sort ?? null,
1770
+ return: 'guid',
1771
+ class: QrefEntityClass,
1772
+ }, qrefSelectors, QrefEntityClass.ETYPE, count, params, false, makeTableSuffix(), etypes, 'r' + referenceTableSuffix + '."reference"');
1773
+ const qrefName = `param${++count.i}`;
1774
+ curQuery +=
1775
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1776
+ 'EXISTS (SELECT "guid" FROM ' +
1777
+ PostgreSQLDriver.escape(this.prefix + 'references_' + etype) +
1778
+ ' r' +
1779
+ referenceTableSuffix +
1780
+ ' WHERE r' +
1781
+ referenceTableSuffix +
1782
+ '."guid"=' +
1783
+ ieTable +
1784
+ '."guid" AND r' +
1785
+ referenceTableSuffix +
1786
+ '."name"=@' +
1787
+ qrefName +
1788
+ ' AND EXISTS (' +
1789
+ qrefQuery.query +
1790
+ '))';
1791
+ params[qrefName] = curValue[0];
1792
+ }
1255
1793
  break;
1256
1794
  }
1257
1795
  }
1258
1796
  return curQuery;
1259
1797
  });
1260
1798
  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';
1799
+ let sortByInner;
1800
+ let sortJoin = '';
1801
+ let sortJoinInner = '';
1802
+ const order = options.reverse ? ' DESC' : '';
1803
+ if (sort == null) {
1804
+ sortBy = '';
1805
+ sortByInner = '';
1806
+ }
1807
+ else {
1808
+ switch (sort) {
1809
+ case 'mdate':
1810
+ sortBy = `ORDER BY ${eTable}."mdate"${order}`;
1811
+ sortByInner = `ORDER BY ${ieTable}."mdate"${order}`;
1812
+ break;
1813
+ case 'cdate':
1814
+ sortBy = `ORDER BY ${eTable}."cdate"${order}`;
1815
+ sortByInner = `ORDER BY ${ieTable}."cdate"${order}`;
1816
+ break;
1817
+ default:
1818
+ const name = `param${++count.i}`;
1819
+ sortJoin = `LEFT JOIN (
1820
+ SELECT "guid", "string", "number"
1821
+ FROM ${PostgreSQLDriver.escape(this.prefix + 'data_' + etype)}
1822
+ WHERE "name"=@${name}
1823
+ ORDER BY "number"${order}, "string"${order}
1824
+ ) ${sTable} ON ${eTable}."guid"=${sTable}."guid"`;
1825
+ sortJoinInner = `LEFT JOIN (
1826
+ SELECT "guid", "string", "number"
1827
+ FROM ${PostgreSQLDriver.escape(this.prefix + 'data_' + etype)}
1828
+ WHERE "name"=@${name}
1829
+ ORDER BY "number"${order}, "string"${order}
1830
+ ) ${sTable} ON ${ieTable}."guid"=${sTable}."guid"`;
1831
+ sortBy = `ORDER BY ${sTable}."number"${order}, ${sTable}."string"${order}`;
1832
+ sortByInner = sortBy;
1833
+ params[name] = sort;
1834
+ break;
1835
+ }
1272
1836
  }
1273
1837
  let query;
1274
1838
  if (queryParts.length) {
@@ -1285,18 +1849,23 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1285
1849
  offset = ` OFFSET ${Math.floor(isNaN(Number(options.offset)) ? 0 : Number(options.offset))}`;
1286
1850
  }
1287
1851
  const whereClause = queryParts.join(') AND (');
1852
+ const guidClause = guidExplicitSelector
1853
+ ? `${guidExplicitSelector(`${ieTable}."guid"`)} AND `
1854
+ : guidSelector
1855
+ ? `${ieTable}."guid"=${guidSelector} AND `
1856
+ : '';
1288
1857
  if (options.return === 'count') {
1289
1858
  if (limit || offset) {
1290
1859
  query = `SELECT COUNT(${countTable}."guid") AS "count" FROM (
1291
- SELECT COUNT(${ieTable}."guid") AS "guid"
1860
+ SELECT ${ieTable}."guid" AS "guid"
1292
1861
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}
1293
- WHERE (${whereClause})${limit}${offset}
1862
+ WHERE ${guidClause}(${whereClause})${limit}${offset}
1294
1863
  ) ${countTable}`;
1295
1864
  }
1296
1865
  else {
1297
1866
  query = `SELECT COUNT(${ieTable}."guid") AS "count"
1298
1867
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}
1299
- WHERE (${whereClause})`;
1868
+ WHERE ${guidClause}(${whereClause})`;
1300
1869
  }
1301
1870
  }
1302
1871
  else if (options.return === 'guid') {
@@ -1305,8 +1874,9 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1305
1874
  : `${ieTable}."guid"`;
1306
1875
  query = `SELECT ${guidColumn} AS "guid"
1307
1876
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}
1308
- WHERE (${whereClause})
1309
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}`;
1877
+ ${sortJoinInner}
1878
+ WHERE ${guidClause}(${whereClause})
1879
+ ${sortByInner ? sortByInner + ', ' : 'ORDER BY '}${ieTable}."guid"${limit}${offset}`;
1310
1880
  }
1311
1881
  else {
1312
1882
  query = `SELECT
@@ -1314,20 +1884,30 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1314
1884
  ${eTable}."tags",
1315
1885
  ${eTable}."cdate",
1316
1886
  ${eTable}."mdate",
1887
+ encode(${eTable}."user", 'hex') AS "user",
1888
+ encode(${eTable}."group", 'hex') AS "group",
1889
+ ${eTable}."acUser",
1890
+ ${eTable}."acGroup",
1891
+ ${eTable}."acOther",
1892
+ array(SELECT encode(n, 'hex') FROM unnest(${eTable}."acRead") AS n) as "acRead",
1893
+ array(SELECT encode(n, 'hex') FROM unnest(${eTable}."acWrite") AS n) as "acWrite",
1894
+ array(SELECT encode(n, 'hex') FROM unnest(${eTable}."acFull") AS n) as "acFull",
1317
1895
  ${dTable}."name",
1318
1896
  ${dTable}."value",
1319
- ${cTable}."string",
1320
- ${cTable}."number"
1897
+ ${dTable}."json",
1898
+ ${dTable}."string",
1899
+ ${dTable}."number"
1321
1900
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${eTable}
1322
1901
  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"
1902
+ ${sortJoin}
1324
1903
  INNER JOIN (
1325
1904
  SELECT ${ieTable}."guid"
1326
1905
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}
1327
- WHERE (${whereClause})
1328
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}
1906
+ ${sortJoinInner}
1907
+ WHERE ${guidClause}(${whereClause})
1908
+ ${sortByInner}${limit}${offset}
1329
1909
  ) ${fTable} ON ${eTable}."guid"=${fTable}."guid"
1330
- ORDER BY ${eTable}.${sortBy}`;
1910
+ ${sortBy ? sortBy + ', ' : 'ORDER BY '}${eTable}."guid"`;
1331
1911
  }
1332
1912
  }
1333
1913
  }
@@ -1344,16 +1924,21 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1344
1924
  if ('offset' in options) {
1345
1925
  offset = ` OFFSET ${Math.floor(isNaN(Number(options.offset)) ? 0 : Number(options.offset))}`;
1346
1926
  }
1927
+ const guidClause = guidExplicitSelector
1928
+ ? ` WHERE ${guidExplicitSelector(`${ieTable}."guid"`)}`
1929
+ : guidSelector
1930
+ ? ` WHERE ${ieTable}."guid"=${guidSelector}`
1931
+ : '';
1347
1932
  if (options.return === 'count') {
1348
1933
  if (limit || offset) {
1349
1934
  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}
1935
+ SELECT ${ieTable}."guid" AS "guid"
1936
+ FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}${guidClause}${limit}${offset}
1352
1937
  ) ${countTable}`;
1353
1938
  }
1354
1939
  else {
1355
1940
  query = `SELECT COUNT(${ieTable}."guid") AS "count"
1356
- FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}`;
1941
+ FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}${guidClause}`;
1357
1942
  }
1358
1943
  }
1359
1944
  else if (options.return === 'guid') {
@@ -1362,7 +1947,9 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1362
1947
  : `${ieTable}."guid"`;
1363
1948
  query = `SELECT ${guidColumn} AS "guid"
1364
1949
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}
1365
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}`;
1950
+ ${sortJoinInner}
1951
+ ${guidClause}
1952
+ ${sortByInner ? sortByInner + ', ' : 'ORDER BY '}${ieTable}."guid"${limit}${offset}`;
1366
1953
  }
1367
1954
  else {
1368
1955
  if (limit || offset) {
@@ -1371,19 +1958,30 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1371
1958
  ${eTable}."tags",
1372
1959
  ${eTable}."cdate",
1373
1960
  ${eTable}."mdate",
1961
+ encode(${eTable}."user", 'hex') AS "user",
1962
+ encode(${eTable}."group", 'hex') AS "group",
1963
+ ${eTable}."acUser",
1964
+ ${eTable}."acGroup",
1965
+ ${eTable}."acOther",
1966
+ array(SELECT encode(n, 'hex') FROM unnest(${eTable}."acRead") AS n) as "acRead",
1967
+ array(SELECT encode(n, 'hex') FROM unnest(${eTable}."acWrite") AS n) as "acWrite",
1968
+ array(SELECT encode(n, 'hex') FROM unnest(${eTable}."acFull") AS n) as "acFull",
1374
1969
  ${dTable}."name",
1375
1970
  ${dTable}."value",
1376
- ${cTable}."string",
1377
- ${cTable}."number"
1971
+ ${dTable}."json",
1972
+ ${dTable}."string",
1973
+ ${dTable}."number"
1378
1974
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${eTable}
1379
1975
  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"
1976
+ ${sortJoin}
1381
1977
  INNER JOIN (
1382
1978
  SELECT ${ieTable}."guid"
1383
1979
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}
1384
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}
1980
+ ${sortJoinInner}
1981
+ ${guidClause}
1982
+ ${sortByInner}${limit}${offset}
1385
1983
  ) ${fTable} ON ${eTable}."guid"=${fTable}."guid"
1386
- ORDER BY ${eTable}.${sortBy}`;
1984
+ ${sortBy ? sortBy + ', ' : 'ORDER BY '}${eTable}."guid"`;
1387
1985
  }
1388
1986
  else {
1389
1987
  query = `SELECT
@@ -1391,14 +1989,24 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1391
1989
  ${eTable}."tags",
1392
1990
  ${eTable}."cdate",
1393
1991
  ${eTable}."mdate",
1992
+ encode(${eTable}."user", 'hex') AS "user",
1993
+ encode(${eTable}."group", 'hex') AS "group",
1994
+ ${eTable}."acUser",
1995
+ ${eTable}."acGroup",
1996
+ ${eTable}."acOther",
1997
+ array(SELECT encode(n, 'hex') FROM unnest(${eTable}."acRead") AS n) as "acRead",
1998
+ array(SELECT encode(n, 'hex') FROM unnest(${eTable}."acWrite") AS n) as "acWrite",
1999
+ array(SELECT encode(n, 'hex') FROM unnest(${eTable}."acFull") AS n) as "acFull",
1394
2000
  ${dTable}."name",
1395
2001
  ${dTable}."value",
1396
- ${cTable}."string",
1397
- ${cTable}."number"
2002
+ ${dTable}."json",
2003
+ ${dTable}."string",
2004
+ ${dTable}."number"
1398
2005
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${eTable}
1399
2006
  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}`;
2007
+ ${sortJoin}
2008
+ ${guidExplicitSelector ? `WHERE ${guidExplicitSelector(`${eTable}."guid"`)}` : guidSelector ? `WHERE ${eTable}."guid"=${guidSelector}` : ''}
2009
+ ${sortBy ? sortBy + ', ' : 'ORDER BY '}${eTable}."guid"`;
1402
2010
  }
1403
2011
  }
1404
2012
  }
@@ -1414,33 +2022,38 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1414
2022
  }
1415
2023
  performQuery(options, formattedSelectors, etype) {
1416
2024
  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]();
2025
+ const result = this.queryArray(query, { etypes, params }).then((val) => val[Symbol.iterator]());
1425
2026
  return {
1426
2027
  result,
1427
2028
  };
1428
2029
  }
1429
2030
  async getEntities(options = {}, ...selectors) {
1430
- const { result: resultPromise, process } = this.getEntitesRowLike(options, selectors, (options, formattedSelectors, etype) => this.performQuery(options, formattedSelectors, etype), () => {
2031
+ const { result: resultPromise, process } = this.getEntitiesRowLike(
2032
+ // @ts-ignore: options is correct here.
2033
+ options, selectors, ({ options, selectors, etype }) => this.performQuery(options, selectors, etype), () => {
1431
2034
  const next = result.next();
1432
2035
  return next.done ? null : next.value;
1433
2036
  }, () => 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),
2037
+ tags: row.tags.filter((tag) => tag),
2038
+ cdate: isNaN(Number(row.cdate)) ? Date.now() : Number(row.cdate),
2039
+ mdate: isNaN(Number(row.mdate)) ? Date.now() : Number(row.mdate),
2040
+ user: row.user,
2041
+ group: row.group,
2042
+ acUser: row.acUser,
2043
+ acGroup: row.acGroup,
2044
+ acOther: row.acOther,
2045
+ acRead: row.acRead?.filter((guid) => guid) ?? [],
2046
+ acWrite: row.acWrite?.filter((guid) => guid) ?? [],
2047
+ acFull: row.acFull?.filter((guid) => guid) ?? [],
1437
2048
  }), (row) => ({
1438
2049
  name: row.name,
1439
2050
  svalue: row.value === 'N'
1440
2051
  ? JSON.stringify(Number(row.number))
1441
2052
  : row.value === 'S'
1442
- ? JSON.stringify(row.string)
1443
- : row.value,
2053
+ ? JSON.stringify(PostgreSQLDriver.unescapeNulls(row.string))
2054
+ : row.value === 'J'
2055
+ ? PostgreSQLDriver.unescapeNullSequences(JSON.stringify(row.json))
2056
+ : row.value,
1444
2057
  }));
1445
2058
  const result = await resultPromise;
1446
2059
  const value = process();
@@ -1449,31 +2062,9 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1449
2062
  }
1450
2063
  return value;
1451
2064
  }
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
2065
  async getUID(name) {
1475
2066
  if (name == null) {
1476
- throw new nymph_1.InvalidParametersError('Name not given for UID.');
2067
+ throw new InvalidParametersError('Name not given for UID.');
1477
2068
  }
1478
2069
  const result = await this.queryGet(`SELECT "cur_uid" FROM ${PostgreSQLDriver.escape(`${this.prefix}uids`)} WHERE "name"=@name;`, {
1479
2070
  params: {
@@ -1482,50 +2073,75 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1482
2073
  });
1483
2074
  return result?.cur_uid == null ? null : Number(result.cur_uid);
1484
2075
  }
1485
- async import(filename) {
2076
+ async importEntity(entity) {
2077
+ return await this.importEntityInternal(entity);
2078
+ }
2079
+ async importEntityTokens(entity) {
2080
+ return await this.importEntityInternal(entity, { only: 'tokens' });
2081
+ }
2082
+ async importEntityTilmeldAC(entity) {
2083
+ return await this.importEntityInternal(entity, { only: 'tilmeldAC' });
2084
+ }
2085
+ async importEntityInternal({ guid, cdate, mdate, tags, sdata, etype, }, { only = undefined } = {}) {
1486
2086
  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');`, {
2087
+ let promises = [];
2088
+ if (only == null) {
2089
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1489
2090
  etypes: [etype],
1490
2091
  params: {
1491
2092
  guid,
1492
2093
  },
1493
- });
1494
- await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid", "tags", "cdate", "mdate") VALUES (decode(@guid, 'hex'), @tags, @cdate, @mdate);`, {
2094
+ }));
2095
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1495
2096
  etypes: [etype],
1496
2097
  params: {
1497
2098
  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
2099
  },
1506
- });
1507
- const promises = [];
1508
- promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
2100
+ }));
2101
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1509
2102
  etypes: [etype],
1510
2103
  params: {
1511
2104
  guid,
1512
2105
  },
1513
2106
  }));
1514
- promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
2107
+ }
2108
+ if (only == null || only === 'tokens') {
2109
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1515
2110
  etypes: [etype],
1516
2111
  params: {
1517
2112
  guid,
1518
2113
  },
1519
2114
  }));
1520
- promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
2115
+ }
2116
+ if (only == null) {
2117
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1521
2118
  etypes: [etype],
1522
2119
  params: {
1523
2120
  guid,
1524
2121
  },
1525
2122
  }));
1526
- await Promise.all(promises);
1527
- delete sdata.cdate;
1528
- delete sdata.mdate;
2123
+ }
2124
+ await Promise.all(promises);
2125
+ promises = [];
2126
+ if (only == null) {
2127
+ let { user, group, acUser, acGroup, acOther, acRead, acWrite, acFull } = this.removeAndReturnACValues(etype, {}, sdata);
2128
+ 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)"});`, {
2129
+ etypes: [etype],
2130
+ params: {
2131
+ guid,
2132
+ tags,
2133
+ cdate: isNaN(cdate) ? null : cdate,
2134
+ mdate: isNaN(mdate) ? null : mdate,
2135
+ user,
2136
+ group,
2137
+ acUser,
2138
+ acGroup,
2139
+ acOther,
2140
+ acRead,
2141
+ acWrite,
2142
+ acFull,
2143
+ },
2144
+ });
1529
2145
  for (const name in sdata) {
1530
2146
  const value = sdata[name];
1531
2147
  const uvalue = JSON.parse(value);
@@ -1536,24 +2152,22 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1536
2152
  ? 'N'
1537
2153
  : typeof uvalue === 'string'
1538
2154
  ? '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);`, {
2155
+ : 'J';
2156
+ const jsonValue = storageValue === 'J'
2157
+ ? PostgreSQLDriver.escapeNullSequences(value)
2158
+ : null;
2159
+ 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
2160
  etypes: [etype],
1543
2161
  params: {
1544
2162
  guid,
1545
2163
  name,
1546
2164
  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}`,
2165
+ jsonValue,
2166
+ string: storageValue === 'J'
2167
+ ? null
2168
+ : PostgreSQLDriver.escapeNulls(`${uvalue}`),
1556
2169
  number: isNaN(Number(uvalue)) ? null : Number(uvalue),
2170
+ truthy: !!uvalue,
1557
2171
  },
1558
2172
  }));
1559
2173
  const references = this.findReferences(value);
@@ -1568,43 +2182,130 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1568
2182
  }));
1569
2183
  }
1570
2184
  }
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);`, {
2185
+ }
2186
+ if (only === 'tilmeldAC') {
2187
+ let { user, group, acUser, acGroup, acOther, acRead, acWrite, acFull } = this.removeAndReturnACValues(etype, {}, sdata);
2188
+ 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');`, {
2189
+ etypes: [etype],
1579
2190
  params: {
1580
- name,
1581
- curUid,
2191
+ user,
2192
+ group,
2193
+ acUser,
2194
+ acGroup,
2195
+ acOther,
2196
+ acRead,
2197
+ acWrite,
2198
+ acFull,
2199
+ guid,
1582
2200
  },
2201
+ }));
2202
+ }
2203
+ const EntityClass = this.nymph.getEntityClassByEtype(etype);
2204
+ if (only == null || only === 'tokens') {
2205
+ for (let name in sdata) {
2206
+ let tokenString = null;
2207
+ try {
2208
+ tokenString = EntityClass.getFTSText(name, JSON.parse(sdata[name]));
2209
+ }
2210
+ catch (e) {
2211
+ // Ignore error.
2212
+ }
2213
+ if (tokenString != null) {
2214
+ const tokens = this.tokenizer.tokenize(tokenString);
2215
+ while (tokens.length) {
2216
+ const currentTokens = tokens.splice(0, 100);
2217
+ const params = {
2218
+ guid,
2219
+ name,
2220
+ };
2221
+ const values = [];
2222
+ for (let i = 0; i < currentTokens.length; i++) {
2223
+ const token = currentTokens[i];
2224
+ params['token' + i] = token.token;
2225
+ params['position' + i] = token.position;
2226
+ params['stem' + i] = token.stem;
2227
+ values.push("(decode(@guid, 'hex'), @name, @token" +
2228
+ i +
2229
+ ', @position' +
2230
+ i +
2231
+ ', @stem' +
2232
+ i +
2233
+ ')');
2234
+ }
2235
+ promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} ("guid", "name", "token", "position", "stem") VALUES ${values.join(', ')};`, {
2236
+ etypes: [etype],
2237
+ params,
2238
+ }));
2239
+ }
2240
+ }
2241
+ }
2242
+ }
2243
+ if (only == null) {
2244
+ const uniques = await EntityClass.getUniques({
2245
+ guid,
2246
+ cdate,
2247
+ mdate,
2248
+ tags,
2249
+ data: {},
2250
+ sdata,
1583
2251
  });
1584
- }, async () => {
1585
- await this.internalTransaction('nymph-import');
1586
- }, async () => {
1587
- await this.commit('nymph-import');
2252
+ for (const unique of uniques) {
2253
+ promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} ("guid", "unique") VALUES (decode(@guid, 'hex'), @unique);`, {
2254
+ etypes: [etype],
2255
+ params: {
2256
+ guid,
2257
+ unique,
2258
+ },
2259
+ }).catch((e) => {
2260
+ if (e instanceof EntityUniqueConstraintError) {
2261
+ this.nymph.config.debugError('postgresql', `Import entity unique constraint violation for GUID "${guid}" on etype "${etype}": "${unique}"`);
2262
+ }
2263
+ return e;
2264
+ }));
2265
+ }
2266
+ }
2267
+ await Promise.all(promises);
2268
+ }
2269
+ catch (e) {
2270
+ this.nymph.config.debugError('postgresql', `Import entity error: "${e}"`);
2271
+ throw e;
2272
+ }
2273
+ }
2274
+ async importUID({ name, value }) {
2275
+ try {
2276
+ await this.internalTransaction(`nymph-import-uid-${name}`);
2277
+ await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}uids`)} WHERE "name"=@name;`, {
2278
+ params: {
2279
+ name,
2280
+ },
1588
2281
  });
1589
- return result;
2282
+ await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}uids`)} ("name", "cur_uid") VALUES (@name, @value);`, {
2283
+ params: {
2284
+ name,
2285
+ value,
2286
+ },
2287
+ });
2288
+ await this.commit(`nymph-import-uid-${name}`);
1590
2289
  }
1591
2290
  catch (e) {
1592
- await this.rollback('nymph-import');
1593
- return false;
2291
+ this.nymph.config.debugError('postgresql', `Import UID error: "${e}"`);
2292
+ await this.rollback(`nymph-import-uid-${name}`);
2293
+ throw e;
1594
2294
  }
1595
2295
  }
1596
2296
  async newUID(name) {
1597
2297
  if (name == null) {
1598
- throw new nymph_1.InvalidParametersError('Name not given for UID.');
2298
+ throw new InvalidParametersError('Name not given for UID.');
1599
2299
  }
1600
2300
  await this.internalTransaction('nymph-newuid');
2301
+ let curUid = undefined;
1601
2302
  try {
1602
2303
  const lock = await this.queryGet(`SELECT "cur_uid" FROM ${PostgreSQLDriver.escape(`${this.prefix}uids`)} WHERE "name"=@name FOR UPDATE;`, {
1603
2304
  params: {
1604
2305
  name,
1605
2306
  },
1606
2307
  });
1607
- let curUid = lock?.cur_uid == null ? undefined : Number(lock.cur_uid);
2308
+ curUid = lock?.cur_uid == null ? undefined : Number(lock.cur_uid);
1608
2309
  if (curUid == null) {
1609
2310
  curUid = 1;
1610
2311
  await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}uids`)} ("name", "cur_uid") VALUES (@name, @curUid);`, {
@@ -1623,17 +2324,18 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1623
2324
  },
1624
2325
  });
1625
2326
  }
1626
- await this.commit('nymph-newuid');
1627
- return curUid;
1628
2327
  }
1629
2328
  catch (e) {
2329
+ this.nymph.config.debugError('postgresql', `New UID error: "${e}"`);
1630
2330
  await this.rollback('nymph-newuid');
1631
2331
  throw e;
1632
2332
  }
2333
+ await this.commit('nymph-newuid');
2334
+ return curUid;
1633
2335
  }
1634
2336
  async renameUID(oldName, newName) {
1635
2337
  if (oldName == null || newName == null) {
1636
- throw new nymph_1.InvalidParametersError('Name not given for UID.');
2338
+ throw new InvalidParametersError('Name not given for UID.');
1637
2339
  }
1638
2340
  await this.queryRun(`UPDATE ${PostgreSQLDriver.escape(`${this.prefix}uids`)} SET "name"=@newName WHERE "name"=@oldName;`, {
1639
2341
  params: {
@@ -1645,7 +2347,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1645
2347
  }
1646
2348
  async rollback(name) {
1647
2349
  if (name == null || typeof name !== 'string' || name.length === 0) {
1648
- throw new nymph_1.InvalidParametersError('Transaction rollback attempted without a name.');
2350
+ throw new InvalidParametersError('Transaction rollback attempted without a name.');
1649
2351
  }
1650
2352
  if (!this.transaction || this.transaction.count === 0) {
1651
2353
  this.transaction = null;
@@ -1662,7 +2364,8 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1662
2364
  return true;
1663
2365
  }
1664
2366
  async saveEntity(entity) {
1665
- const insertData = async (guid, data, sdata, etype) => {
2367
+ const insertData = async (guid, data, sdata, uniques, etype) => {
2368
+ const EntityClass = this.nymph.getEntityClassByEtype(etype);
1666
2369
  const runInsertQuery = async (name, value, svalue) => {
1667
2370
  if (value === undefined) {
1668
2371
  return;
@@ -1671,24 +2374,23 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1671
2374
  ? 'N'
1672
2375
  : typeof value === 'string'
1673
2376
  ? 'S'
1674
- : svalue;
2377
+ : 'J';
2378
+ const jsonValue = storageValue === 'J'
2379
+ ? PostgreSQLDriver.escapeNullSequences(svalue)
2380
+ : null;
1675
2381
  const promises = [];
1676
- promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} ("guid", "name", "value") VALUES (decode(@guid, 'hex'), @name, @storageValue);`, {
2382
+ 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
2383
  etypes: [etype],
1678
2384
  params: {
1679
2385
  guid,
1680
2386
  name,
1681
2387
  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}`,
2388
+ jsonValue,
2389
+ string: storageValue === 'J'
2390
+ ? null
2391
+ : PostgreSQLDriver.escapeNulls(`${value}`),
1691
2392
  number: isNaN(Number(value)) ? null : Number(value),
2393
+ truthy: !!value,
1692
2394
  },
1693
2395
  }));
1694
2396
  const references = this.findReferences(svalue);
@@ -1702,8 +2404,60 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1702
2404
  },
1703
2405
  }));
1704
2406
  }
2407
+ let tokenString = null;
2408
+ try {
2409
+ tokenString = EntityClass.getFTSText(name, value);
2410
+ }
2411
+ catch (e) {
2412
+ // Ignore error.
2413
+ }
2414
+ if (tokenString != null) {
2415
+ const tokens = this.tokenizer.tokenize(tokenString);
2416
+ while (tokens.length) {
2417
+ const currentTokens = tokens.splice(0, 100);
2418
+ const params = {
2419
+ guid,
2420
+ name,
2421
+ };
2422
+ const values = [];
2423
+ for (let i = 0; i < currentTokens.length; i++) {
2424
+ const token = currentTokens[i];
2425
+ params['token' + i] = token.token;
2426
+ params['position' + i] = token.position;
2427
+ params['stem' + i] = token.stem;
2428
+ values.push("(decode(@guid, 'hex'), @name, @token" +
2429
+ i +
2430
+ ', @position' +
2431
+ i +
2432
+ ', @stem' +
2433
+ i +
2434
+ ')');
2435
+ }
2436
+ promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} ("guid", "name", "token", "position", "stem") VALUES ${values.join(', ')};`, {
2437
+ etypes: [etype],
2438
+ params,
2439
+ }));
2440
+ }
2441
+ }
1705
2442
  await Promise.all(promises);
1706
2443
  };
2444
+ for (const unique of uniques) {
2445
+ try {
2446
+ await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} ("guid", "unique") VALUES (decode(@guid, 'hex'), @unique);`, {
2447
+ etypes: [etype],
2448
+ params: {
2449
+ guid,
2450
+ unique,
2451
+ },
2452
+ });
2453
+ }
2454
+ catch (e) {
2455
+ if (e instanceof EntityUniqueConstraintError) {
2456
+ this.nymph.config.debugError('postgresql', `Save entity unique constraint violation for GUID "${guid}" on etype "${etype}": "${unique}"`);
2457
+ }
2458
+ throw e;
2459
+ }
2460
+ }
1707
2461
  for (const name in data) {
1708
2462
  await runInsertQuery(name, data[name], JSON.stringify(data[name]));
1709
2463
  }
@@ -1711,19 +2465,38 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1711
2465
  await runInsertQuery(name, JSON.parse(sdata[name]), sdata[name]);
1712
2466
  }
1713
2467
  };
2468
+ let inTransaction = false;
1714
2469
  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);`, {
2470
+ const result = await this.saveEntityRowLike(entity, async ({ guid, tags, data, sdata, uniques, cdate, etype }) => {
2471
+ if (Object.keys(data).length === 0 &&
2472
+ Object.keys(sdata).length === 0) {
2473
+ return false;
2474
+ }
2475
+ let { user, group, acUser, acGroup, acOther, acRead, acWrite, acFull, } = this.removeAndReturnACValues(etype, data, sdata);
2476
+ 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
2477
  etypes: [etype],
1718
2478
  params: {
1719
2479
  guid,
1720
2480
  tags,
1721
2481
  cdate,
2482
+ user,
2483
+ group,
2484
+ acUser,
2485
+ acGroup,
2486
+ acOther,
2487
+ acRead,
2488
+ acWrite,
2489
+ acFull,
1722
2490
  },
1723
2491
  });
1724
- await insertData(guid, data, sdata, etype);
2492
+ await insertData(guid, data, sdata, uniques, etype);
1725
2493
  return true;
1726
- }, async (entity, guid, tags, data, sdata, mdate, etype) => {
2494
+ }, async ({ entity, guid, tags, data, sdata, uniques, mdate, etype }) => {
2495
+ if (Object.keys(data).length === 0 &&
2496
+ Object.keys(sdata).length === 0) {
2497
+ return false;
2498
+ }
2499
+ let { user, group, acUser, acGroup, acOther, acRead, acWrite, acFull, } = this.removeAndReturnACValues(etype, data, sdata);
1727
2500
  const promises = [];
1728
2501
  promises.push(this.queryRun(`SELECT 1 FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`, {
1729
2502
  etypes: [etype],
@@ -1737,24 +2510,38 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1737
2510
  guid,
1738
2511
  },
1739
2512
  }));
1740
- promises.push(this.queryRun(`SELECT 1 FROM ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`, {
2513
+ promises.push(this.queryRun(`SELECT 1 FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`, {
1741
2514
  etypes: [etype],
1742
2515
  params: {
1743
2516
  guid,
1744
2517
  },
1745
2518
  }));
1746
- promises.push(this.queryRun(`SELECT 1 FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`, {
2519
+ promises.push(this.queryRun(`SELECT 1 FROM ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`, {
2520
+ etypes: [etype],
2521
+ params: {
2522
+ guid,
2523
+ },
2524
+ }));
2525
+ promises.push(this.queryRun(`SELECT 1 FROM ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`, {
1747
2526
  etypes: [etype],
1748
2527
  params: {
1749
2528
  guid,
1750
2529
  },
1751
2530
  }));
1752
2531
  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;`, {
2532
+ 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
2533
  etypes: [etype],
1755
2534
  params: {
1756
2535
  tags,
1757
2536
  mdate,
2537
+ user,
2538
+ group,
2539
+ acUser,
2540
+ acGroup,
2541
+ acOther,
2542
+ acRead,
2543
+ acWrite,
2544
+ acFull,
1758
2545
  guid,
1759
2546
  emdate: isNaN(Number(entity.mdate)) ? 0 : Number(entity.mdate),
1760
2547
  },
@@ -1768,44 +2555,57 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1768
2555
  guid,
1769
2556
  },
1770
2557
  }));
1771
- promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
2558
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1772
2559
  etypes: [etype],
1773
2560
  params: {
1774
2561
  guid,
1775
2562
  },
1776
2563
  }));
1777
- promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
2564
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
2565
+ etypes: [etype],
2566
+ params: {
2567
+ guid,
2568
+ },
2569
+ }));
2570
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1778
2571
  etypes: [etype],
1779
2572
  params: {
1780
2573
  guid,
1781
2574
  },
1782
2575
  }));
1783
2576
  await Promise.all(promises);
1784
- await insertData(guid, data, sdata, etype);
2577
+ await insertData(guid, data, sdata, uniques, etype);
1785
2578
  success = true;
1786
2579
  }
1787
2580
  return success;
1788
2581
  }, async () => {
1789
2582
  await this.internalTransaction('nymph-save');
2583
+ inTransaction = true;
1790
2584
  }, async (success) => {
1791
- if (success) {
1792
- await this.commit('nymph-save');
1793
- }
1794
- else {
1795
- await this.rollback('nymph-save');
2585
+ if (inTransaction) {
2586
+ inTransaction = false;
2587
+ if (success) {
2588
+ await this.commit('nymph-save');
2589
+ }
2590
+ else {
2591
+ await this.rollback('nymph-save');
2592
+ }
1796
2593
  }
1797
2594
  return success;
1798
2595
  });
1799
2596
  return result;
1800
2597
  }
1801
2598
  catch (e) {
1802
- await this.rollback('nymph-save');
2599
+ this.nymph.config.debugError('postgresql', `Save entity error: "${e}"`);
2600
+ if (inTransaction) {
2601
+ await this.rollback('nymph-save');
2602
+ }
1803
2603
  throw e;
1804
2604
  }
1805
2605
  }
1806
2606
  async setUID(name, curUid) {
1807
2607
  if (name == null) {
1808
- throw new nymph_1.InvalidParametersError('Name not given for UID.');
2608
+ throw new InvalidParametersError('Name not given for UID.');
1809
2609
  }
1810
2610
  await this.internalTransaction('nymph-setuid');
1811
2611
  try {
@@ -1821,23 +2621,25 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1821
2621
  curUid,
1822
2622
  },
1823
2623
  });
1824
- await this.commit('nymph-setuid');
1825
- return true;
1826
2624
  }
1827
2625
  catch (e) {
1828
2626
  await this.rollback('nymph-setuid');
1829
2627
  throw e;
1830
2628
  }
2629
+ await this.commit('nymph-setuid');
2630
+ return true;
1831
2631
  }
1832
2632
  async internalTransaction(name) {
1833
2633
  if (name == null || typeof name !== 'string' || name.length === 0) {
1834
- throw new nymph_1.InvalidParametersError('Transaction start attempted without a name.');
2634
+ throw new InvalidParametersError('Transaction start attempted without a name.');
1835
2635
  }
1836
2636
  if (!this.transaction || this.transaction.count === 0) {
2637
+ // Lock to one connection.
1837
2638
  this.transaction = {
1838
2639
  count: 0,
1839
2640
  connection: await this.getConnection(),
1840
2641
  };
2642
+ // We're not in a transaction yet, so start one.
1841
2643
  await this.queryRun('BEGIN;');
1842
2644
  }
1843
2645
  await this.queryRun(`SAVEPOINT ${PostgreSQLDriver.escape(name)};`);
@@ -1845,7 +2647,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1845
2647
  return this.transaction;
1846
2648
  }
1847
2649
  async startTransaction(name) {
1848
- const inTransaction = this.inTransaction();
2650
+ const inTransaction = await this.inTransaction();
1849
2651
  const transaction = await this.internalTransaction(name);
1850
2652
  if (!inTransaction) {
1851
2653
  this.transaction = null;
@@ -1854,6 +2656,121 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1854
2656
  nymph.driver.transaction = transaction;
1855
2657
  return nymph;
1856
2658
  }
2659
+ async removeTilmeldOldRows(etype, connection) {
2660
+ await this.internalTransaction('nymph-remove-tilmeld-rows');
2661
+ try {
2662
+ for (let name of [
2663
+ 'user',
2664
+ 'group',
2665
+ 'acUser',
2666
+ 'acGroup',
2667
+ 'acOther',
2668
+ 'acRead',
2669
+ 'acWrite',
2670
+ 'acFull',
2671
+ ]) {
2672
+ await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} WHERE "name"=@name;`, {
2673
+ etypes: [etype],
2674
+ params: {
2675
+ name,
2676
+ },
2677
+ connection,
2678
+ });
2679
+ await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "name"=@name;`, {
2680
+ etypes: [etype],
2681
+ params: {
2682
+ name,
2683
+ },
2684
+ connection,
2685
+ });
2686
+ await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} WHERE "name"=@name;`, {
2687
+ etypes: [etype],
2688
+ params: {
2689
+ name,
2690
+ },
2691
+ connection,
2692
+ });
2693
+ }
2694
+ }
2695
+ catch (e) {
2696
+ this.nymph.config.debugError('postgresql', `Remove tilmeld rows error: "${e}"`);
2697
+ await this.rollback('nymph-remove-tilmeld-rows');
2698
+ throw e;
2699
+ }
2700
+ await this.commit('nymph-remove-tilmeld-rows');
2701
+ return true;
2702
+ }
2703
+ async needsMigration() {
2704
+ 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;', {
2705
+ params: {
2706
+ db: this.config.database,
2707
+ prefix: this.prefix + 'data_' + '%',
2708
+ },
2709
+ });
2710
+ if (table?.table_name) {
2711
+ 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\';', {
2712
+ params: {
2713
+ db: this.config.database,
2714
+ table: table.table_name,
2715
+ },
2716
+ });
2717
+ if (!result?.exists) {
2718
+ return 'json';
2719
+ }
2720
+ }
2721
+ 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;', {
2722
+ params: {
2723
+ db: this.config.database,
2724
+ tokenTable: this.prefix + 'tokens_' + '%',
2725
+ },
2726
+ });
2727
+ if (!table2 || !table2.table_name) {
2728
+ return 'tokens';
2729
+ }
2730
+ 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;', {
2731
+ params: {
2732
+ db: this.config.database,
2733
+ prefix: this.prefix + 'entities_' + '%',
2734
+ },
2735
+ });
2736
+ if (table3?.table_name) {
2737
+ 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\';', {
2738
+ params: {
2739
+ db: this.config.database,
2740
+ table: table3.table_name,
2741
+ },
2742
+ });
2743
+ if (!result?.exists) {
2744
+ return 'tilmeldColumns';
2745
+ }
2746
+ }
2747
+ return false;
2748
+ }
2749
+ async liveMigration(migrationType) {
2750
+ if (migrationType === 'tokenTables') {
2751
+ const etypes = await this.getEtypes();
2752
+ const connection = await this.getConnection(true);
2753
+ for (let etype of etypes) {
2754
+ await this.createTokensTable(etype, connection);
2755
+ }
2756
+ connection.done();
2757
+ }
2758
+ else if (migrationType === 'tilmeldColumns') {
2759
+ const etypes = await this.getEtypes();
2760
+ const connection = await this.getConnection(true);
2761
+ for (let etype of etypes) {
2762
+ await this.addTilmeldColumnsAndIndexes(etype, connection);
2763
+ }
2764
+ connection.done();
2765
+ }
2766
+ else if (migrationType === 'tilmeldRemoveOldRows') {
2767
+ const etypes = await this.getEtypes();
2768
+ const connection = await this.getConnection(true);
2769
+ for (let etype of etypes) {
2770
+ await this.removeTilmeldOldRows(etype, connection);
2771
+ }
2772
+ connection.done();
2773
+ }
2774
+ }
1857
2775
  }
1858
- exports.default = PostgreSQLDriver;
1859
2776
  //# sourceMappingURL=PostgreSQLDriver.js.map