@peopl-health/nexus 5.71.0 → 5.71.2
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/lib/clinical/flags/clinicalContextFlags.js +10 -0
- package/lib/clinical/helpers/clinicalProfileHelper.js +2 -3
- package/lib/clinical/helpers/doctorReviewMessageHelper.js +8 -4
- package/lib/clinical/memory/DefaultMemoryManager.js +3 -2
- package/lib/clinical/services/escalationDraftService.js +10 -4
- package/lib/clinical/services/patientProfileService.js +145 -0
- package/lib/config/lifecycle.js +2 -0
- package/lib/index.js +11 -0
- package/lib/models/patientProfileModel.js +24 -0
- package/package.json +1 -1
|
@@ -10,6 +10,14 @@ function contextVerifierLive() {
|
|
|
10
10
|
return flagOn('CLINICAL_CONTEXT_VERIFIER_LIVE');
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
function profileFromMongo() {
|
|
14
|
+
return flagOn('PATIENT_PROFILE_FROM_MONGO');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function profileSyncOn() {
|
|
18
|
+
return flagOn('PATIENT_PROFILE_SYNC');
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
function contextMirrorLive() {
|
|
14
22
|
return flagOn('CLINICAL_CONTEXT_MIRROR_LIVE') && contextReadLive();
|
|
15
23
|
}
|
|
@@ -18,4 +26,6 @@ module.exports = {
|
|
|
18
26
|
contextReadLive,
|
|
19
27
|
contextMirrorLive,
|
|
20
28
|
contextVerifierLive,
|
|
29
|
+
profileFromMongo,
|
|
30
|
+
profileSyncOn,
|
|
21
31
|
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
const {
|
|
2
|
-
const { Follow_Up_ID } = require('../../config/airtableConfig');
|
|
1
|
+
const { resolveProfileIdentity } = require('../services/patientProfileService');
|
|
3
2
|
const { safeParse } = require('../../utils/jsonUtils');
|
|
4
3
|
|
|
5
4
|
const CONTEXT_FIELD = 'clinical-context-json';
|
|
@@ -35,7 +34,7 @@ function richestProfile(records) {
|
|
|
35
34
|
|
|
36
35
|
async function readClinicalProfile(code) {
|
|
37
36
|
try {
|
|
38
|
-
const identity = await
|
|
37
|
+
const identity = await resolveProfileIdentity(code);
|
|
39
38
|
if (identity.kind === 'missing') return null;
|
|
40
39
|
if (identity.kind !== 'resolved') return undefined;
|
|
41
40
|
return richestProfile([identity.record]);
|
|
@@ -27,6 +27,7 @@ function flagsByStatus(sections) {
|
|
|
27
27
|
absent: named(false),
|
|
28
28
|
unassessed: pending.map((flag) => inlineOf(flag.flag)),
|
|
29
29
|
drugSafety: pending.filter((flag) => flag.blocksDrug !== false).map((flag) => inlineOf(flag.flag)),
|
|
30
|
+
urgent: pending.filter((flag) => flag.urgentIfPositive === true).map((flag) => inlineOf(flag.flag)),
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
|
|
@@ -60,13 +61,16 @@ function proposalLine({ proposal, withheldReason, conduct, sections }) {
|
|
|
60
61
|
const declared = [...(proposal.conditions ?? []), ...(proposal.ruledOut ?? []).map((key) => `descartado ${key}`)]
|
|
61
62
|
.map((key) => inlineOf(key)?.replace(/_/g, ' ')).filter(Boolean);
|
|
62
63
|
const table = formularyFor(proposal.symptom)?.label;
|
|
63
|
-
const { unassessed, drugSafety } = flagsByStatus(sections);
|
|
64
|
-
const
|
|
65
|
-
|
|
64
|
+
const { unassessed, drugSafety, urgent } = flagsByStatus(sections);
|
|
65
|
+
const gate = unassessed.filter((flag) => drugSafety.includes(flag) || urgent.includes(flag));
|
|
66
|
+
const call = urgent.length ? `Llamar antes de indicar: ${urgent.join(', ')}. ` : '';
|
|
67
|
+
const lead = gate.length
|
|
68
|
+
? `→ ${call}Propuesta para su aprobación si se confirma sin ${gate.join(', ')} (tabla ${table})`
|
|
66
69
|
: `→ Propuesta para su aprobación (tabla ${table})`;
|
|
70
|
+
const armedConduct = urgent.length ? inlineOf(conduct) : null;
|
|
67
71
|
return [
|
|
68
72
|
`${lead}: ${sentence(drugs.join('; '))}`,
|
|
69
|
-
unassessed.length && 'Si algo de lo pendiente es positivo: valoración presencial.',
|
|
73
|
+
armedConduct ? sentence(armedConduct) : unassessed.length && 'Si algo de lo pendiente es positivo: valoración presencial.',
|
|
70
74
|
declared.length && `Verificar: ${sentence(declared.join(', '))}`,
|
|
71
75
|
].filter(Boolean).join(' ');
|
|
72
76
|
}
|
|
@@ -3,7 +3,8 @@ const { handlePendingFunctionCalls: handlePendingFunctionCallsUtil } = require('
|
|
|
3
3
|
|
|
4
4
|
const { assembleRiskHeadline } = require('../services/riskHeadlineService');
|
|
5
5
|
const { resolveNutritionPlan } = require('../services/nutritionPlanService');
|
|
6
|
-
const {
|
|
6
|
+
const { identityError } = require('../services/patientIdentityService');
|
|
7
|
+
const { resolveProfileIdentity } = require('../services/patientProfileService');
|
|
7
8
|
const { Follow_Up_ID } = require('../../config/airtableConfig');
|
|
8
9
|
const runtimeConfig = require('../../config/runtimeConfig');
|
|
9
10
|
|
|
@@ -269,7 +270,7 @@ class DefaultMemoryManager extends MemoryManager {
|
|
|
269
270
|
|
|
270
271
|
async _getClinicalContext(whatsappId) {
|
|
271
272
|
try {
|
|
272
|
-
const identity = await
|
|
273
|
+
const identity = await resolveProfileIdentity(whatsappId);
|
|
273
274
|
if (identity.kind === 'ambiguous') throw identityError(identity.kind);
|
|
274
275
|
const record = identity.record || {};
|
|
275
276
|
const symptomsRecords = await getRecordByFilter(Follow_Up_ID, 'all_triages_last_month', fieldEquals('whatsapp_id', whatsappId));
|
|
@@ -36,14 +36,16 @@ function settleProposal(parsed) {
|
|
|
36
36
|
const items = Array.isArray(raw?.items) ? raw.items : [];
|
|
37
37
|
const stated = trimmed(parsed?.proposalWithheldReason);
|
|
38
38
|
const conduct = stated && !drugsNamedIn(stated).length ? stated : null;
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
const { present, urgent } = flagsByStatus(parsed);
|
|
40
|
+
const callFirst = urgent.length && !present.length ? `llamar antes de indicar: ${urgent.join(', ')}` : null;
|
|
41
|
+
if (withheld) return { proposal: null, withheldReason: [withheld, callFirst].filter(Boolean).join('; '), conduct, strippedItems: items.length };
|
|
42
|
+
if (!items.length) return { proposal: null, withheldReason: callFirst, conduct, strippedItems: 0 };
|
|
41
43
|
|
|
42
44
|
const conditions = Array.isArray(raw.conditions) ? raw.conditions : [];
|
|
43
45
|
const ruledOut = Array.isArray(raw.ruledOut) ? raw.ruledOut : [];
|
|
44
46
|
const proposal = { symptom: raw.symptom, items, conditions, ruledOut };
|
|
45
47
|
const { valid, issues } = validateProposal(proposal, { conditions, ruledOut });
|
|
46
|
-
return valid ? { proposal, withheldReason: null, strippedItems: 0 } : { invalid: issues };
|
|
48
|
+
return valid ? { proposal, withheldReason: null, conduct, strippedItems: 0 } : { invalid: issues };
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
const textList = (value) => (Array.isArray(value) ? value.map(trimmed).filter(Boolean) : []);
|
|
@@ -155,7 +157,11 @@ async function generateEscalationDraft({
|
|
|
155
157
|
|
|
156
158
|
const issues = settled.flatMap((entry) => entry.invalid ?? []);
|
|
157
159
|
if (!issues.length) issues.push(...crossSymptomIssues(proposals));
|
|
158
|
-
const
|
|
160
|
+
const isConfirmed = ({ block, proposal }) => {
|
|
161
|
+
const { drugSafety, urgent } = flagsByStatus(block);
|
|
162
|
+
return proposal && !drugSafety.length && !urgent.length;
|
|
163
|
+
};
|
|
164
|
+
const confirmed = settled.filter(isConfirmed).map((entry) => entry.proposal);
|
|
159
165
|
const patientDrugs = unproposedDrugsIn(patientMessage, confirmed);
|
|
160
166
|
if (!issues.length && patientDrugs.length) {
|
|
161
167
|
issues.push(`el mensaje al paciente menciona ${patientDrugs.join(', ')}, que no está en la propuesta de tratamiento`);
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const { isShadowRun } = require('../helpers/shadowHelper');
|
|
2
|
+
const { profileFromMongo } = require('../flags/clinicalContextFlags');
|
|
3
|
+
const { Estado_General_ID, Follow_Up_ID } = require('../../config/airtableConfig');
|
|
4
|
+
const runtimeConfig = require('../../config/runtimeConfig');
|
|
5
|
+
const { MATCH_ALL } = require('../../helpers/airtableFormulaHelper');
|
|
6
|
+
const { getContextSyncState } = require('../../models/contextSyncStateModel');
|
|
7
|
+
const { getPatientProfile } = require('../../models/patientProfileModel');
|
|
8
|
+
const { getRecordByFilter } = require('../../services/airtableService');
|
|
9
|
+
const { inlineOf } = require('../../utils/coercionUtils');
|
|
10
|
+
const { createIntervalSweeper } = require('../../utils/intervalSweeper');
|
|
11
|
+
const { logger } = require('../../utils/logger');
|
|
12
|
+
const { resolvePatientIdentity } = require('./patientIdentityService');
|
|
13
|
+
|
|
14
|
+
const TABLE = 'estado_general';
|
|
15
|
+
const FIELDS = ['whatsapp_id', 'clinical-context-json', 'last_modified_clinical_context', 'doctor', 'tipo', 'source', 'quimio_context'];
|
|
16
|
+
const JOB = 'patient-profile';
|
|
17
|
+
const MS_PER_MINUTE = 60 * 1000;
|
|
18
|
+
const DEFAULT_INTERVAL_MS = 2 * MS_PER_MINUTE;
|
|
19
|
+
const OVERLAP_MS = 5 * MS_PER_MINUTE;
|
|
20
|
+
const RECONCILE_MS = 24 * 60 * MS_PER_MINUTE;
|
|
21
|
+
const MAX_REMOVAL_SHARE = 0.05;
|
|
22
|
+
const MISSING_TTL_MS = 2 * MS_PER_MINUTE;
|
|
23
|
+
const missingUntil = new Map();
|
|
24
|
+
|
|
25
|
+
const intervalMs = () => Number(runtimeConfig.get('PATIENT_PROFILE_SYNC_INTERVAL_MS', DEFAULT_INTERVAL_MS));
|
|
26
|
+
|
|
27
|
+
function changedSince(watermark) {
|
|
28
|
+
const from = new Date(new Date(watermark).getTime() - OVERLAP_MS).toISOString();
|
|
29
|
+
return `IS_AFTER(LAST_MODIFIED_TIME(), DATETIME_PARSE('${from}'))`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function toProfile(row, syncedAt) {
|
|
33
|
+
const fields = Object.fromEntries(FIELDS.filter((f) => row[f] !== undefined).map((f) => [f, row[f]]));
|
|
34
|
+
return { recordId: row._recordId, whatsappId: inlineOf(row.whatsapp_id) || null, fields, syncedAt };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function readState() {
|
|
38
|
+
return (await getContextSyncState().findOne({ job: JOB }).lean()) || { job: JOB, watermark: null, fullReconciledAt: null };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function saveState({ startedAt, written, reason = null, reconciled = false, advance = true }) {
|
|
42
|
+
await getContextSyncState().updateOne(
|
|
43
|
+
{ job: JOB },
|
|
44
|
+
{
|
|
45
|
+
$set: { job: JOB, lastRunAt: startedAt, lastWritten: written, lastReason: reason, ...(reconciled ? { fullReconciledAt: startedAt } : {}) },
|
|
46
|
+
...(advance ? { $max: { watermark: startedAt } } : {}),
|
|
47
|
+
},
|
|
48
|
+
{ upsert: true },
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function upsertProfiles(rows, syncedAt) {
|
|
53
|
+
const usable = rows.filter((row) => /^rec[A-Za-z0-9]+$/.test(row._recordId || ''));
|
|
54
|
+
if (!usable.length) return 0;
|
|
55
|
+
await getPatientProfile().bulkWrite(usable.map((row) => {
|
|
56
|
+
const profile = toProfile(row, syncedAt);
|
|
57
|
+
return { updateOne: { filter: { recordId: profile.recordId }, update: { $set: profile }, upsert: true } };
|
|
58
|
+
}), { ordered: false });
|
|
59
|
+
return usable.length;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function removeUnseen(seen, startedAt) {
|
|
63
|
+
const Model = getPatientProfile();
|
|
64
|
+
const unseen = { recordId: { $nin: seen }, syncedAt: { $lt: startedAt } };
|
|
65
|
+
const stale = await Model.countDocuments(unseen);
|
|
66
|
+
const stored = await Model.countDocuments({});
|
|
67
|
+
if (stale > Math.max(1, stored * MAX_REMOVAL_SHARE)) {
|
|
68
|
+
logger.error('[PatientProfileSync] A full read would remove too many profiles; nothing removed', { stale, stored });
|
|
69
|
+
return { removed: 0, reason: 'removal-skipped' };
|
|
70
|
+
}
|
|
71
|
+
return { removed: stale ? (await Model.deleteMany(unseen)).deletedCount || 0 : 0, reason: null };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function syncPatientProfiles({ now = new Date() } = {}) {
|
|
75
|
+
const state = await readState();
|
|
76
|
+
const empty = (await getPatientProfile().countDocuments({})) === 0;
|
|
77
|
+
const reconcile = empty || !state.watermark || !state.fullReconciledAt || now - new Date(state.fullReconciledAt) >= RECONCILE_MS;
|
|
78
|
+
let rows;
|
|
79
|
+
try {
|
|
80
|
+
rows = await getRecordByFilter(Estado_General_ID, TABLE, reconcile ? MATCH_ALL : changedSince(state.watermark), null, FIELDS, true);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
await saveState({ startedAt: now, written: 0, reason: 'airtable-unavailable', advance: false });
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
const written = await upsertProfiles(rows, now);
|
|
86
|
+
const { removed, reason } = reconcile ? await removeUnseen(rows.map((row) => row._recordId).filter(Boolean), now) : { removed: 0, reason: null };
|
|
87
|
+
await saveState({ startedAt: now, written, reason, reconciled: reconcile });
|
|
88
|
+
return { ok: true, written, removed, reconciled: reconcile, reason };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let storeReady = false;
|
|
92
|
+
|
|
93
|
+
async function profileStoreReady() {
|
|
94
|
+
if (!storeReady) storeReady = Boolean((await readState()).fullReconciledAt);
|
|
95
|
+
return storeReady;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function fromAirtable(code, docs = []) {
|
|
99
|
+
const identity = await resolvePatientIdentity(Estado_General_ID, code);
|
|
100
|
+
if (identity.kind !== 'resolved' || isShadowRun()) return identity;
|
|
101
|
+
const keep = async () => {
|
|
102
|
+
await upsertProfiles([{ ...identity.record, _recordId: identity.recordId }], new Date());
|
|
103
|
+
const gone = docs.map((d) => d.recordId).filter((id) => id !== identity.recordId);
|
|
104
|
+
if (!gone.length) return;
|
|
105
|
+
logger.warn('[PatientProfile] Removing stored profiles Airtable no longer has for this number', { removed: gone, kept: identity.recordId });
|
|
106
|
+
await getPatientProfile().deleteMany({ recordId: { $in: gone } });
|
|
107
|
+
};
|
|
108
|
+
await keep().catch((error) => logger.warn('[PatientProfile] Could not keep a profile read from Airtable', { error: error.message }));
|
|
109
|
+
return identity;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function resolveProfileIdentity(code) {
|
|
113
|
+
if (!profileFromMongo()) return resolvePatientIdentity(Follow_Up_ID, code);
|
|
114
|
+
if (typeof code !== 'string' || !code.trim()) return resolvePatientIdentity(Estado_General_ID, code);
|
|
115
|
+
const key = inlineOf(code);
|
|
116
|
+
if ((missingUntil.get(key) || 0) > Date.now()) return { kind: 'missing' };
|
|
117
|
+
let docs;
|
|
118
|
+
try {
|
|
119
|
+
if (!(await profileStoreReady())) return resolvePatientIdentity(Estado_General_ID, code);
|
|
120
|
+
docs = await getPatientProfile().find({ whatsappId: key }).lean();
|
|
121
|
+
} catch (error) {
|
|
122
|
+
logger.warn('[PatientProfile] Mongo unavailable; reading Airtable', { error: error.message });
|
|
123
|
+
return resolvePatientIdentity(Estado_General_ID, code);
|
|
124
|
+
}
|
|
125
|
+
if (docs.length === 1) return { kind: 'resolved', recordId: docs[0].recordId, record: { ...docs[0].fields } };
|
|
126
|
+
const identity = await fromAirtable(code, docs);
|
|
127
|
+
if (identity.kind === 'missing') missingUntil.set(key, Date.now() + MISSING_TTL_MS);
|
|
128
|
+
return identity;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const sweeper = createIntervalSweeper({
|
|
132
|
+
label: '[PatientProfileSync]',
|
|
133
|
+
sweep: () => syncPatientProfiles(),
|
|
134
|
+
defaultIntervalMs: intervalMs,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
function startPatientProfileSync(options) {
|
|
138
|
+
const timer = sweeper.start(options);
|
|
139
|
+
syncPatientProfiles().catch((error) => logger.error('[PatientProfileSync] First sync failed', { error: error.message }));
|
|
140
|
+
return timer;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const stopPatientProfileSync = () => sweeper.stop();
|
|
144
|
+
|
|
145
|
+
module.exports = { resolveProfileIdentity, syncPatientProfiles, startPatientProfileSync, stopPatientProfileSync };
|
package/lib/config/lifecycle.js
CHANGED
|
@@ -10,6 +10,7 @@ const { stopReviewDraftSweep } = require('../services/reviewDraftSweepService');
|
|
|
10
10
|
const { stopReviewAlertSweep } = require('../services/reviewAlertSweepService');
|
|
11
11
|
const { clearInteractiveRoutes } = require('../core/interactiveRouteService');
|
|
12
12
|
const { _resetDefaultInstance } = require('../core/NexusMessaging');
|
|
13
|
+
const { stopPatientProfileSync } = require('../clinical/services/patientProfileService');
|
|
13
14
|
const { _resetSafetyNetConfig } = require('../clinical/config/safetyNetConfig');
|
|
14
15
|
const { _resetSafetyNetState } = require('../clinical/services/pausedSafetyNetService');
|
|
15
16
|
const { stopSafetyNetSweep } = require('../clinical/services/safetyNetPledgeService');
|
|
@@ -24,6 +25,7 @@ const SWEEP_STOP_MS = 2000;
|
|
|
24
25
|
function stopAllSweeps({ timeoutMs = SWEEP_STOP_MS, label = '[lifecycle]' } = {}) {
|
|
25
26
|
const stopped = Promise.allSettled([
|
|
26
27
|
stopSafetyNetSweep(), stopRecommendationRecoverySweep(), stopInboundRecoverySweep(), stopReviewDraftSweep(), stopReviewAlertSweep(),
|
|
28
|
+
stopPatientProfileSync(),
|
|
27
29
|
]);
|
|
28
30
|
return withTimeout(stopped, timeoutMs, 'sweeps.stop')
|
|
29
31
|
.catch(error => logger.warn(`${label} A sweep was still running`, { error: error.message }));
|
package/lib/index.js
CHANGED
|
@@ -34,6 +34,8 @@ const { startRecommendationRecoverySweep } = require('./clinical/services/recomm
|
|
|
34
34
|
const { startInboundRecoverySweep } = require('./services/inboundRecoveryService');
|
|
35
35
|
const { startReviewDraftSweep } = require('./services/reviewDraftSweepService');
|
|
36
36
|
const { startReviewAlertSweep } = require('./services/reviewAlertSweepService');
|
|
37
|
+
const { startPatientProfileSync } = require('./clinical/services/patientProfileService');
|
|
38
|
+
const { profileSyncOn } = require('./clinical/flags/clinicalContextFlags');
|
|
37
39
|
const { requestIdMiddleware, getRequestId } = require('./middleware/requestId');
|
|
38
40
|
const { QueueAdapter, LocalQueueAdapter, RedisQueueAdapter, createQueueAdapter, registerQueueAdapter } = require('./queue');
|
|
39
41
|
const { DrainingError } = require('./core/BatchingManager');
|
|
@@ -215,6 +217,15 @@ class Nexus {
|
|
|
215
217
|
logger.warn('Warning: failed to arm the clinical review draft sweep:', e?.message || e);
|
|
216
218
|
}
|
|
217
219
|
|
|
220
|
+
try {
|
|
221
|
+
if (profileSyncOn()) {
|
|
222
|
+
startPatientProfileSync();
|
|
223
|
+
logger.info('[Nexus] patient profile sync armed');
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {
|
|
226
|
+
logger.warn('Warning: failed to arm the patient profile sync:', e?.message || e);
|
|
227
|
+
}
|
|
228
|
+
|
|
218
229
|
try {
|
|
219
230
|
if (options.reviewAlerts?.enabled === true) {
|
|
220
231
|
startReviewAlertSweep({ intervalMs: options.reviewAlerts.intervalMs });
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const { getDb, getModelDatabase } = require('../config/mongoConfig');
|
|
4
|
+
|
|
5
|
+
const patientProfileSchema = new mongoose.Schema({
|
|
6
|
+
recordId: { type: String, required: true },
|
|
7
|
+
whatsappId: { type: String, default: null },
|
|
8
|
+
fields: { type: mongoose.Schema.Types.Mixed, default: {} },
|
|
9
|
+
syncedAt: { type: Date, default: null },
|
|
10
|
+
}, { timestamps: true, minimize: false });
|
|
11
|
+
|
|
12
|
+
patientProfileSchema.index({ recordId: 1 }, { unique: true });
|
|
13
|
+
patientProfileSchema.index({ whatsappId: 1 });
|
|
14
|
+
|
|
15
|
+
const getPatientProfile = () => {
|
|
16
|
+
const dbName = getModelDatabase('PatientProfile');
|
|
17
|
+
const db = dbName ? getDb(dbName) : mongoose;
|
|
18
|
+
return db.models.PatientProfile || db.model('PatientProfile', patientProfileSchema);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
getPatientProfile,
|
|
23
|
+
patientProfileSchema,
|
|
24
|
+
};
|