@livenetworks/ashlar 1.5.1 → 1.6.1

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.
Files changed (217) hide show
  1. package/README.md +189 -177
  2. package/js/COMPONENTS.md +55 -49
  3. package/js/index.js +22 -5
  4. package/js/ln-accordion/README.md +186 -137
  5. package/js/ln-accordion/ln-accordion.js +1 -1
  6. package/js/ln-ajax/README.md +73 -4
  7. package/js/ln-ajax/ln-ajax.js +1 -1
  8. package/js/ln-ajax/src/ln-ajax.js +13 -13
  9. package/js/ln-api-connector/README.md +261 -150
  10. package/js/ln-api-connector/ln-api-connector.js +1 -1
  11. package/js/ln-api-connector/src/ln-api-connector.js +368 -265
  12. package/js/ln-api-queue/README.md +219 -0
  13. package/js/ln-api-queue/ln-api-queue.js +1 -0
  14. package/js/ln-api-queue/src/ln-api-queue.js +320 -0
  15. package/js/ln-api-queue/src/queue-storage.js +455 -0
  16. package/js/ln-autoresize/README.md +47 -0
  17. package/js/ln-autoresize/ln-autoresize.js +1 -1
  18. package/js/ln-autosave/README.md +33 -1
  19. package/js/ln-autosave/ln-autosave.js +1 -1
  20. package/js/ln-autosave/src/ln-autosave.js +4 -4
  21. package/js/ln-circular-progress/README.md +27 -13
  22. package/js/ln-circular-progress/ln-circular-progress.js +1 -1
  23. package/js/ln-circular-progress/src/ln-circular-progress.js +18 -5
  24. package/js/ln-confirm/README.md +165 -86
  25. package/js/ln-confirm/_ln-confirm.scss +11 -0
  26. package/js/ln-confirm/ln-confirm.js +1 -1
  27. package/js/ln-confirm/src/ln-confirm.js +82 -42
  28. package/js/ln-core/README.md +896 -0
  29. package/js/ln-core/hash.js +92 -0
  30. package/js/ln-core/helpers.js +337 -8
  31. package/js/ln-core/index.js +4 -2
  32. package/js/ln-core/persist.js +29 -3
  33. package/js/ln-core/positioning.js +1 -35
  34. package/js/ln-core/window-cache.js +213 -0
  35. package/js/ln-couchdb-connector/README.md +177 -156
  36. package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
  37. package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +398 -348
  38. package/js/ln-data-coordinator/README.md +536 -165
  39. package/js/ln-data-coordinator/ln-data-coordinator.js +1 -1
  40. package/js/ln-data-coordinator/src/data-read-policy.js +18 -0
  41. package/js/ln-data-coordinator/src/ln-data-coordinator.js +1042 -249
  42. package/js/ln-data-coordinator/src/mutation-receipts.js +46 -0
  43. package/js/ln-data-store/README.md +241 -94
  44. package/js/ln-data-store/ln-data-store.js +1 -1
  45. package/js/ln-data-store/src/ln-data-store.js +694 -699
  46. package/js/ln-date/README.md +39 -1
  47. package/js/ln-date/ln-date-dev.scss +14 -0
  48. package/js/ln-date/ln-date.js +1 -1
  49. package/js/ln-date/src/ln-date.js +236 -178
  50. package/js/ln-debug/README.md +58 -0
  51. package/js/ln-debug/ln-debug.js +1 -0
  52. package/js/ln-debug/src/ln-debug.js +19 -0
  53. package/js/ln-dropdown/README.md +31 -8
  54. package/js/ln-dropdown/ln-dropdown.js +1 -1
  55. package/js/ln-dropdown/src/ln-dropdown.js +30 -13
  56. package/js/ln-editor/README.md +274 -0
  57. package/js/ln-editor/ln-editor.js +1 -0
  58. package/js/ln-editor/ln-editor.scss +43 -0
  59. package/js/ln-editor/src/ln-editor.js +619 -0
  60. package/js/ln-external-links/README.md +44 -5
  61. package/js/ln-external-links/ln-external-links.js +1 -1
  62. package/js/ln-fill/README.md +199 -0
  63. package/js/ln-fill/ln-fill.js +1 -0
  64. package/js/ln-fill/src/ln-fill.js +87 -0
  65. package/js/ln-filter/README.md +92 -2
  66. package/js/ln-filter/ln-filter-dev.scss +8 -0
  67. package/js/ln-filter/ln-filter.js +1 -1
  68. package/js/ln-filter/src/ln-filter.js +22 -7
  69. package/js/ln-form/README.md +212 -49
  70. package/js/ln-form/ln-form.js +1 -1
  71. package/js/ln-form/src/ln-form.js +99 -199
  72. package/js/ln-http/README.md +125 -89
  73. package/js/ln-http/ln-http.js +1 -1
  74. package/js/ln-http/src/ln-http.js +32 -13
  75. package/js/ln-icon/README.md +130 -0
  76. package/js/ln-icon/ln-icon.js +1 -0
  77. package/js/{ln-icons/src/ln-icons.js → ln-icon/src/ln-icon.js} +20 -15
  78. package/js/ln-link/README.md +47 -9
  79. package/js/ln-link/ln-link.js +1 -1
  80. package/js/ln-link/src/ln-link.js +22 -6
  81. package/js/ln-list/README.md +129 -0
  82. package/js/ln-list/ln-list.js +1 -0
  83. package/js/ln-list/ln-list.scss +28 -0
  84. package/js/ln-list/src/ln-list.js +1216 -0
  85. package/js/ln-modal/README.md +185 -154
  86. package/js/ln-modal/ln-modal-dev.scss +13 -0
  87. package/js/ln-modal/ln-modal.js +1 -1
  88. package/js/ln-modal/ln-modal.scss +28 -4
  89. package/js/ln-modal/src/ln-modal.js +137 -201
  90. package/js/ln-modal-coordinator/README.md +99 -0
  91. package/js/ln-modal-coordinator/ln-modal-coordinator-dev.scss +8 -0
  92. package/js/ln-modal-coordinator/ln-modal-coordinator.js +1 -0
  93. package/js/ln-modal-coordinator/src/ln-modal-coordinator.js +345 -0
  94. package/js/ln-nav/README.md +46 -3
  95. package/js/ln-nav/ln-nav.js +1 -1
  96. package/js/ln-nav/src/ln-nav.js +92 -127
  97. package/js/ln-number/README.md +28 -1
  98. package/js/ln-number/ln-number-dev.scss +13 -0
  99. package/js/ln-number/ln-number.js +1 -1
  100. package/js/ln-number/src/ln-number.js +443 -350
  101. package/js/ln-options/README.md +66 -0
  102. package/js/ln-options/ln-options.js +1 -0
  103. package/js/ln-options/src/ln-options.js +74 -0
  104. package/js/ln-popover/README.md +40 -5
  105. package/js/ln-popover/ln-popover.js +1 -1
  106. package/js/ln-popover/src/ln-popover.js +38 -11
  107. package/js/ln-progress/README.md +413 -442
  108. package/js/ln-progress/ln-progress.js +1 -1
  109. package/js/ln-progress/src/ln-progress.js +98 -150
  110. package/js/ln-router/README.md +432 -0
  111. package/js/ln-router/ln-router.js +1 -0
  112. package/js/ln-router/src/ln-router.js +539 -0
  113. package/js/ln-search/README.md +158 -83
  114. package/js/ln-search/ln-search-dev.scss +8 -0
  115. package/js/ln-search/ln-search.js +1 -1
  116. package/js/ln-search/src/ln-search.js +12 -9
  117. package/js/ln-slug/README.md +110 -0
  118. package/js/ln-slug/ln-slug.js +1 -0
  119. package/js/ln-slug/src/ln-slug.js +74 -0
  120. package/js/ln-sort/README.md +170 -0
  121. package/js/ln-sort/ln-sort-dev.scss +8 -0
  122. package/js/ln-sort/ln-sort.js +1 -0
  123. package/js/ln-sort/src/ln-sort.js +178 -0
  124. package/js/ln-sortable/README.md +133 -95
  125. package/js/ln-sortable/ln-sortable.js +1 -1
  126. package/js/ln-stat/README.md +55 -0
  127. package/js/ln-stat/ln-stat.js +1 -0
  128. package/js/ln-stat/src/ln-stat.js +55 -0
  129. package/js/ln-table/README.md +285 -101
  130. package/js/ln-table/ln-table-dev.scss +17 -0
  131. package/js/ln-table/ln-table.js +1 -1
  132. package/js/ln-table/ln-table.scss +1 -1
  133. package/js/ln-table/src/ln-table.js +1493 -473
  134. package/js/ln-table-coordinator/README.md +73 -0
  135. package/js/ln-table-coordinator/ln-table-coordinator.js +1 -0
  136. package/js/ln-table-coordinator/src/ln-table-coordinator.js +160 -0
  137. package/js/ln-tabs/README.md +221 -137
  138. package/js/ln-tabs/ln-tabs-dev.scss +18 -0
  139. package/js/ln-tabs/ln-tabs.js +1 -1
  140. package/js/ln-tabs/src/ln-tabs.js +41 -24
  141. package/js/ln-time/README.md +14 -0
  142. package/js/ln-time/ln-time.js +1 -1
  143. package/js/ln-time/src/ln-time.js +223 -192
  144. package/js/ln-toast/README.md +178 -122
  145. package/js/ln-toast/ln-toast.js +1 -15
  146. package/js/ln-toast/src/ln-toast.js +232 -210
  147. package/js/ln-toast/template.html +16 -12
  148. package/js/ln-toggle/README.md +61 -2
  149. package/js/ln-toggle/ln-toggle-dev.scss +13 -0
  150. package/js/ln-toggle/ln-toggle.js +1 -1
  151. package/js/ln-toggle/src/ln-toggle.js +56 -43
  152. package/js/ln-tooltip/README.md +95 -58
  153. package/js/ln-tooltip/ln-tooltip-dev.scss +9 -0
  154. package/js/ln-tooltip/ln-tooltip.js +1 -1
  155. package/js/ln-tooltip/src/ln-tooltip.js +45 -6
  156. package/js/ln-translations/README.md +118 -96
  157. package/js/ln-translations/ln-translations.js +1 -1
  158. package/js/ln-translations/src/ln-translations.js +10 -5
  159. package/js/ln-upload/README.md +85 -10
  160. package/js/ln-upload/ln-upload.js +1 -1
  161. package/js/ln-upload/src/ln-upload.js +26 -10
  162. package/js/ln-validate/README.md +72 -0
  163. package/js/ln-validate/ln-validate-dev.scss +15 -0
  164. package/js/ln-validate/ln-validate.js +1 -1
  165. package/js/ln-validate/src/ln-validate.js +64 -0
  166. package/package.json +61 -56
  167. package/scss/base/_global.scss +2 -2
  168. package/scss/base/_typography.scss +1 -2
  169. package/scss/components/_date.scss +1 -0
  170. package/scss/components/_dropdown.scss +0 -5
  171. package/scss/components/_editor.scss +5 -0
  172. package/scss/components/_filter.scss +26 -25
  173. package/scss/components/_form.scss +7 -0
  174. package/scss/components/_ln-table.scss +13 -3
  175. package/scss/components/_sort.scss +10 -0
  176. package/scss/components/_table.scss +36 -23
  177. package/scss/components/_toast.scss +68 -41
  178. package/scss/components/_toggle.scss +16 -0
  179. package/scss/config/_density.scss +205 -117
  180. package/scss/config/_icons.scss +10 -9
  181. package/scss/config/_tokens.scss +69 -47
  182. package/scss/config/mixins/_accordion.scss +2 -7
  183. package/scss/config/mixins/_alert.scss +4 -4
  184. package/scss/config/mixins/_breadcrumbs.scss +2 -2
  185. package/scss/config/mixins/_btn.scss +1 -1
  186. package/scss/config/mixins/_card.scss +89 -14
  187. package/scss/config/mixins/_collapsible.scss +3 -0
  188. package/scss/config/mixins/_colors.scss +1 -1
  189. package/scss/config/mixins/_diagnostics.scss +11 -0
  190. package/scss/config/mixins/_display.scss +1 -0
  191. package/scss/config/mixins/_dropdown.scss +3 -5
  192. package/scss/config/mixins/_editor.scss +124 -0
  193. package/scss/config/mixins/_empty-state.scss +7 -0
  194. package/scss/config/mixins/_filter.scss +3 -5
  195. package/scss/config/mixins/_footer.scss +1 -2
  196. package/scss/config/mixins/_form.scss +207 -21
  197. package/scss/config/mixins/_index.scss +2 -0
  198. package/scss/config/mixins/_ln-table.scss +41 -2
  199. package/scss/config/mixins/_modal.scss +149 -66
  200. package/scss/config/mixins/_nav.scss +3 -4
  201. package/scss/config/mixins/_popover.scss +39 -0
  202. package/scss/config/mixins/_sidebar.scss +1 -1
  203. package/scss/config/mixins/_sort.scss +42 -0
  204. package/scss/config/mixins/_table.scss +85 -60
  205. package/scss/config/mixins/_toast.scss +179 -152
  206. package/scss/config/mixins/_tooltip.scss +17 -7
  207. package/scss/config/mixins/_typography.scss +4 -2
  208. package/scss/ln-ashlar-dev.scss +27 -9
  209. package/scss/ln-ashlar.scss +7 -1
  210. package/js/ln-data-table/README.md +0 -110
  211. package/js/ln-data-table/ln-data-table.js +0 -1
  212. package/js/ln-data-table/ln-data-table.scss +0 -10
  213. package/js/ln-data-table/src/ln-data-table.js +0 -1087
  214. package/js/ln-icons/README.md +0 -88
  215. package/js/ln-icons/ln-icons.js +0 -1
  216. package/js/ln-table/ln-table-sort.js +0 -1
  217. package/js/ln-table/src/ln-table-sort.js +0 -156
@@ -1,348 +1,398 @@
1
- import { registerComponent, dispatch, buildUrl, getHeaders, parseHeaders } from '../../ln-core';
2
-
3
- (function () {
4
- const DOM_SELECTOR = 'data-ln-couchdb-connector';
5
- const DOM_ATTRIBUTE = 'lnCouchDbConnector';
6
- const DOM_ALIAS = 'lnConnector';
7
-
8
- if (window[DOM_ATTRIBUTE] !== undefined) return;
9
-
10
- // ─── Component Constructor ─────────────────────────────
11
-
12
- function _component(dom) {
13
- this.dom = dom;
14
- dom[DOM_ATTRIBUTE] = this;
15
- dom[DOM_ALIAS] = this; // Alias for 3-tier compatibility
16
-
17
- this.refreshConfig();
18
- this._handlers = null;
19
- _bindEvents(this);
20
-
21
- return this;
22
- }
23
-
24
- // ─── Refresh Config from DOM Attributes ─────────────────
25
-
26
- _component.prototype.refreshConfig = function () {
27
- const dom = this.dom;
28
-
29
- this.url = dom.getAttribute('data-ln-couchdb-url') || '';
30
- this.db = dom.getAttribute('data-ln-couchdb-db') || '';
31
- this.auth = dom.getAttribute('data-ln-couchdb-auth') || '';
32
- this.credentials = 'same-origin';
33
-
34
- const rawHeaders = dom.getAttribute('data-ln-couchdb-headers') || '';
35
- this.headers = parseHeaders(rawHeaders, 'ln-couchdb-connector');
36
-
37
- if (this.auth) {
38
- console.warn('[ln-couchdb-connector] Security Warning: Sensitive authorization credentials detected in data-ln-couchdb-auth attribute. Storing basic authentication credentials in HTML DOM attributes is highly discouraged and vulnerable to XSS credential extraction. Please use HttpOnly session cookies or a Backend Proxy Gateway instead.');
39
- }
40
- if (rawHeaders.toLowerCase().includes('authorization')) {
41
- console.warn('[ln-couchdb-connector] Security Warning: Sensitive authorization credentials detected in data-ln-couchdb-headers attribute. Please use HttpOnly session cookies or a Backend Proxy Gateway instead.');
42
- }
43
-
44
- dispatch(dom, 'ln-couchdb-connector:config-changed', {
45
- url: this.url,
46
- db: this.db,
47
- auth: this.auth ? '[REDACTED]' : '',
48
- headers: this.headers
49
- });
50
- };
51
-
52
- // ─── CouchDB REST API Methods (Promises) ────────────────
53
-
54
- /**
55
- * Fetch changed records since a sequence.
56
- * Calls CouchDB /{db}/_changes?include_docs=true&since={since}
57
- */
58
- _component.prototype.fetchDelta = function (since) {
59
- const self = this;
60
- const params = ['include_docs=true', 'feed=normal'];
61
- if (since) params.push('since=' + encodeURIComponent(since));
62
- const url = buildUrl(self.url, self.db, '_changes') + '?' + params.join('&');
63
-
64
- return window.fetch(url, { method: 'GET', headers: getHeaders(self.headers, self.auth), credentials: self.credentials })
65
- .then(res => {
66
- if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + res.statusText);
67
- return res.json();
68
- })
69
- .then(data => {
70
- const results = data.results || [];
71
- return {
72
- data: results.filter(r => !r.deleted && r.doc).map(r => Object.assign({}, r.doc, { id: r.doc._id })),
73
- deleted: results.filter(r => r.deleted).map(r => r.id),
74
- synced_at: data.last_seq || since || ''
75
- };
76
- });
77
- };
78
-
79
- /**
80
- * Create a document in CouchDB.
81
- * Sends POST /{db}
82
- */
83
- _component.prototype.create = function (payload) {
84
- const self = this;
85
- const doc = Object.assign({ _id: payload.id }, payload);
86
- if (!doc._id) delete doc._id;
87
-
88
- return window.fetch(buildUrl(self.url, self.db), {
89
- method: 'POST',
90
- headers: getHeaders(self.headers, self.auth),
91
- credentials: self.credentials,
92
- body: JSON.stringify(doc)
93
- })
94
- .then(res => {
95
- if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + res.statusText);
96
- return res.json();
97
- })
98
- .then(resData => Object.assign({}, doc, { id: resData.id, _id: resData.id, _rev: resData.rev }));
99
- };
100
-
101
- /**
102
- * Update a document in CouchDB.
103
- * Sends PUT /{db}/{id}
104
- * Handles revision checks and automatic revision fetching if rev is missing.
105
- */
106
- _component.prototype.update = function (id, payload) {
107
- const self = this;
108
- const doc = Object.assign({ id: String(id), _id: String(id) }, payload);
109
- const rev = doc._rev || doc.rev;
110
-
111
- const getRevPromise = rev ? Promise.resolve(rev) :
112
- window.fetch(buildUrl(self.url, self.db, null, id), { method: 'GET', headers: getHeaders(self.headers, self.auth), credentials: self.credentials })
113
- .then(res => {
114
- if (!res.ok) throw new Error('Could not retrieve document for revision mapping');
115
- return res.json().then(d => d._rev);
116
- });
117
-
118
- return getRevPromise.then(activeRev => {
119
- const finalDoc = Object.assign({}, doc, { _rev: activeRev });
120
- delete finalDoc.rev;
121
-
122
- const headers = Object.assign(getHeaders(self.headers, self.auth), { 'If-Match': activeRev });
123
- return window.fetch(buildUrl(self.url, self.db, null, id), {
124
- method: 'PUT',
125
- headers: headers,
126
- credentials: self.credentials,
127
- body: JSON.stringify(finalDoc)
128
- })
129
- .then(res => {
130
- if (res.ok) return res.json().then(data => Object.assign({}, finalDoc, { _rev: data.rev }));
131
- if (res.status === 409) return res.json().then(data => {
132
- const err = new Error('Conflict');
133
- err.status = 409;
134
- err.data = data;
135
- throw err;
136
- });
137
- throw new Error('HTTP ' + res.status + ': ' + res.statusText);
138
- });
139
- });
140
- };
141
-
142
- /**
143
- * Delete a document in CouchDB.
144
- * Sends DELETE /{db}/{id}?rev={rev}
145
- */
146
- _component.prototype.delete = function (id, rev) {
147
- const self = this;
148
- const getRevPromise = rev ? Promise.resolve(rev) :
149
- window.fetch(buildUrl(self.url, self.db, null, id), { method: 'GET', headers: getHeaders(self.headers, self.auth), credentials: self.credentials })
150
- .then(res => {
151
- if (!res.ok) throw new Error('Could not retrieve document for revision delete');
152
- return res.json().then(d => d._rev);
153
- });
154
-
155
- return getRevPromise.then(activeRev => {
156
- const deleteUrl = buildUrl(self.url, self.db, null, id) + '?rev=' + encodeURIComponent(activeRev);
157
- return window.fetch(deleteUrl, { method: 'DELETE', headers: getHeaders(self.headers, self.auth), credentials: self.credentials })
158
- .then(res => {
159
- if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + res.statusText);
160
- return res.json();
161
- });
162
- });
163
- };
164
-
165
- /**
166
- * Bulk delete documents in CouchDB.
167
- * Sends POST /{db}/_bulk_docs with {"docs": [{"_id": "...", "_rev": "...", "_deleted": true}]}
168
- */
169
- _component.prototype.bulkDelete = function (ids) {
170
- const self = this;
171
- if (!ids || ids.length === 0) return Promise.resolve({ ok: true, deletedCount: 0 });
172
-
173
- return window.fetch(buildUrl(self.url, self.db, '_all_docs'), {
174
- method: 'POST',
175
- headers: getHeaders(self.headers, self.auth),
176
- credentials: self.credentials,
177
- body: JSON.stringify({ keys: ids })
178
- })
179
- .then(res => {
180
- if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + res.statusText);
181
- return res.json();
182
- })
183
- .then(data => {
184
- const rows = data.rows || [];
185
- const docsToDelete = rows
186
- .filter(r => !r.error && r.value && r.value.rev)
187
- .map(r => ({ _id: r.id, _rev: r.value.rev, _deleted: true }));
188
-
189
- if (docsToDelete.length === 0) return { ok: true, deletedCount: 0 };
190
-
191
- return window.fetch(buildUrl(self.url, self.db, '_bulk_docs'), {
192
- method: 'POST',
193
- headers: getHeaders(self.headers, self.auth),
194
- credentials: self.credentials,
195
- body: JSON.stringify({ docs: docsToDelete })
196
- })
197
- .then(res => {
198
- if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + res.statusText);
199
- return res.json();
200
- })
201
- .then(bulkData => ({ ok: true, results: bulkData, deletedCount: docsToDelete.length }));
202
- });
203
- };
204
-
205
- // ─── DOM Event Routing ────────────────────────────────────
206
-
207
- function _bindEvents(self) {
208
- self._handlers = {
209
- sync: function (e) {
210
- const detail = e.detail || {};
211
- self.fetchDelta(detail.since)
212
- .then(function (data) {
213
- dispatch(self.dom, 'ln-couchdb-connector:fetched', { data: data, since: detail.since });
214
- })
215
- .catch(function (err) {
216
- dispatch(self.dom, 'ln-couchdb-connector:error', {
217
- action: 'sync',
218
- error: err.message,
219
- status: err.status || 0,
220
- since: detail.since
221
- });
222
- });
223
- },
224
- create: function (e) {
225
- const detail = e.detail || {};
226
- self.create(detail.data)
227
- .then(function (record) {
228
- dispatch(self.dom, 'ln-couchdb-connector:created', { record: record, tempId: detail.tempId });
229
- })
230
- .catch(function (err) {
231
- dispatch(self.dom, 'ln-couchdb-connector:error', {
232
- action: 'create',
233
- error: err.message,
234
- status: err.status || 0,
235
- tempId: detail.tempId
236
- });
237
- });
238
- },
239
- update: function (e) {
240
- const detail = e.detail || {};
241
- const payload = Object.assign({}, detail.data);
242
-
243
- // Handle expected revision details
244
- if (detail.expected_version !== undefined) {
245
- payload._rev = detail.expected_version;
246
- }
247
-
248
- self.update(detail.id, payload)
249
- .then(function (record) {
250
- dispatch(self.dom, 'ln-couchdb-connector:updated', { record: record, id: detail.id });
251
- })
252
- .catch(function (err) {
253
- dispatch(self.dom, 'ln-couchdb-connector:error', {
254
- action: 'update',
255
- error: err.message,
256
- status: err.status || 0,
257
- id: detail.id,
258
- conflictData: err.status === 409 ? err.data : null
259
- });
260
- });
261
- },
262
- delete: function (e) {
263
- const detail = e.detail || {};
264
- self.delete(detail.id, detail.rev)
265
- .then(function (res) {
266
- dispatch(self.dom, 'ln-couchdb-connector:deleted', { response: res, id: detail.id });
267
- })
268
- .catch(function (err) {
269
- dispatch(self.dom, 'ln-couchdb-connector:error', {
270
- action: 'delete',
271
- error: err.message,
272
- status: err.status || 0,
273
- id: detail.id
274
- });
275
- });
276
- },
277
- bulkDelete: function (e) {
278
- const detail = e.detail || {};
279
- self.bulkDelete(detail.ids)
280
- .then(function (res) {
281
- dispatch(self.dom, 'ln-couchdb-connector:bulk-deleted', { response: res, ids: detail.ids });
282
- })
283
- .catch(function (err) {
284
- dispatch(self.dom, 'ln-couchdb-connector:error', {
285
- action: 'bulk-delete',
286
- error: err.message,
287
- status: err.status || 0,
288
- ids: detail.ids
289
- });
290
- });
291
- }
292
- };
293
-
294
- // Bind events for CouchDB namespaces and also REST/API connector namespaces for 3-tier compatibility
295
- const namespaces = ['ln-couchdb-connector', 'ln-api-connector', 'ln-rest-connector'];
296
- namespaces.forEach(function (ns) {
297
- self.dom.addEventListener(ns + ':request-sync', self._handlers.sync);
298
- self.dom.addEventListener(ns + ':request-fetch', self._handlers.sync);
299
- self.dom.addEventListener(ns + ':request-create', self._handlers.create);
300
- self.dom.addEventListener(ns + ':request-update', self._handlers.update);
301
- self.dom.addEventListener(ns + ':request-delete', self._handlers.delete);
302
- self.dom.addEventListener(ns + ':request-bulk-delete', self._handlers.bulkDelete);
303
- });
304
- }
305
-
306
- _component.prototype.destroy = function () {
307
- if (!this.dom[DOM_ATTRIBUTE]) return;
308
-
309
- const self = this;
310
- if (self._handlers) {
311
- const namespaces = ['ln-couchdb-connector', 'ln-api-connector', 'ln-rest-connector'];
312
- namespaces.forEach(function (ns) {
313
- self.dom.removeEventListener(ns + ':request-sync', self._handlers.sync);
314
- self.dom.removeEventListener(ns + ':request-fetch', self._handlers.sync);
315
- self.dom.removeEventListener(ns + ':request-create', self._handlers.create);
316
- self.dom.removeEventListener(ns + ':request-update', self._handlers.update);
317
- self.dom.removeEventListener(ns + ':request-delete', self._handlers.delete);
318
- self.dom.removeEventListener(ns + ':request-bulk-delete', self._handlers.bulkDelete);
319
- });
320
- self._handlers = null;
321
- }
322
-
323
- dispatch(this.dom, 'ln-couchdb-connector:destroyed', { target: this.dom });
324
-
325
- delete this.dom[DOM_ATTRIBUTE];
326
- delete this.dom[DOM_ALIAS];
327
- };
328
-
329
- // ─── Attribute Sync ────────────────────────────────────────
330
-
331
- function _syncAttribute(el) {
332
- const instance = el[DOM_ATTRIBUTE];
333
- if (!instance) return;
334
- instance.refreshConfig();
335
- }
336
-
337
- // ─── Registration ──────────────────────────────────────
338
-
339
- registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-couchdb-connector', {
340
- extraAttributes: [
341
- 'data-ln-couchdb-url',
342
- 'data-ln-couchdb-db',
343
- 'data-ln-couchdb-auth',
344
- 'data-ln-couchdb-headers'
345
- ],
346
- onAttributeChange: _syncAttribute
347
- });
348
- })();
1
+ import { registerComponent, dispatch, buildUrl, getHeaders, parseHeaders } from '../../ln-core';
2
+
3
+ (function () {
4
+ const DOM_SELECTOR = 'data-ln-couchdb-connector';
5
+ const DOM_ATTRIBUTE = 'lnCouchDbConnector';
6
+ const DOM_ALIAS = 'lnConnector';
7
+
8
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
9
+
10
+ // ─── Response Envelope Unwrap (presence-checked `{message, content}`) ──
11
+ // Raw CouchDB never sends this envelope (content=body, message=null — no-op).
12
+ // A proxy/gateway in front of CouchDB may wrap responses; this makes that
13
+ // transparent without requiring it. Mirrors ln-api-connector.js's unwrap.
14
+ function _unwrapEnvelope(body) {
15
+ const content = (body && body.content !== undefined) ? body.content : body;
16
+ const message = (body && body.message) ? body.message : null;
17
+ return { content: content, message: message };
18
+ }
19
+
20
+ // ─── Component Constructor ─────────────────────────────
21
+
22
+ function _component(dom) {
23
+ this.dom = dom;
24
+ dom[DOM_ATTRIBUTE] = this;
25
+ dom[DOM_ALIAS] = this; // Alias for 3-tier compatibility
26
+
27
+ this.refreshConfig();
28
+ this._handlers = null;
29
+ _bindEvents(this);
30
+
31
+ return this;
32
+ }
33
+
34
+ // ─── Refresh Config from DOM Attributes ─────────────────
35
+
36
+ _component.prototype.refreshConfig = function () {
37
+ const dom = this.dom;
38
+
39
+ this.url = dom.getAttribute('data-ln-couchdb-url') || '';
40
+ this.db = dom.getAttribute('data-ln-couchdb-db') || '';
41
+ this.auth = dom.getAttribute('data-ln-couchdb-auth') || '';
42
+ this.credentials = 'same-origin';
43
+
44
+ const rawHeaders = dom.getAttribute('data-ln-couchdb-headers') || '';
45
+ this.headers = parseHeaders(rawHeaders, 'ln-couchdb-connector');
46
+
47
+ if (this.auth) {
48
+ console.warn('[ln-couchdb-connector] Security Warning: Sensitive authorization credentials detected in data-ln-couchdb-auth attribute. Storing basic authentication credentials in HTML DOM attributes is highly discouraged and vulnerable to XSS credential extraction. Please use HttpOnly session cookies or a Backend Proxy Gateway instead.');
49
+ }
50
+ if (rawHeaders.toLowerCase().includes('authorization')) {
51
+ console.warn('[ln-couchdb-connector] Security Warning: Sensitive authorization credentials detected in data-ln-couchdb-headers attribute. Please use HttpOnly session cookies or a Backend Proxy Gateway instead.');
52
+ }
53
+
54
+ dispatch(dom, 'ln-couchdb-connector:config-changed', {
55
+ url: this.url,
56
+ db: this.db,
57
+ auth: this.auth ? '[REDACTED]' : '',
58
+ headers: this.headers
59
+ });
60
+ };
61
+
62
+ // ─── CouchDB REST API Methods (Promises) ────────────────
63
+
64
+ /**
65
+ * Fetch changed records since a sequence.
66
+ * Calls CouchDB /{db}/_changes?include_docs=true&since={since}
67
+ */
68
+ function _mutationHeaders(self, idempotencyKey, extra) {
69
+ const headers = Object.assign({}, getHeaders(self.headers, self.auth), extra || {});
70
+ if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;
71
+ return headers;
72
+ }
73
+
74
+ _component.prototype.fetchDelta = function (since) {
75
+ const self = this;
76
+ const params = ['include_docs=true', 'feed=normal'];
77
+ if (since) params.push('since=' + encodeURIComponent(since));
78
+ const url = buildUrl(self.url, self.db, '_changes') + '?' + params.join('&');
79
+
80
+ return window.fetch(url, { method: 'GET', headers: getHeaders(self.headers, self.auth), credentials: self.credentials })
81
+ .then(res => {
82
+ if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + res.statusText);
83
+ return res.json();
84
+ })
85
+ .then(data => {
86
+ const results = data.results || [];
87
+ return {
88
+ data: results.filter(r => !r.deleted && r.doc).map(r => Object.assign({}, r.doc, { id: r.doc._id })),
89
+ deleted: results.filter(r => r.deleted).map(r => r.id),
90
+ synced_at: data.last_seq || since || ''
91
+ };
92
+ });
93
+ };
94
+
95
+ /**
96
+ * Create a document in CouchDB.
97
+ * Sends POST /{db}
98
+ */
99
+ function _rawCreate(self, payload, idempotencyKey) {
100
+ const doc = Object.assign({ _id: payload.id }, payload);
101
+ if (!doc._id) delete doc._id;
102
+
103
+ return window.fetch(buildUrl(self.url, self.db), {
104
+ method: 'POST',
105
+ headers: _mutationHeaders(self, idempotencyKey),
106
+ credentials: self.credentials,
107
+ body: JSON.stringify(doc)
108
+ })
109
+ .then(res => {
110
+ if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + res.statusText);
111
+ return res.json();
112
+ })
113
+ .then(body => {
114
+ const unwrapped = _unwrapEnvelope(body);
115
+ const resData = unwrapped.content;
116
+ const record = Object.assign({}, doc, { id: resData.id, _id: resData.id, _rev: resData.rev });
117
+ return { record: record, message: unwrapped.message };
118
+ });
119
+ }
120
+
121
+ _component.prototype.create = function (payload, idempotencyKey) {
122
+ return _rawCreate(this, payload, idempotencyKey).then(r => r.record);
123
+ };
124
+
125
+ /**
126
+ * Update a document in CouchDB.
127
+ * Sends PUT /{db}/{id}
128
+ * Handles revision checks and automatic revision fetching if rev is missing.
129
+ */
130
+ function _rawUpdate(self, id, payload, idempotencyKey) {
131
+ const doc = Object.assign({ id: String(id), _id: String(id) }, payload);
132
+ const rev = doc._rev || doc.rev;
133
+
134
+ const getRevPromise = rev ? Promise.resolve(rev) :
135
+ window.fetch(buildUrl(self.url, self.db, null, id), { method: 'GET', headers: getHeaders(self.headers, self.auth), credentials: self.credentials })
136
+ .then(res => {
137
+ if (!res.ok) throw new Error('Could not retrieve document for revision mapping');
138
+ return res.json().then(d => d._rev);
139
+ });
140
+
141
+ return getRevPromise.then(activeRev => {
142
+ const finalDoc = Object.assign({}, doc, { _rev: activeRev });
143
+ delete finalDoc.rev;
144
+
145
+ const headers = _mutationHeaders(self, idempotencyKey, { 'If-Match': activeRev });
146
+ return window.fetch(buildUrl(self.url, self.db, null, id), {
147
+ method: 'PUT',
148
+ headers: headers,
149
+ credentials: self.credentials,
150
+ body: JSON.stringify(finalDoc)
151
+ })
152
+ .then(res => {
153
+ if (res.ok) return res.json().then(body => {
154
+ const unwrapped = _unwrapEnvelope(body);
155
+ const record = Object.assign({}, finalDoc, { _rev: unwrapped.content.rev });
156
+ return { record: record, message: unwrapped.message };
157
+ });
158
+ if (res.status === 409) return res.json().then(data => {
159
+ const err = new Error('Conflict');
160
+ err.status = 409;
161
+ err.data = data;
162
+ throw err;
163
+ });
164
+ throw new Error('HTTP ' + res.status + ': ' + res.statusText);
165
+ });
166
+ });
167
+ }
168
+
169
+ _component.prototype.update = function (id, payload, idempotencyKey) {
170
+ return _rawUpdate(this, id, payload, idempotencyKey).then(r => r.record);
171
+ };
172
+
173
+ /**
174
+ * Delete a document in CouchDB.
175
+ * Sends DELETE /{db}/{id}?rev={rev}
176
+ */
177
+ function _rawDelete(self, id, rev, idempotencyKey) {
178
+ const getRevPromise = rev ? Promise.resolve(rev) :
179
+ window.fetch(buildUrl(self.url, self.db, null, id), { method: 'GET', headers: getHeaders(self.headers, self.auth), credentials: self.credentials })
180
+ .then(res => {
181
+ if (!res.ok) throw new Error('Could not retrieve document for revision delete');
182
+ return res.json().then(d => d._rev);
183
+ });
184
+
185
+ return getRevPromise.then(activeRev => {
186
+ const deleteUrl = buildUrl(self.url, self.db, null, id) + '?rev=' + encodeURIComponent(activeRev);
187
+ return window.fetch(deleteUrl, { method: 'DELETE', headers: _mutationHeaders(self, idempotencyKey), credentials: self.credentials })
188
+ .then(res => {
189
+ if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + res.statusText);
190
+ return res.json();
191
+ })
192
+ .then(body => {
193
+ const unwrapped = _unwrapEnvelope(body);
194
+ return { response: unwrapped.content, message: unwrapped.message };
195
+ });
196
+ });
197
+ }
198
+
199
+ _component.prototype.delete = function (id, rev, idempotencyKey) {
200
+ return _rawDelete(this, id, rev, idempotencyKey).then(r => r.response);
201
+ };
202
+
203
+ /**
204
+ * Bulk delete documents in CouchDB.
205
+ * Sends POST /{db}/_bulk_docs with {"docs": [{"_id": "...", "_rev": "...", "_deleted": true}]}
206
+ */
207
+ function _rawBulkDelete(self, ids, idempotencyKey) {
208
+ if (!ids || ids.length === 0) return Promise.resolve({ response: { ok: true, deletedCount: 0 }, message: null });
209
+
210
+ return window.fetch(buildUrl(self.url, self.db, '_all_docs'), {
211
+ method: 'POST',
212
+ headers: getHeaders(self.headers, self.auth),
213
+ credentials: self.credentials,
214
+ body: JSON.stringify({ keys: ids })
215
+ })
216
+ .then(res => {
217
+ if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + res.statusText);
218
+ return res.json();
219
+ })
220
+ .then(data => {
221
+ const rows = data.rows || [];
222
+ const docsToDelete = rows
223
+ .filter(r => !r.error && r.value && r.value.rev)
224
+ .map(r => ({ _id: r.id, _rev: r.value.rev, _deleted: true }));
225
+
226
+ if (docsToDelete.length === 0) return { response: { ok: true, deletedCount: 0 }, message: null };
227
+
228
+ return window.fetch(buildUrl(self.url, self.db, '_bulk_docs'), {
229
+ method: 'POST',
230
+ headers: _mutationHeaders(self, idempotencyKey),
231
+ credentials: self.credentials,
232
+ body: JSON.stringify({ docs: docsToDelete })
233
+ })
234
+ .then(res => {
235
+ if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + res.statusText);
236
+ return res.json();
237
+ })
238
+ .then(body => {
239
+ const unwrapped = _unwrapEnvelope(body);
240
+ return { response: { ok: true, results: unwrapped.content, deletedCount: docsToDelete.length }, message: unwrapped.message };
241
+ });
242
+ });
243
+ }
244
+
245
+ _component.prototype.bulkDelete = function (ids, idempotencyKey) {
246
+ return _rawBulkDelete(this, ids, idempotencyKey).then(r => r.response);
247
+ };
248
+
249
+ // ─── DOM Event Routing ────────────────────────────────────
250
+
251
+ function _bindEvents(self) {
252
+ self._handlers = {
253
+ sync: function (e) {
254
+ const detail = e.detail || {};
255
+ self.fetchDelta(detail.since)
256
+ .then(function (data) {
257
+ dispatch(self.dom, 'ln-couchdb-connector:fetched', { data: data, since: detail.since, meta: detail.meta || null });
258
+ })
259
+ .catch(function (err) {
260
+ dispatch(self.dom, 'ln-couchdb-connector:error', {
261
+ action: 'sync',
262
+ error: err.message,
263
+ status: err.status || 0,
264
+ since: detail.since,
265
+ meta: detail.meta || null
266
+ });
267
+ });
268
+ },
269
+ create: function (e) {
270
+ const detail = e.detail || {};
271
+ _rawCreate(self, detail.data, detail.idempotencyKey)
272
+ .then(function (r) {
273
+ dispatch(self.dom, 'ln-couchdb-connector:created', { record: r.record, tempId: detail.tempId, message: r.message, meta: detail.meta || null });
274
+ })
275
+ .catch(function (err) {
276
+ dispatch(self.dom, 'ln-couchdb-connector:error', {
277
+ action: 'create',
278
+ error: err.message,
279
+ status: err.status || 0,
280
+ tempId: detail.tempId,
281
+ meta: detail.meta || null
282
+ });
283
+ });
284
+ },
285
+ update: function (e) {
286
+ const detail = e.detail || {};
287
+ const payload = Object.assign({}, detail.data);
288
+
289
+ // Handle expected revision details
290
+ if (detail.expected_version !== undefined) {
291
+ payload._rev = detail.expected_version;
292
+ }
293
+
294
+ _rawUpdate(self, detail.id, payload, detail.idempotencyKey)
295
+ .then(function (r) {
296
+ dispatch(self.dom, 'ln-couchdb-connector:updated', { record: r.record, id: detail.id, message: r.message, meta: detail.meta || null });
297
+ })
298
+ .catch(function (err) {
299
+ dispatch(self.dom, 'ln-couchdb-connector:error', {
300
+ action: 'update',
301
+ error: err.message,
302
+ status: err.status || 0,
303
+ id: detail.id,
304
+ data: err.status === 409 ? err.data : null,
305
+ conflictData: err.status === 409 ? err.data : null,
306
+ meta: detail.meta || null
307
+ });
308
+ });
309
+ },
310
+ delete: function (e) {
311
+ const detail = e.detail || {};
312
+ _rawDelete(self, detail.id, detail.rev, detail.idempotencyKey)
313
+ .then(function (r) {
314
+ dispatch(self.dom, 'ln-couchdb-connector:deleted', { response: r.response, id: detail.id, message: r.message, meta: detail.meta || null });
315
+ })
316
+ .catch(function (err) {
317
+ dispatch(self.dom, 'ln-couchdb-connector:error', {
318
+ action: 'delete',
319
+ error: err.message,
320
+ status: err.status || 0,
321
+ id: detail.id,
322
+ meta: detail.meta || null
323
+ });
324
+ });
325
+ },
326
+ bulkDelete: function (e) {
327
+ const detail = e.detail || {};
328
+ _rawBulkDelete(self, detail.ids, detail.idempotencyKey)
329
+ .then(function (r) {
330
+ dispatch(self.dom, 'ln-couchdb-connector:bulk-deleted', { response: r.response, ids: detail.ids, message: r.message, meta: detail.meta || null });
331
+ })
332
+ .catch(function (err) {
333
+ dispatch(self.dom, 'ln-couchdb-connector:error', {
334
+ action: 'bulk-delete',
335
+ error: err.message,
336
+ status: err.status || 0,
337
+ ids: detail.ids,
338
+ meta: detail.meta || null
339
+ });
340
+ });
341
+ }
342
+ };
343
+
344
+ // Bind events for CouchDB namespaces and also REST/API connector namespaces for 3-tier compatibility
345
+ const namespaces = ['ln-couchdb-connector', 'ln-api-connector', 'ln-rest-connector'];
346
+ namespaces.forEach(function (ns) {
347
+ self.dom.addEventListener(ns + ':request-sync', self._handlers.sync);
348
+ self.dom.addEventListener(ns + ':request-fetch', self._handlers.sync);
349
+ self.dom.addEventListener(ns + ':request-create', self._handlers.create);
350
+ self.dom.addEventListener(ns + ':request-update', self._handlers.update);
351
+ self.dom.addEventListener(ns + ':request-delete', self._handlers.delete);
352
+ self.dom.addEventListener(ns + ':request-bulk-delete', self._handlers.bulkDelete);
353
+ });
354
+ }
355
+
356
+ _component.prototype.destroy = function () {
357
+ if (!this.dom[DOM_ATTRIBUTE]) return;
358
+
359
+ const self = this;
360
+ if (self._handlers) {
361
+ const namespaces = ['ln-couchdb-connector', 'ln-api-connector', 'ln-rest-connector'];
362
+ namespaces.forEach(function (ns) {
363
+ self.dom.removeEventListener(ns + ':request-sync', self._handlers.sync);
364
+ self.dom.removeEventListener(ns + ':request-fetch', self._handlers.sync);
365
+ self.dom.removeEventListener(ns + ':request-create', self._handlers.create);
366
+ self.dom.removeEventListener(ns + ':request-update', self._handlers.update);
367
+ self.dom.removeEventListener(ns + ':request-delete', self._handlers.delete);
368
+ self.dom.removeEventListener(ns + ':request-bulk-delete', self._handlers.bulkDelete);
369
+ });
370
+ self._handlers = null;
371
+ }
372
+
373
+ dispatch(this.dom, 'ln-couchdb-connector:destroyed', { target: this.dom });
374
+
375
+ delete this.dom[DOM_ATTRIBUTE];
376
+ delete this.dom[DOM_ALIAS];
377
+ };
378
+
379
+ // ─── Attribute Sync ────────────────────────────────────────
380
+
381
+ function _syncAttribute(el) {
382
+ const instance = el[DOM_ATTRIBUTE];
383
+ if (!instance) return;
384
+ instance.refreshConfig();
385
+ }
386
+
387
+ // ─── Registration ──────────────────────────────────────
388
+
389
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-couchdb-connector', {
390
+ extraAttributes: [
391
+ 'data-ln-couchdb-url',
392
+ 'data-ln-couchdb-db',
393
+ 'data-ln-couchdb-auth',
394
+ 'data-ln-couchdb-headers'
395
+ ],
396
+ onAttributeChange: _syncAttribute
397
+ });
398
+ })();