cht-conf 6.6.0 → 6.7.0
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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [6.7.0](https://github.com/medic/cht-conf/compare/v6.6.1...v6.7.0) (2026-11-13)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **#690:** add individual attachment upload for forms with large attachments ([#724](https://github.com/medic/cht-conf/issues/724)) ([8bea476](https://github.com/medic/cht-conf/commit/8bea47639725b411b80ac8f5773df751920009e9)), closes [#690](https://github.com/medic/cht-conf/issues/690)
|
|
7
|
+
|
|
8
|
+
## [6.6.1](https://github.com/medic/cht-conf/compare/v6.6.0...v6.6.1) (2026-08-31)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **#821:** ignore deleted primary-contact ref in move-contacts ([#824](https://github.com/medic/cht-conf/issues/824)) ([d9904ef](https://github.com/medic/cht-conf/commit/d9904ef2aa85e0da84263a7e95f01515be37fce6)), closes [#821](https://github.com/medic/cht-conf/issues/821) [#821](https://github.com/medic/cht-conf/issues/821)
|
|
14
|
+
|
|
1
15
|
# [6.6.0](https://github.com/medic/cht-conf/compare/v6.5.2...v6.6.0) (2026-08-21)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -161,7 +161,18 @@ async function assertSourcePrimaryContactType(db, contactTypeInfo, sourceDoc) {
|
|
|
161
161
|
return;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
let sourcePrimaryContactDoc;
|
|
165
|
+
try {
|
|
166
|
+
sourcePrimaryContactDoc = await db.get(sourcePrimaryContactId);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (err.status === 404) {
|
|
169
|
+
log.warn(`Source "${sourceDoc._id}" has primary contact "${sourcePrimaryContactId}"` +
|
|
170
|
+
` which is deleted or missing. Ignoring.`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
175
|
+
|
|
165
176
|
const primaryContactIsPlace = isPlace(contactTypeInfo, sourcePrimaryContactDoc);
|
|
166
177
|
if (primaryContactIsPlace) {
|
|
167
178
|
throw new Error(`Source "${sourceDoc._id}" has primary contact "${sourcePrimaryContactId}" which is of type place`);
|
|
@@ -1,12 +1,146 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
const mime = require('mime-types');
|
|
2
|
+
const log = require('./log');
|
|
3
|
+
|
|
4
|
+
const MAX_RETRY = 3;
|
|
5
|
+
|
|
6
|
+
const mimeCache = new Map();
|
|
7
|
+
const getContentType = (attachmentName, attachment) => {
|
|
8
|
+
if (attachment.content_type) {return attachment.content_type;}
|
|
9
|
+
if (mimeCache.has(attachmentName)) {return mimeCache.get(attachmentName);}
|
|
10
|
+
const contentType = mime.lookup(attachmentName) || 'application/octet-stream';
|
|
11
|
+
mimeCache.set(attachmentName, contentType);
|
|
12
|
+
return contentType;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
const putDoc = async (db, doc, existingRev = null) => {
|
|
17
|
+
if (existingRev) {
|
|
18
|
+
doc._rev = existingRev;
|
|
19
|
+
} else {
|
|
20
|
+
delete doc._rev;
|
|
21
|
+
}
|
|
22
|
+
return await db.put(doc);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const getDoc = async (db, docId) => {
|
|
26
|
+
try {
|
|
27
|
+
return await db.get(docId);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
if (e.status === 404) {return null;}
|
|
30
|
+
throw e instanceof Error ? e : new Error(JSON.stringify(e));
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
async function addDocAttachment(db, options, retries = MAX_RETRY) {
|
|
35
|
+
const { docId, attachmentName, attachment, currentRev } = options;
|
|
36
|
+
const contentType = getContentType(attachmentName, attachment);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
return await db.putAttachment(docId, attachmentName, currentRev, attachment.data, contentType);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
if (err.status === 409 && retries >= 0) {
|
|
42
|
+
const latestDoc = await getDoc(db, docId);
|
|
43
|
+
return addDocAttachment(db, { ...options, currentRev: latestDoc._rev }, retries - 1);
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`Failed to add attachment ${attachmentName} to ${docId}: ${err.message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
const handleAttachments = async (db, docId, attachments, initialRev) => {
|
|
51
|
+
let currentRev = initialRev;
|
|
52
|
+
const latestDoc = await getDoc(db, docId);
|
|
53
|
+
if (!latestDoc) {
|
|
54
|
+
throw new Error(`Document ${docId} not found`);
|
|
55
|
+
}
|
|
56
|
+
currentRev = latestDoc._rev || currentRev;
|
|
57
|
+
|
|
58
|
+
for (const attachmentName of Object.keys(attachments)) {
|
|
59
|
+
const result = await addDocAttachment(db, {
|
|
60
|
+
docId,
|
|
61
|
+
attachmentName,
|
|
62
|
+
attachment: attachments[attachmentName],
|
|
63
|
+
currentRev
|
|
64
|
+
}, MAX_RETRY);
|
|
65
|
+
currentRev = result.rev;
|
|
66
|
+
}
|
|
67
|
+
return currentRev;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const splitAttachments = (attachments, docId) => {
|
|
71
|
+
const functionalRegex = /^(form|model)\.xml$|^form\.html$|^xml$/i;
|
|
72
|
+
const functionalAttachments = {};
|
|
73
|
+
const mediaAttachments = {};
|
|
74
|
+
|
|
75
|
+
for (const [name, value] of Object.entries(attachments || {})) {
|
|
76
|
+
if (!value?.data) {
|
|
77
|
+
log.warn(`Skipping invalid attachment ${name} for ${docId}: missing data`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
(functionalRegex.test(name) ? functionalAttachments : mediaAttachments)[name] = value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { functionalAttachments, mediaAttachments };
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const saveFunctionalDoc = async (db, doc, functionalAttachments, existingRev) => {
|
|
87
|
+
const docToSave = {
|
|
88
|
+
...doc,
|
|
89
|
+
...(Object.keys(functionalAttachments).length > 0 && { _attachments: functionalAttachments })
|
|
90
|
+
};
|
|
91
|
+
return putDoc(db, docToSave, existingRev);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const saveMediaAttachments = async (db, docId, mediaAttachments, rev) => {
|
|
95
|
+
if (Object.keys(mediaAttachments).length > 0) {
|
|
96
|
+
await handleAttachments(db, docId, mediaAttachments, rev);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleLargeDocument = async (db, doc, retries = MAX_RETRY) => {
|
|
101
|
+
if (retries < 0) {
|
|
102
|
+
throw new Error(`Large document update failed for ${doc._id} after retries`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const { functionalAttachments, mediaAttachments } = splitAttachments(doc._attachments, doc._id);
|
|
107
|
+
const latestDoc = await getDoc(db, doc._id);
|
|
108
|
+
const res = await saveFunctionalDoc(db, doc, functionalAttachments, latestDoc ? latestDoc._rev : null);
|
|
109
|
+
|
|
110
|
+
log.info(`Uploading ${doc._id}...`);
|
|
111
|
+
|
|
112
|
+
await saveMediaAttachments(db, doc._id, mediaAttachments, res.rev);
|
|
113
|
+
|
|
114
|
+
return res;
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (err.status === 409) {
|
|
117
|
+
return handleLargeDocument(db, doc, retries - 1);
|
|
118
|
+
}
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const handleUpsertError = async (db, doc, err, retries) => {
|
|
124
|
+
if (err.status === 409) {
|
|
125
|
+
return upsertDoc(db, doc, retries - 1);
|
|
126
|
+
}
|
|
127
|
+
if (err.status === 413) {
|
|
128
|
+
return handleLargeDocument(db, doc);
|
|
129
|
+
}
|
|
130
|
+
throw err;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const upsertDoc = async (db, doc, retries = MAX_RETRY) => {
|
|
134
|
+
if (retries < 0) {
|
|
135
|
+
throw new Error(`Document update failed for ${doc._id} after retries due to conflicts`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const existingDoc = await getDoc(db, doc._id);
|
|
140
|
+
return await putDoc(db, doc, existingDoc ? existingDoc._rev : null);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
return handleUpsertError(db, doc, err, retries);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
module.exports = upsertDoc;
|