@nsshunt/stsfhirpg 1.2.8 → 1.2.10
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.
- package/dist/stsfhirpg.cjs +654 -2
- package/dist/stsfhirpg.cjs.map +1 -1
- package/dist/stsfhirpg.mjs +655 -3
- package/dist/stsfhirpg.mjs.map +1 -1
- package/package.json +1 -1
- package/types/fhir-database/dbsearchindex.d.ts +1 -0
- package/types/fhir-database/dbsearchindex.d.ts.map +1 -1
- package/types/fhir-utils/resourceHelper.d.ts +2 -0
- package/types/fhir-utils/resourceHelper.d.ts.map +1 -1
- package/types/searchParameterManager.d.ts +0 -19
- package/types/searchParameterManager.d.ts.map +1 -1
package/dist/stsfhirpg.cjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const fs = require("node:fs");
|
|
4
|
+
const require$$0 = require("crypto");
|
|
4
5
|
const stsutils = require("@nsshunt/stsutils");
|
|
5
|
-
const stsfhirclient = require("@nsshunt/stsfhirclient");
|
|
6
6
|
const stsconfig = require("@nsshunt/stsconfig");
|
|
7
|
+
const redis = require("redis");
|
|
8
|
+
const stsfhirclient = require("@nsshunt/stsfhirclient");
|
|
7
9
|
const cluster = require("cluster");
|
|
8
|
-
const require$$0 = require("crypto");
|
|
9
10
|
const require$$0$3 = require("events");
|
|
10
11
|
const require$$1$1 = require("util");
|
|
11
12
|
const require$$0$2 = require("dns");
|
|
@@ -6995,6 +6996,635 @@ function hashReferenceParam(reference) {
|
|
|
6995
6996
|
function hashUriParam(uri, lowercase = false) {
|
|
6996
6997
|
return murmur64Signed(lowercase ? uri.toLowerCase() : uri);
|
|
6997
6998
|
}
|
|
6999
|
+
const RELEASE_SCRIPT = `
|
|
7000
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
7001
|
+
return redis.call("del", KEYS[1])
|
|
7002
|
+
else
|
|
7003
|
+
return 0
|
|
7004
|
+
end
|
|
7005
|
+
`;
|
|
7006
|
+
const RENEW_SCRIPT = `
|
|
7007
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
7008
|
+
return redis.call("pexpire", KEYS[1], ARGV[2])
|
|
7009
|
+
else
|
|
7010
|
+
return 0
|
|
7011
|
+
end
|
|
7012
|
+
`;
|
|
7013
|
+
class RedisDistributedLock {
|
|
7014
|
+
client;
|
|
7015
|
+
ttlMs;
|
|
7016
|
+
heartbeatMs;
|
|
7017
|
+
keyPrefix;
|
|
7018
|
+
constructor(client2, options) {
|
|
7019
|
+
this.client = client2;
|
|
7020
|
+
this.ttlMs = options?.ttlMs ?? 3e4;
|
|
7021
|
+
this.heartbeatMs = options?.heartbeatMs ?? Math.floor(this.ttlMs / 3);
|
|
7022
|
+
this.keyPrefix = options?.keyPrefix ?? "lock:";
|
|
7023
|
+
if (this.ttlMs <= 0) {
|
|
7024
|
+
throw new Error("ttlMs must be > 0");
|
|
7025
|
+
}
|
|
7026
|
+
if (this.heartbeatMs <= 0) {
|
|
7027
|
+
throw new Error("heartbeatMs must be > 0");
|
|
7028
|
+
}
|
|
7029
|
+
if (this.heartbeatMs >= this.ttlMs) {
|
|
7030
|
+
throw new Error("heartbeatMs should be less than ttlMs");
|
|
7031
|
+
}
|
|
7032
|
+
}
|
|
7033
|
+
buildKey(name) {
|
|
7034
|
+
return `${this.keyPrefix}${name}`;
|
|
7035
|
+
}
|
|
7036
|
+
async acquire(name, ttlMs) {
|
|
7037
|
+
const key = this.buildKey(name);
|
|
7038
|
+
const token = require$$0.randomUUID();
|
|
7039
|
+
const effectiveTtlMs = ttlMs ?? this.ttlMs;
|
|
7040
|
+
const result2 = await this.client.set(key, token, {
|
|
7041
|
+
NX: true,
|
|
7042
|
+
PX: effectiveTtlMs
|
|
7043
|
+
});
|
|
7044
|
+
if (result2 !== "OK") {
|
|
7045
|
+
return null;
|
|
7046
|
+
}
|
|
7047
|
+
return {
|
|
7048
|
+
key,
|
|
7049
|
+
token,
|
|
7050
|
+
ttlMs: effectiveTtlMs
|
|
7051
|
+
};
|
|
7052
|
+
}
|
|
7053
|
+
async release(lock) {
|
|
7054
|
+
const result2 = await this.client.eval(RELEASE_SCRIPT, {
|
|
7055
|
+
keys: [lock.key],
|
|
7056
|
+
arguments: [lock.token]
|
|
7057
|
+
});
|
|
7058
|
+
return Number(result2) === 1;
|
|
7059
|
+
}
|
|
7060
|
+
async renew(lock, ttlMs) {
|
|
7061
|
+
const effectiveTtlMs = ttlMs ?? lock.ttlMs;
|
|
7062
|
+
const result2 = await this.client.eval(RENEW_SCRIPT, {
|
|
7063
|
+
keys: [lock.key],
|
|
7064
|
+
arguments: [lock.token, String(effectiveTtlMs)]
|
|
7065
|
+
});
|
|
7066
|
+
return Number(result2) === 1;
|
|
7067
|
+
}
|
|
7068
|
+
async isOwner(lock) {
|
|
7069
|
+
const currentValue = await this.client.get(lock.key);
|
|
7070
|
+
return currentValue === lock.token;
|
|
7071
|
+
}
|
|
7072
|
+
async runExclusive(name, task, options) {
|
|
7073
|
+
const lock = await this.acquire(name, options?.ttlMs);
|
|
7074
|
+
if (!lock) {
|
|
7075
|
+
if (options?.onLockNotAcquired) {
|
|
7076
|
+
await options.onLockNotAcquired();
|
|
7077
|
+
}
|
|
7078
|
+
return { acquired: false };
|
|
7079
|
+
}
|
|
7080
|
+
let timer = null;
|
|
7081
|
+
let stopped = false;
|
|
7082
|
+
let renewalError = null;
|
|
7083
|
+
const autoRenew = options?.autoRenew ?? false;
|
|
7084
|
+
const heartbeatMs = options?.heartbeatMs ?? this.heartbeatMs;
|
|
7085
|
+
const stopHeartbeat = () => {
|
|
7086
|
+
stopped = true;
|
|
7087
|
+
if (timer) {
|
|
7088
|
+
clearInterval(timer);
|
|
7089
|
+
timer = null;
|
|
7090
|
+
}
|
|
7091
|
+
};
|
|
7092
|
+
if (autoRenew) {
|
|
7093
|
+
timer = setInterval(async () => {
|
|
7094
|
+
if (stopped) {
|
|
7095
|
+
return;
|
|
7096
|
+
}
|
|
7097
|
+
try {
|
|
7098
|
+
const ok = await this.renew(lock, lock.ttlMs);
|
|
7099
|
+
if (!ok) {
|
|
7100
|
+
renewalError = new Error(
|
|
7101
|
+
`Lost lock ownership while renewing "${lock.key}"`
|
|
7102
|
+
);
|
|
7103
|
+
stopHeartbeat();
|
|
7104
|
+
}
|
|
7105
|
+
} catch (err) {
|
|
7106
|
+
renewalError = err instanceof Error ? err : new Error(String(err));
|
|
7107
|
+
stopHeartbeat();
|
|
7108
|
+
}
|
|
7109
|
+
}, heartbeatMs).unref();
|
|
7110
|
+
}
|
|
7111
|
+
try {
|
|
7112
|
+
const result2 = await task();
|
|
7113
|
+
if (renewalError) {
|
|
7114
|
+
throw renewalError;
|
|
7115
|
+
}
|
|
7116
|
+
return { acquired: true, result: result2 };
|
|
7117
|
+
} finally {
|
|
7118
|
+
stopHeartbeat();
|
|
7119
|
+
try {
|
|
7120
|
+
await this.release(lock);
|
|
7121
|
+
} catch {
|
|
7122
|
+
}
|
|
7123
|
+
}
|
|
7124
|
+
}
|
|
7125
|
+
}
|
|
7126
|
+
class SearchParameterManager {
|
|
7127
|
+
redis;
|
|
7128
|
+
alreadyComplete = null;
|
|
7129
|
+
options;
|
|
7130
|
+
constructor(options) {
|
|
7131
|
+
this.options = options;
|
|
7132
|
+
}
|
|
7133
|
+
EnsureSearchParameterDataLoaded = async () => {
|
|
7134
|
+
if (this.alreadyComplete) {
|
|
7135
|
+
return this.alreadyComplete;
|
|
7136
|
+
}
|
|
7137
|
+
if (!this.redis) {
|
|
7138
|
+
const redisUrl = stsconfig.goptions.imRedisMessageProcessorUrl;
|
|
7139
|
+
console.log(`SearchParameterManager(): redis trying to connect ...`);
|
|
7140
|
+
this.redis = redis.createClient({
|
|
7141
|
+
url: redisUrl
|
|
7142
|
+
});
|
|
7143
|
+
await this.redis.connect();
|
|
7144
|
+
console.log(`SearchParameterManager(): redis connected`);
|
|
7145
|
+
}
|
|
7146
|
+
try {
|
|
7147
|
+
const pollIntervalMs = this.options?.pollIntervalMs ?? 250;
|
|
7148
|
+
const lockTtlMs = this.options?.lockTtlMs ?? 3e4;
|
|
7149
|
+
const autoRenew = this.options?.autoRenew ?? true;
|
|
7150
|
+
const heartbeatMs = this.options?.heartbeatMs ?? 1e4;
|
|
7151
|
+
const completeTtlSeconds = this.options?.completeTtlSeconds;
|
|
7152
|
+
const timeoutMs = this.options?.timeoutMs;
|
|
7153
|
+
const completeKey = `fhir:searchparam:${this.options.name}:complete`;
|
|
7154
|
+
const startedAt = Date.now();
|
|
7155
|
+
const lockManager = new RedisDistributedLock(this.redis, {
|
|
7156
|
+
ttlMs: 3e4,
|
|
7157
|
+
heartbeatMs: 1e4,
|
|
7158
|
+
keyPrefix: "lock:"
|
|
7159
|
+
});
|
|
7160
|
+
while (true) {
|
|
7161
|
+
if (typeof timeoutMs === "number" && timeoutMs > 0) {
|
|
7162
|
+
const elapsedMs = Date.now() - startedAt;
|
|
7163
|
+
if (elapsedMs >= timeoutMs) {
|
|
7164
|
+
throw new Error(
|
|
7165
|
+
`Timed out waiting for search param "${this.options.name}" to be loaded`
|
|
7166
|
+
);
|
|
7167
|
+
}
|
|
7168
|
+
}
|
|
7169
|
+
try {
|
|
7170
|
+
this.alreadyComplete = await this.redis.get(completeKey);
|
|
7171
|
+
if (this.alreadyComplete) {
|
|
7172
|
+
console.log(`ensureSearchParamLoaded():alreadyComplete: PID: [${process.pid}] data loaded: [${this.alreadyComplete}]`);
|
|
7173
|
+
return this.alreadyComplete;
|
|
7174
|
+
}
|
|
7175
|
+
} catch (error) {
|
|
7176
|
+
console.error(error);
|
|
7177
|
+
throw error;
|
|
7178
|
+
}
|
|
7179
|
+
const runResult = await lockManager.runExclusive(
|
|
7180
|
+
`fhir:searchparam:${this.options.name}`,
|
|
7181
|
+
async () => {
|
|
7182
|
+
console.log(`ensureSearchParamLoaded(): PID: [${process.pid}] lock acquired`);
|
|
7183
|
+
this.alreadyComplete = await this.redis.get(completeKey);
|
|
7184
|
+
if (this.alreadyComplete) {
|
|
7185
|
+
console.log(`ensureSearchParamLoaded():completeAfterLock: PID: [${process.pid}] data loaded: [${this.alreadyComplete}]`);
|
|
7186
|
+
return;
|
|
7187
|
+
}
|
|
7188
|
+
try {
|
|
7189
|
+
await this.#LoadDefinitions();
|
|
7190
|
+
} catch (error) {
|
|
7191
|
+
console.error(error);
|
|
7192
|
+
throw error;
|
|
7193
|
+
}
|
|
7194
|
+
const completeValue = JSON.stringify({
|
|
7195
|
+
loaded: true,
|
|
7196
|
+
loadedAt: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
7197
|
+
name: this.options.name,
|
|
7198
|
+
workerPid: process.pid
|
|
7199
|
+
});
|
|
7200
|
+
console.log(`ensureSearchParamLoaded(): PID: [${process.pid}] data loaded: [${completeValue}]`);
|
|
7201
|
+
if (typeof completeTtlSeconds === "number" && completeTtlSeconds > 0) {
|
|
7202
|
+
await this.redis.set(completeKey, completeValue, {
|
|
7203
|
+
EX: completeTtlSeconds
|
|
7204
|
+
});
|
|
7205
|
+
} else {
|
|
7206
|
+
await this.redis.set(completeKey, completeValue);
|
|
7207
|
+
}
|
|
7208
|
+
this.alreadyComplete = completeValue;
|
|
7209
|
+
},
|
|
7210
|
+
{
|
|
7211
|
+
ttlMs: lockTtlMs,
|
|
7212
|
+
autoRenew,
|
|
7213
|
+
heartbeatMs
|
|
7214
|
+
}
|
|
7215
|
+
);
|
|
7216
|
+
if (runResult.acquired) {
|
|
7217
|
+
if (this.alreadyComplete) {
|
|
7218
|
+
return this.alreadyComplete;
|
|
7219
|
+
} else {
|
|
7220
|
+
throw new Error(`EnsureSearchParameterDataLoaded(): runResult.acquired is true but this.alreadyComplete not set`);
|
|
7221
|
+
}
|
|
7222
|
+
}
|
|
7223
|
+
console.log(`ensureSearchParamLoaded(): PID: [${process.pid}] Did not get lock`);
|
|
7224
|
+
await stsutils.Sleep(pollIntervalMs);
|
|
7225
|
+
}
|
|
7226
|
+
} catch (error) {
|
|
7227
|
+
console.error(`SearchParameterManager:EnsureSearchParameterDataLoaded(): Error: [${error}]`);
|
|
7228
|
+
throw error;
|
|
7229
|
+
}
|
|
7230
|
+
};
|
|
7231
|
+
Stop = async () => {
|
|
7232
|
+
if (this.redis) {
|
|
7233
|
+
await this.redis.close();
|
|
7234
|
+
console.log(`SearchParameterManager(): redis closed`);
|
|
7235
|
+
}
|
|
7236
|
+
};
|
|
7237
|
+
#LoadDefinitions = async () => {
|
|
7238
|
+
console.log("Data Load Starting ...");
|
|
7239
|
+
for (let i = 0; i < 10; i++) {
|
|
7240
|
+
console.log(`Loading ...: [${i}]`);
|
|
7241
|
+
await stsutils.Sleep(500);
|
|
7242
|
+
}
|
|
7243
|
+
console.log("All Done ***********************************************************************************");
|
|
7244
|
+
};
|
|
7245
|
+
/*
|
|
7246
|
+
private GetTypeKey(typeName: string) {
|
|
7247
|
+
return `stsfhir_type_${typeName}`;
|
|
7248
|
+
}
|
|
7249
|
+
|
|
7250
|
+
private GetSearchParameterByFullUrlKey(fullUrl: string) {
|
|
7251
|
+
return `stsfhir_sp_${fullUrl}`;
|
|
7252
|
+
}
|
|
7253
|
+
|
|
7254
|
+
private GetResourceKey(resourceType: string) {
|
|
7255
|
+
return `stsfhir_resource_${resourceType}`;
|
|
7256
|
+
}
|
|
7257
|
+
|
|
7258
|
+
private GetSearchParameterByResourceTypeKey(resourceType: string) {
|
|
7259
|
+
return `stsfhir_sp_resource_${resourceType}`;
|
|
7260
|
+
}
|
|
7261
|
+
|
|
7262
|
+
private GetSTSCustomSearchParameterByResourceTypeKey(resourceType: string) {
|
|
7263
|
+
return `stsfhir_stscustom_sp_resource_${resourceType}`;
|
|
7264
|
+
}
|
|
7265
|
+
|
|
7266
|
+
// Remove ( ) when either are present
|
|
7267
|
+
private _RemoveOuterParentheses = (input: string): string => {
|
|
7268
|
+
return input.replace(/^\(|\)$/g, '');
|
|
7269
|
+
}
|
|
7270
|
+
|
|
7271
|
+
// Remove ( ) only when both are present
|
|
7272
|
+
private _RemoveSurroundingParentheses = (input: string): string => {
|
|
7273
|
+
return input.replace(/^\((.*)\)$/, '$1').trim();
|
|
7274
|
+
}
|
|
7275
|
+
*/
|
|
7276
|
+
/*
|
|
7277
|
+
NamingSystem derived-from reference NamingSystem.relatedArtifact.where(type='derived-from').resource
|
|
7278
|
+
|
|
7279
|
+
NamingSystem predecessor reference NamingSystem.relatedArtifact.where(type='predecessor').resource
|
|
7280
|
+
|
|
7281
|
+
ClinicalUseDefinition product reference ClinicalUseDefinition.subject.where(resolve() is MedicinalProductDefinition)
|
|
7282
|
+
|
|
7283
|
+
OrganizationAffiliation email token OrganizationAffiliation.contact.telecom.where(system='email')
|
|
7284
|
+
OrganizationAffiliation phone token OrganizationAffiliation.contact.telecom.where(system='phone')
|
|
7285
|
+
|
|
7286
|
+
PackagedProductDefinition biological reference PackagedProductDefinition.packaging.containedItem.item.reference
|
|
7287
|
+
PackagedProductDefinition contained-item reference PackagedProductDefinition.packaging.containedItem.item.reference
|
|
7288
|
+
|
|
7289
|
+
Patient deceased token Patient.deceased.exists() and Patient.deceased != false
|
|
7290
|
+
Practitioner deceased token Practitioner.deceased.exists() and Practitioner.deceased != false
|
|
7291
|
+
|
|
7292
|
+
Location contains special Location.extension('http://hl7.org/fhir/StructureDefinition/location-boundary-geojson').value
|
|
7293
|
+
|
|
7294
|
+
QuestionnaireResponse item-subject reference QuestionnaireResponse.item.where(extension('http://hl7.org/fhir/StructureDefinition/questionnaireresponse-isSubject').exists()).answer.valueReference
|
|
7295
|
+
*/
|
|
7296
|
+
/*
|
|
7297
|
+
private _ProcessExpressions = (resourceName: string, SP_EXPRESSION: string): any => {
|
|
7298
|
+
let expressions: any[] = [];
|
|
7299
|
+
|
|
7300
|
+
//SP_EXPRESSION = this.#RemoveSurroundingParentheses(val2.expression);
|
|
7301
|
+
expressions = SP_EXPRESSION.split('|').map(e => e.trim());
|
|
7302
|
+
|
|
7303
|
+
expressions = expressions.map(e => this._RemoveSurroundingParentheses(e).trim());
|
|
7304
|
+
|
|
7305
|
+
if (resourceName !== '') {
|
|
7306
|
+
expressions = expressions.filter(e => e.split('.')[0].trim().localeCompare(resourceName) === 0);
|
|
7307
|
+
}
|
|
7308
|
+
|
|
7309
|
+
let original_expressions = [ ...expressions ];
|
|
7310
|
+
|
|
7311
|
+
// ofType processing
|
|
7312
|
+
expressions = expressions.map((e: string) => {
|
|
7313
|
+
let capturedTypeOf = undefined;
|
|
7314
|
+
return {
|
|
7315
|
+
path: e.replace(
|
|
7316
|
+
/\.ofType\(([^)]+)\)/g,
|
|
7317
|
+
(_: string, typeName: string) => {
|
|
7318
|
+
capturedTypeOf = true;
|
|
7319
|
+
return typeName.charAt(0).toUpperCase() + typeName.slice(1)
|
|
7320
|
+
}),
|
|
7321
|
+
typeOf: capturedTypeOf
|
|
7322
|
+
}
|
|
7323
|
+
});
|
|
7324
|
+
|
|
7325
|
+
//@@ exists processing here is more or less hard coded to the deceased attribute.
|
|
7326
|
+
//@@ in future, we need an actual fhir language parser.
|
|
7327
|
+
expressions.forEach(expression => {
|
|
7328
|
+
try {
|
|
7329
|
+
const sp = expression as ISearchParamExpressionRecord;
|
|
7330
|
+
if (sp.path.indexOf('exists()') >= 0) {
|
|
7331
|
+
// Person.deceased.exists() and Person.deceased != false
|
|
7332
|
+
const parts = sp.path.split(' ');
|
|
7333
|
+
const i = parts[0].indexOf('exists()');
|
|
7334
|
+
if (i >= 0) {
|
|
7335
|
+
let fieldExistsEx = parts[0].substring(0, i-1);
|
|
7336
|
+
let op = parts[1];
|
|
7337
|
+
let fieldTestEx = parts[2];
|
|
7338
|
+
let comparator = parts[3];
|
|
7339
|
+
let value = parts[4];
|
|
7340
|
+
|
|
7341
|
+
const fieldExistsParts = fieldExistsEx.split('.');
|
|
7342
|
+
let fieldExists = fieldExistsParts.slice(1).join('.');
|
|
7343
|
+
|
|
7344
|
+
const fieldTestParts = fieldTestEx.split('.');
|
|
7345
|
+
let fieldTest = fieldTestParts.slice(1).join('.');
|
|
7346
|
+
|
|
7347
|
+
sp.exists = {
|
|
7348
|
+
fieldExists: [ `${fieldExists}Boolean`, `${fieldExists}DataTime` ], // [x]Boolean or [x]DateTime
|
|
7349
|
+
op,
|
|
7350
|
+
fieldTest: [ `${fieldTest}Boolean`, `${fieldTest}DataTime` ],
|
|
7351
|
+
comparator,
|
|
7352
|
+
value: [ value, undefined ]
|
|
7353
|
+
}
|
|
7354
|
+
}
|
|
7355
|
+
}
|
|
7356
|
+
} catch (error) {
|
|
7357
|
+
|
|
7358
|
+
}
|
|
7359
|
+
});
|
|
7360
|
+
|
|
7361
|
+
// where(resolve()) processing
|
|
7362
|
+
let capturedType: string | undefined;
|
|
7363
|
+
expressions = expressions.map((obj: any) => {
|
|
7364
|
+
capturedType = undefined;
|
|
7365
|
+
const { path, typeOf, exists } = obj;
|
|
7366
|
+
return {
|
|
7367
|
+
path: path.replace(
|
|
7368
|
+
/\.where\(resolve\(\)\s+is\s+([^)]+)\)/,
|
|
7369
|
+
(_: string, typeName: string) => {
|
|
7370
|
+
capturedType = typeName;
|
|
7371
|
+
//return ` // reference = ${capturedType}`; // remove the .where(...) part (you can put ` // ${capturedType}` if you want to keep a comment)
|
|
7372
|
+
return '';
|
|
7373
|
+
}),
|
|
7374
|
+
typeOf,
|
|
7375
|
+
reference: capturedType,
|
|
7376
|
+
exists
|
|
7377
|
+
}
|
|
7378
|
+
});
|
|
7379
|
+
|
|
7380
|
+
// .where processing
|
|
7381
|
+
// The element before the .where will be an array. So this test checks which element(s) from that array are satisifed.
|
|
7382
|
+
let capturedWhere: any = undefined;
|
|
7383
|
+
expressions = expressions.map((obj: any) => {
|
|
7384
|
+
capturedWhere = undefined;
|
|
7385
|
+
const { path, reference, typeOf, exists } = obj;
|
|
7386
|
+
const retVal: any = {
|
|
7387
|
+
path: path.replace(/([^.]+)\.where\(([^=]+)='([^']+)'\)/g, (_: string, field: string, subfield: string, value: string) => {
|
|
7388
|
+
capturedWhere = { field, subfield, value }
|
|
7389
|
+
return field;
|
|
7390
|
+
}),
|
|
7391
|
+
typeOf,
|
|
7392
|
+
reference,
|
|
7393
|
+
exists
|
|
7394
|
+
}
|
|
7395
|
+
if (capturedWhere) {
|
|
7396
|
+
capturedWhere.full = `${capturedWhere.field}[*].${capturedWhere.subfield}=${capturedWhere.value}`;
|
|
7397
|
+
retVal.where = capturedWhere;
|
|
7398
|
+
}
|
|
7399
|
+
return retVal;
|
|
7400
|
+
});
|
|
7401
|
+
|
|
7402
|
+
|
|
7403
|
+
return {
|
|
7404
|
+
expressions,
|
|
7405
|
+
original_expressions
|
|
7406
|
+
};
|
|
7407
|
+
}
|
|
7408
|
+
|
|
7409
|
+
#LoadDefinitions = async (): Promise<void> => {
|
|
7410
|
+
try {
|
|
7411
|
+
let usePath = '';
|
|
7412
|
+
if (fs.existsSync(`${specPath1}${resourcesPath}`)) {
|
|
7413
|
+
usePath = specPath1;
|
|
7414
|
+
} else {
|
|
7415
|
+
usePath = specPath2;
|
|
7416
|
+
}
|
|
7417
|
+
const __resources: any = JSON.parse(fs.readFileSync(`${usePath}${resourcesPath}`, 'utf-8'));
|
|
7418
|
+
const __types: any = JSON.parse(fs.readFileSync(`${usePath}${typesPath}`, 'utf-8'));
|
|
7419
|
+
const __searchParams: any = JSON.parse(fs.readFileSync(`${usePath}${searchParamsPath}`, 'utf-8'));
|
|
7420
|
+
|
|
7421
|
+
for (let i=0; i < (__resources.entry as any[]).length; i++) {
|
|
7422
|
+
const entry = __resources.entry[i];
|
|
7423
|
+
const resource: any = entry.resource;
|
|
7424
|
+
if (resource?.snapshot?.element) {
|
|
7425
|
+
const rt = resource.type as string;
|
|
7426
|
+
await this.redis!.set(this.GetResourceKey(rt), JSON.stringify(resource.snapshot.element));
|
|
7427
|
+
}
|
|
7428
|
+
};
|
|
7429
|
+
|
|
7430
|
+
for (let i=0; i < (__types.entry as any[]).length; i++) {
|
|
7431
|
+
const entry = __types.entry[i];
|
|
7432
|
+
const type = entry.resource;
|
|
7433
|
+
if (type?.snapshot?.element) {
|
|
7434
|
+
await this.redis!.set(this.GetTypeKey(type.type), JSON.stringify(type.snapshot.element));
|
|
7435
|
+
}
|
|
7436
|
+
};
|
|
7437
|
+
|
|
7438
|
+
for (let i=0; i < (__searchParams.entry as any[]).length; i++) {
|
|
7439
|
+
const entry = __searchParams.entry[i];
|
|
7440
|
+
await this.redis!.set(this.GetSearchParameterByFullUrlKey(entry.fullUrl), JSON.stringify(entry.resource));
|
|
7441
|
+
const { code, base } = entry.resource;
|
|
7442
|
+
if (base) {
|
|
7443
|
+
for (let j=0; j < base.length; j++) {
|
|
7444
|
+
const resourceType = base[j];
|
|
7445
|
+
await this.redis!.hSet(this.GetSearchParameterByResourceTypeKey(resourceType), code, JSON.stringify(entry.resource));
|
|
7446
|
+
}
|
|
7447
|
+
}
|
|
7448
|
+
};
|
|
7449
|
+
|
|
7450
|
+
//await this.#LoadDefinitionsPass2(__searchParams);
|
|
7451
|
+
|
|
7452
|
+
console.log(`All completed ...`);
|
|
7453
|
+
} catch (error) {
|
|
7454
|
+
console.error(error);
|
|
7455
|
+
throw error;
|
|
7456
|
+
}
|
|
7457
|
+
}
|
|
7458
|
+
*/
|
|
7459
|
+
/*
|
|
7460
|
+
#LoadDefinitionsPass2 = async (__searchParams: any): Promise<void> => {
|
|
7461
|
+
try {
|
|
7462
|
+
for (let i=0; i < (__searchParams.entry as any[]).length; i++) {
|
|
7463
|
+
|
|
7464
|
+
console.log(`Processing (i) entry: [${i}]`);
|
|
7465
|
+
|
|
7466
|
+
if (i === 47 ) {
|
|
7467
|
+
console.log(`here...`)
|
|
7468
|
+
}
|
|
7469
|
+
|
|
7470
|
+
const entry = __searchParams.entry[i];
|
|
7471
|
+
|
|
7472
|
+
const { id, url, code, base, type, expression, component, target } = entry.resource;
|
|
7473
|
+
|
|
7474
|
+
if (base) {
|
|
7475
|
+
for (let j=0; j < base.length; j++) {
|
|
7476
|
+
const resourceType = base[j];
|
|
7477
|
+
const SP_ID = id;
|
|
7478
|
+
const SP_NAME = code; // code is the true SP name (but in the spec, name is usually the same as code)
|
|
7479
|
+
const SP_TYPE = type;
|
|
7480
|
+
const SP_URL = url;
|
|
7481
|
+
const SP_TARGET = target;
|
|
7482
|
+
let SP_EXPRESSION: string = expression;
|
|
7483
|
+
let expressions: any[] = [];
|
|
7484
|
+
let original_expressions: any[] = [];
|
|
7485
|
+
if (SP_EXPRESSION) {
|
|
7486
|
+
const retVal = this._ProcessExpressions(code, SP_EXPRESSION);
|
|
7487
|
+
expressions = retVal.expressions;
|
|
7488
|
+
original_expressions = retVal.original_expressions;
|
|
7489
|
+
}
|
|
7490
|
+
|
|
7491
|
+
const record: ISearchParamRecord = {
|
|
7492
|
+
SP_ID,
|
|
7493
|
+
SP_NAME,
|
|
7494
|
+
SP_TYPE,
|
|
7495
|
+
SP_URL,
|
|
7496
|
+
SP_TARGET,
|
|
7497
|
+
expressions,
|
|
7498
|
+
original_expressions,
|
|
7499
|
+
SP_DEFINITION: entry.resource // this.resourceHelper.GetSearchParam(SP_URL)
|
|
7500
|
+
};
|
|
7501
|
+
|
|
7502
|
+
for (let i=0; i < record.expressions.length; i++) {
|
|
7503
|
+
//console.log(record.expressions[i]);
|
|
7504
|
+
const rf = await this.GetResourceField(resourceType, record.expressions[i].path);
|
|
7505
|
+
if (rf) {
|
|
7506
|
+
expressions[i].RES_FIELD = rf;
|
|
7507
|
+
}
|
|
7508
|
+
}
|
|
7509
|
+
|
|
7510
|
+
if (component) {
|
|
7511
|
+
record.component = [ ...component ];
|
|
7512
|
+
for (let k=0; k < record.component.length; k++) {
|
|
7513
|
+
const c = record.component[k] as any;
|
|
7514
|
+
//c.SP_RAW_PATHS = (c.expression.split('|') as any[]).map(rp => rp.trim());
|
|
7515
|
+
//c.SP_PATHS = (c.SP_RAW_PATHS as any[]).map(rawPath => this.ResolveFhirChoiceType(rawPath));
|
|
7516
|
+
const retVal = this._ProcessExpressions('', c.expression);
|
|
7517
|
+
c.expressions = retVal.expressions;
|
|
7518
|
+
c.original_expressions = retVal.original_expressions;
|
|
7519
|
+
|
|
7520
|
+
let def = null;
|
|
7521
|
+
const foundSearchParameter = await this.redis!.get(this.GetSearchParameterByFullUrlKey(c.definition));
|
|
7522
|
+
if (foundSearchParameter) {
|
|
7523
|
+
def = JSON.parse(foundSearchParameter);
|
|
7524
|
+
}
|
|
7525
|
+
//const def = await this.GetSearchParam(c.definition); //@@ fails here ...
|
|
7526
|
+
|
|
7527
|
+
if (def && def.type) {
|
|
7528
|
+
c.type = def.type
|
|
7529
|
+
}
|
|
7530
|
+
}
|
|
7531
|
+
}
|
|
7532
|
+
|
|
7533
|
+
await this.redis!.hSet(this.GetSTSCustomSearchParameterByResourceTypeKey(resourceType),
|
|
7534
|
+
code, JSON.stringify(record));
|
|
7535
|
+
|
|
7536
|
+
console.log(`Loaded...(j): [${j}]`);
|
|
7537
|
+
}
|
|
7538
|
+
console.log(`All Loaded...`);
|
|
7539
|
+
}
|
|
7540
|
+
console.log(`Loaded...(i): [${i}]`);
|
|
7541
|
+
};
|
|
7542
|
+
console.log(`Loaded...`);
|
|
7543
|
+
} catch (error) {
|
|
7544
|
+
console.error(error);
|
|
7545
|
+
throw error;
|
|
7546
|
+
}
|
|
7547
|
+
}
|
|
7548
|
+
*/
|
|
7549
|
+
/*
|
|
7550
|
+
GetType = async (typeName: string): Promise<any[]> => {
|
|
7551
|
+
await this.EnsureSearchParameterDataLoaded();
|
|
7552
|
+
const foundType = await this.redis!.get(this.GetTypeKey(typeName));
|
|
7553
|
+
if (foundType) {
|
|
7554
|
+
return JSON.parse(foundType);
|
|
7555
|
+
}
|
|
7556
|
+
return [ ];
|
|
7557
|
+
}
|
|
7558
|
+
|
|
7559
|
+
GetResource = async (resourceType: string): Promise<any[]> => {
|
|
7560
|
+
await this.EnsureSearchParameterDataLoaded();
|
|
7561
|
+
const foundResourceType = await this.redis!.get(this.GetResourceKey(resourceType));
|
|
7562
|
+
if (foundResourceType) {
|
|
7563
|
+
return JSON.parse(foundResourceType);
|
|
7564
|
+
}
|
|
7565
|
+
return [ ];
|
|
7566
|
+
}
|
|
7567
|
+
|
|
7568
|
+
GetResourceField = async (resourceType: string, fieldPath: string): Promise<any> => {
|
|
7569
|
+
const resource: any[] = await this.GetResource(resourceType);
|
|
7570
|
+
if (resource) {
|
|
7571
|
+
const foundRecord = resource.filter(o => o.id === fieldPath);
|
|
7572
|
+
if (foundRecord.length > 0) {
|
|
7573
|
+
return foundRecord[0];
|
|
7574
|
+
}
|
|
7575
|
+
return undefined;
|
|
7576
|
+
}
|
|
7577
|
+
}
|
|
7578
|
+
|
|
7579
|
+
GetSearchParam = async (fullUrl: string): Promise<any> => {
|
|
7580
|
+
await this.EnsureSearchParameterDataLoaded();
|
|
7581
|
+
const foundSearchParameter = await this.redis!.get(this.GetSearchParameterByFullUrlKey(fullUrl));
|
|
7582
|
+
if (foundSearchParameter) {
|
|
7583
|
+
return JSON.parse(foundSearchParameter);
|
|
7584
|
+
}
|
|
7585
|
+
return null;
|
|
7586
|
+
}
|
|
7587
|
+
|
|
7588
|
+
GetSearchParamsByResourceType = async (resourceType: string): Promise<SearchParameter[]> => {
|
|
7589
|
+
await this.EnsureSearchParameterDataLoaded();
|
|
7590
|
+
const all = await this.redis!.hGetAll(this.GetSearchParameterByResourceTypeKey(resourceType));
|
|
7591
|
+
const parsed = Object.values(all).map(v => JSON.parse(v));
|
|
7592
|
+
return parsed;
|
|
7593
|
+
}
|
|
7594
|
+
|
|
7595
|
+
GetSTSCustomSearchParametersByResourceType = async (resourceType: string): Promise<ISearchParamRecord[]> => {
|
|
7596
|
+
await this.EnsureSearchParameterDataLoaded();
|
|
7597
|
+
const all = await this.redis!.hGetAll(this.GetSTSCustomSearchParameterByResourceTypeKey(resourceType));
|
|
7598
|
+
const parsed = Object.values(all).map(v => JSON.parse(v));
|
|
7599
|
+
return parsed;
|
|
7600
|
+
}
|
|
7601
|
+
|
|
7602
|
+
GetSearchParamFromResourceTypeUrl = async (resourceType: string, url: string): Promise<ISearchParamRecord | undefined> => {
|
|
7603
|
+
const customSearchParameters = await this.GetSTSCustomSearchParametersByResourceType(resourceType);
|
|
7604
|
+
const filterByNameAndUrl = customSearchParameters.filter(rt => rt.SP_URL.localeCompare(url) === 0);
|
|
7605
|
+
if (filterByNameAndUrl.length > 0) {
|
|
7606
|
+
return filterByNameAndUrl[0];
|
|
7607
|
+
}
|
|
7608
|
+
}
|
|
7609
|
+
|
|
7610
|
+
GetSearchParamFromResourceType = async (resourceType: string, name: string): Promise<ISearchParamRecord | undefined> => {
|
|
7611
|
+
const customSearchParameters = await this.GetSTSCustomSearchParametersByResourceType(resourceType);
|
|
7612
|
+
const filterByName = customSearchParameters.filter(rt => rt.SP_NAME.localeCompare(name) === 0);
|
|
7613
|
+
if (filterByName.length > 0) {
|
|
7614
|
+
return filterByName[0];
|
|
7615
|
+
}
|
|
7616
|
+
}
|
|
7617
|
+
|
|
7618
|
+
GetPathsFromResourceType = async (resourceType: string, name: string): Promise<string[]> => {
|
|
7619
|
+
const customSearchParameters = await this.GetSTSCustomSearchParametersByResourceType(resourceType);
|
|
7620
|
+
const filterByName = customSearchParameters.filter(rt => rt.SP_NAME.localeCompare(name) === 0);
|
|
7621
|
+
if (filterByName.length > 0) {
|
|
7622
|
+
return filterByName[0].expressions.map(e => e.path)
|
|
7623
|
+
}
|
|
7624
|
+
return [ ];
|
|
7625
|
+
}
|
|
7626
|
+
*/
|
|
7627
|
+
}
|
|
6998
7628
|
const specPath1 = "./dist/";
|
|
6999
7629
|
const specPath2 = "./node_modules/@nsshunt/stsfhirpg/dist/";
|
|
7000
7630
|
const resourcesPath = "fhir-spec/profiles-resources.json";
|
|
@@ -7012,6 +7642,12 @@ class ResourceHelper {
|
|
|
7012
7642
|
}
|
|
7013
7643
|
return ResourceHelper.instance;
|
|
7014
7644
|
}
|
|
7645
|
+
Stop = async () => {
|
|
7646
|
+
if (this.searchParameterManager) {
|
|
7647
|
+
await this.searchParameterManager.Stop();
|
|
7648
|
+
}
|
|
7649
|
+
this.searchParameterManager = void 0;
|
|
7650
|
+
};
|
|
7015
7651
|
#LoadDefinitions = async () => {
|
|
7016
7652
|
let usePath = "";
|
|
7017
7653
|
if (fs.existsSync(`${specPath1}${resourcesPath}`)) {
|
|
@@ -7047,8 +7683,21 @@ class ResourceHelper {
|
|
|
7047
7683
|
this.#definitions.searchParametersByUrl[entry.fullUrl] = entry.resource;
|
|
7048
7684
|
});
|
|
7049
7685
|
await this._LoadSearchParameters();
|
|
7686
|
+
await this.PreLoadSearchParameterData();
|
|
7050
7687
|
return this.#definitions;
|
|
7051
7688
|
};
|
|
7689
|
+
PreLoadSearchParameterData = async () => {
|
|
7690
|
+
console.log(`PreLoadSearchParameterData(): Start ...`);
|
|
7691
|
+
this.searchParameterManager = new SearchParameterManager({
|
|
7692
|
+
name: "stsfhirspdata",
|
|
7693
|
+
pollIntervalMs: 250,
|
|
7694
|
+
lockTtlMs: 3e4,
|
|
7695
|
+
autoRenew: true,
|
|
7696
|
+
heartbeatMs: 1e4
|
|
7697
|
+
});
|
|
7698
|
+
await this.searchParameterManager.EnsureSearchParameterDataLoaded();
|
|
7699
|
+
console.log(`PreLoadSearchParameterData(): End`);
|
|
7700
|
+
};
|
|
7052
7701
|
GetDefinitions = async () => {
|
|
7053
7702
|
if (!this.#definitions) {
|
|
7054
7703
|
await this.#LoadDefinitions();
|
|
@@ -11374,6 +12023,9 @@ class DBSearchIndex {
|
|
|
11374
12023
|
get searchRegistry() {
|
|
11375
12024
|
return SearchRegistry.getInstance();
|
|
11376
12025
|
}
|
|
12026
|
+
Stop = async () => {
|
|
12027
|
+
await this.resourceHelper.Stop();
|
|
12028
|
+
};
|
|
11377
12029
|
#OutputSearchDetails = (resourceName, resourceId, searchFieldRecord) => {
|
|
11378
12030
|
const hl = chalk.yellow.bold.italic;
|
|
11379
12031
|
this.#debug("------------------");
|