@nymphjs/driver-postgresql 1.0.0-beta.13 → 1.0.0-beta.130

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