@comapeo/core 1.0.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/LICENSE.md +9 -0
- package/README.md +31 -0
- package/dist/blob-api.d.ts +92 -0
- package/dist/blob-api.d.ts.map +1 -0
- package/dist/blob-store/index.d.ts +163 -0
- package/dist/blob-store/index.d.ts.map +1 -0
- package/dist/blob-store/live-download.d.ts +107 -0
- package/dist/blob-store/live-download.d.ts.map +1 -0
- package/dist/config-import.d.ts +74 -0
- package/dist/config-import.d.ts.map +1 -0
- package/dist/constants.d.ts +14 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/core-manager/bitfield-rle.d.ts +25 -0
- package/dist/core-manager/bitfield-rle.d.ts.map +1 -0
- package/dist/core-manager/core-index.d.ts +56 -0
- package/dist/core-manager/core-index.d.ts.map +1 -0
- package/dist/core-manager/index.d.ts +125 -0
- package/dist/core-manager/index.d.ts.map +1 -0
- package/dist/core-manager/random-access-file-pool.d.ts +17 -0
- package/dist/core-manager/random-access-file-pool.d.ts.map +1 -0
- package/dist/core-manager/remote-bitfield.d.ts +146 -0
- package/dist/core-manager/remote-bitfield.d.ts.map +1 -0
- package/dist/core-ownership.d.ts +112 -0
- package/dist/core-ownership.d.ts.map +1 -0
- package/dist/datastore/index.d.ts +91 -0
- package/dist/datastore/index.d.ts.map +1 -0
- package/dist/datatype/index.d.ts +108 -0
- package/dist/discovery/local-discovery.d.ts +64 -0
- package/dist/discovery/local-discovery.d.ts.map +1 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/fastify-controller.d.ts +27 -0
- package/dist/fastify-controller.d.ts.map +1 -0
- package/dist/fastify-plugins/blobs.d.ts +6 -0
- package/dist/fastify-plugins/blobs.d.ts.map +1 -0
- package/dist/fastify-plugins/constants.d.ts +3 -0
- package/dist/fastify-plugins/constants.d.ts.map +1 -0
- package/dist/fastify-plugins/icons.d.ts +6 -0
- package/dist/fastify-plugins/icons.d.ts.map +1 -0
- package/dist/fastify-plugins/maps/index.d.ts +11 -0
- package/dist/fastify-plugins/maps/index.d.ts.map +1 -0
- package/dist/fastify-plugins/maps/offline-fallback-map.d.ts +12 -0
- package/dist/fastify-plugins/maps/offline-fallback-map.d.ts.map +1 -0
- package/dist/fastify-plugins/maps/static-maps.d.ts +11 -0
- package/dist/fastify-plugins/maps/static-maps.d.ts.map +1 -0
- package/dist/fastify-plugins/utils.d.ts +23 -0
- package/dist/fastify-plugins/utils.d.ts.map +1 -0
- package/dist/generated/extensions.d.ts +44 -0
- package/dist/generated/extensions.d.ts.map +1 -0
- package/dist/generated/keys.d.ts +36 -0
- package/dist/generated/keys.d.ts.map +1 -0
- package/dist/generated/rpc.d.ts +87 -0
- package/dist/generated/rpc.d.ts.map +1 -0
- package/dist/icon-api.d.ts +109 -0
- package/dist/icon-api.d.ts.map +1 -0
- package/dist/index-writer/index.d.ts +51 -0
- package/dist/index-writer/index.d.ts.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/invite-api.d.ts +70 -0
- package/dist/invite-api.d.ts.map +1 -0
- package/dist/lib/hashmap.d.ts +62 -0
- package/dist/lib/hashmap.d.ts.map +1 -0
- package/dist/lib/hypercore-helpers.d.ts +6 -0
- package/dist/lib/hypercore-helpers.d.ts.map +1 -0
- package/dist/lib/noise-secret-stream-helpers.d.ts +45 -0
- package/dist/lib/noise-secret-stream-helpers.d.ts.map +1 -0
- package/dist/lib/ponyfills.d.ts +10 -0
- package/dist/lib/ponyfills.d.ts.map +1 -0
- package/dist/lib/string.d.ts +2 -0
- package/dist/lib/string.d.ts.map +1 -0
- package/dist/lib/timing-safe-equal.d.ts +15 -0
- package/dist/lib/timing-safe-equal.d.ts.map +1 -0
- package/dist/local-peers.d.ts +151 -0
- package/dist/local-peers.d.ts.map +1 -0
- package/dist/logger.d.ts +32 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/mapeo-manager.d.ts +178 -0
- package/dist/mapeo-manager.d.ts.map +1 -0
- package/dist/mapeo-project.d.ts +3233 -0
- package/dist/mapeo-project.d.ts.map +1 -0
- package/dist/member-api.d.ts +114 -0
- package/dist/member-api.d.ts.map +1 -0
- package/dist/roles.d.ts +157 -0
- package/dist/roles.d.ts.map +1 -0
- package/dist/schema/client.d.ts +284 -0
- package/dist/schema/client.d.ts.map +1 -0
- package/dist/schema/project.d.ts +1812 -0
- package/dist/schema/project.d.ts.map +1 -0
- package/dist/schema/schema-to-drizzle.d.ts +20 -0
- package/dist/schema/schema-to-drizzle.d.ts.map +1 -0
- package/dist/schema/types.d.ts +98 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/utils.d.ts +55 -0
- package/dist/schema/utils.d.ts.map +1 -0
- package/dist/sync/core-sync-state.d.ts +252 -0
- package/dist/sync/core-sync-state.d.ts.map +1 -0
- package/dist/sync/namespace-sync-state.d.ts +47 -0
- package/dist/sync/namespace-sync-state.d.ts.map +1 -0
- package/dist/sync/peer-sync-controller.d.ts +44 -0
- package/dist/sync/peer-sync-controller.d.ts.map +1 -0
- package/dist/sync/sync-api.d.ts +158 -0
- package/dist/sync/sync-api.d.ts.map +1 -0
- package/dist/sync/sync-state.d.ts +40 -0
- package/dist/sync/sync-state.d.ts.map +1 -0
- package/dist/translation-api.d.ts +288 -0
- package/dist/translation-api.d.ts.map +1 -0
- package/dist/types.d.ts +115 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils.d.ts +115 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils_types.d.ts +14 -0
- package/drizzle/client/0000_bumpy_carnage.sql +33 -0
- package/drizzle/client/meta/0000_snapshot.json +199 -0
- package/drizzle/client/meta/_journal.json +13 -0
- package/drizzle/project/0000_spooky_lady_ursula.sql +192 -0
- package/drizzle/project/meta/0000_snapshot.json +1137 -0
- package/drizzle/project/meta/_journal.json +13 -0
- package/package.json +202 -0
- package/src/blob-api.js +139 -0
- package/src/blob-store/index.js +325 -0
- package/src/blob-store/live-download.js +373 -0
- package/src/config-import.js +604 -0
- package/src/constants.js +34 -0
- package/src/core-manager/bitfield-rle.js +235 -0
- package/src/core-manager/core-index.js +87 -0
- package/src/core-manager/index.js +504 -0
- package/src/core-manager/random-access-file-pool.js +30 -0
- package/src/core-manager/remote-bitfield.js +416 -0
- package/src/core-ownership.js +235 -0
- package/src/datastore/README.md +46 -0
- package/src/datastore/index.js +234 -0
- package/src/datatype/README.md +33 -0
- package/src/datatype/index.d.ts +108 -0
- package/src/datatype/index.js +358 -0
- package/src/discovery/local-discovery.js +303 -0
- package/src/errors.js +5 -0
- package/src/fastify-controller.js +84 -0
- package/src/fastify-plugins/blobs.js +139 -0
- package/src/fastify-plugins/constants.js +5 -0
- package/src/fastify-plugins/icons.js +158 -0
- package/src/fastify-plugins/maps/index.js +173 -0
- package/src/fastify-plugins/maps/offline-fallback-map.js +114 -0
- package/src/fastify-plugins/maps/static-maps.js +271 -0
- package/src/fastify-plugins/utils.js +52 -0
- package/src/generated/README.md +3 -0
- package/src/generated/extensions.d.ts +44 -0
- package/src/generated/extensions.js +196 -0
- package/src/generated/extensions.ts +237 -0
- package/src/generated/keys.d.ts +36 -0
- package/src/generated/keys.js +148 -0
- package/src/generated/keys.ts +185 -0
- package/src/generated/rpc.d.ts +87 -0
- package/src/generated/rpc.js +389 -0
- package/src/generated/rpc.ts +463 -0
- package/src/icon-api.js +282 -0
- package/src/index-writer/README.md +38 -0
- package/src/index-writer/index.js +124 -0
- package/src/index.js +16 -0
- package/src/invite-api.js +450 -0
- package/src/lib/hashmap.js +91 -0
- package/src/lib/hypercore-helpers.js +18 -0
- package/src/lib/noise-secret-stream-helpers.js +37 -0
- package/src/lib/ponyfills.js +25 -0
- package/src/lib/string.js +7 -0
- package/src/lib/timing-safe-equal.js +34 -0
- package/src/local-peers.js +737 -0
- package/src/logger.js +99 -0
- package/src/mapeo-manager.js +914 -0
- package/src/mapeo-project.js +980 -0
- package/src/member-api.js +319 -0
- package/src/roles.js +412 -0
- package/src/schema/client.js +55 -0
- package/src/schema/project.js +44 -0
- package/src/schema/schema-to-drizzle.js +118 -0
- package/src/schema/types.ts +153 -0
- package/src/schema/utils.js +51 -0
- package/src/sync/core-sync-state.js +440 -0
- package/src/sync/namespace-sync-state.js +193 -0
- package/src/sync/peer-sync-controller.js +332 -0
- package/src/sync/sync-api.js +588 -0
- package/src/sync/sync-state.js +63 -0
- package/src/translation-api.js +141 -0
- package/src/types.ts +149 -0
- package/src/utils.js +210 -0
- package/src/utils_types.d.ts +14 -0
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
import yauzl from 'yauzl-promise'
|
|
2
|
+
import { validate, valueSchemas } from '@comapeo/schema'
|
|
3
|
+
import { json, buffer } from 'node:stream/consumers'
|
|
4
|
+
import { assert, isDefined } from './utils.js'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { parse as parseBCP47 } from 'bcp-47'
|
|
7
|
+
import { SUPPORTED_CONFIG_VERSION } from './constants.js'
|
|
8
|
+
|
|
9
|
+
// Throw error if a zipfile contains more than 10,000 entries
|
|
10
|
+
const MAX_ENTRIES = 10_000
|
|
11
|
+
const MAX_ICON_SIZE = 10_000_000
|
|
12
|
+
const ICON_NAME_REGEX = /([a-zA-Z0-9-]+)-([a-zA-Z]+)@(\d+)x\.[a-zA-Z]+$/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {yauzl.Entry} Entry
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {{
|
|
19
|
+
* presets: { [id: string]: unknown }
|
|
20
|
+
* fields: { [id: string]: unknown }
|
|
21
|
+
* }} PresetsFile
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** @typedef {('presets' | 'fields')} ValidDocTypes */
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {{
|
|
27
|
+
* [lang: string]: unknown
|
|
28
|
+
* }} TranslationsFile
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** @typedef {NonNullable<import('@comapeo/schema').ProjectSettingsValue['configMetadata']>} MetadataFile */
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {Parameters<import('./icon-api.js').IconApi['create']>[0]} IconData
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} configPath
|
|
39
|
+
*/
|
|
40
|
+
export async function readConfig(configPath) {
|
|
41
|
+
/** @type {Error[]} */
|
|
42
|
+
const warnings = []
|
|
43
|
+
const importDate = new Date().toISOString()
|
|
44
|
+
|
|
45
|
+
const zip = await yauzl.open(configPath)
|
|
46
|
+
if (zip.entryCount > MAX_ENTRIES) {
|
|
47
|
+
// MAX_ENTRIES in MAC can be inacurrate
|
|
48
|
+
throw new Error(`Zip file contains too many entries. Max is ${MAX_ENTRIES}`)
|
|
49
|
+
}
|
|
50
|
+
const entries = await zip.readEntries(MAX_ENTRIES)
|
|
51
|
+
const presetsFile = await findPresetsFile(entries)
|
|
52
|
+
const translationsFile = await findTranslationsFile(entries)
|
|
53
|
+
const metadataFile = await findMetadataFile(entries)
|
|
54
|
+
const iconEntries = getIconEntries(entries)
|
|
55
|
+
assert(
|
|
56
|
+
isValidConfigFile(metadataFile),
|
|
57
|
+
`invalid or missing config file version ${metadataFile.fileVersion}. We support version ${SUPPORTED_CONFIG_VERSION}}`
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
get warnings() {
|
|
62
|
+
return warnings
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
get metadata() {
|
|
66
|
+
return { ...metadataFile, importDate }
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
async close() {
|
|
70
|
+
zip.close()
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @returns {AsyncIterable<IconData>}
|
|
75
|
+
*/
|
|
76
|
+
async *icons() {
|
|
77
|
+
/** @type {IconData | undefined} */
|
|
78
|
+
let icon
|
|
79
|
+
|
|
80
|
+
for (const entry of iconEntries) {
|
|
81
|
+
if (entry.uncompressedSize > MAX_ICON_SIZE) {
|
|
82
|
+
warnings.push(
|
|
83
|
+
new Error(
|
|
84
|
+
`icon ${entry.filename} is bigger than maximum allowed size (10MB) `
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
const buf = await buffer(await entry.openReadStream())
|
|
90
|
+
const iconFilename = entry.filename.replace(/^icons\//, '')
|
|
91
|
+
try {
|
|
92
|
+
const { name, variant } = parseIcon(iconFilename, buf)
|
|
93
|
+
// new icon (first pass)
|
|
94
|
+
if (!icon) {
|
|
95
|
+
icon = {
|
|
96
|
+
name,
|
|
97
|
+
variants: [variant],
|
|
98
|
+
}
|
|
99
|
+
// icon already exists, push new variant
|
|
100
|
+
} else if (icon.name === name) {
|
|
101
|
+
icon.variants.push(variant)
|
|
102
|
+
} else {
|
|
103
|
+
// icon has change
|
|
104
|
+
yield icon
|
|
105
|
+
icon = {
|
|
106
|
+
name,
|
|
107
|
+
variants: [variant],
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
warnings.push(
|
|
112
|
+
err instanceof Error
|
|
113
|
+
? err
|
|
114
|
+
: new Error('Unknown error importing icon')
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (icon) {
|
|
119
|
+
yield icon
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @returns {Iterable<{ name: string, value: import('@comapeo/schema').FieldValue }>}
|
|
125
|
+
*/
|
|
126
|
+
*fields() {
|
|
127
|
+
const { fields } = presetsFile
|
|
128
|
+
for (const [name, field] of Object.entries(fields)) {
|
|
129
|
+
if (!isRecord(field)) {
|
|
130
|
+
warnings.push(new Error(`Invalid field ${name}`))
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
/** @type {Record<string, unknown>} */
|
|
134
|
+
const fieldValue = {
|
|
135
|
+
schemaName: 'field',
|
|
136
|
+
}
|
|
137
|
+
for (const key of Object.keys(valueSchemas.field.properties)) {
|
|
138
|
+
if (Object.hasOwn(field, key)) {
|
|
139
|
+
fieldValue[key] = field[key]
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!validate('field', fieldValue)) {
|
|
143
|
+
warnings.push(new Error(`Invalid field ${name}`))
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
yield {
|
|
147
|
+
name,
|
|
148
|
+
value: fieldValue,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @returns {Iterable<{ fieldNames: string[], iconName: string | undefined, value: import('@comapeo/schema').PresetValue, name: string}>}
|
|
155
|
+
*/
|
|
156
|
+
*presets() {
|
|
157
|
+
const { presets } = presetsFile
|
|
158
|
+
// sort presets using the sort field, turn them into an array
|
|
159
|
+
/** @type {Array<{preset:Record<string, unknown>, name: String}>} */
|
|
160
|
+
const sortedPresets = []
|
|
161
|
+
for (const [presetName, preset] of Object.entries(presets)) {
|
|
162
|
+
if (isRecord(preset)) {
|
|
163
|
+
sortedPresets.push({ name: presetName, preset })
|
|
164
|
+
} else {
|
|
165
|
+
warnings.push(new Error(`invalid preset ${presetName}`))
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
sortedPresets.sort(({ preset }, { preset: nextPreset }) => {
|
|
169
|
+
const sort = typeof preset.sort === 'number' ? preset.sort : Infinity
|
|
170
|
+
const nextSort =
|
|
171
|
+
typeof nextPreset.sort === 'number' ? nextPreset.sort : Infinity
|
|
172
|
+
return sort - nextSort
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const iconFilenames = new Set(
|
|
176
|
+
iconEntries
|
|
177
|
+
.map((icon) => {
|
|
178
|
+
const matches = path.basename(icon.filename).match(ICON_NAME_REGEX)
|
|
179
|
+
if (matches) {
|
|
180
|
+
const [_, name] = matches
|
|
181
|
+
return name
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
.filter(isDefined)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
// 5. for each preset get the corresponding fieldId and iconId, add them to the db
|
|
188
|
+
for (const { preset, name } of sortedPresets) {
|
|
189
|
+
/** @type {Record<string, unknown>} */
|
|
190
|
+
const presetValue = {
|
|
191
|
+
schemaName: 'preset',
|
|
192
|
+
fieldRefs: [],
|
|
193
|
+
iconRef: { docId: 'placeholder', versionId: 'placeholder' },
|
|
194
|
+
addTags: {},
|
|
195
|
+
removeTags: {},
|
|
196
|
+
terms: [],
|
|
197
|
+
}
|
|
198
|
+
for (const key of Object.keys(valueSchemas.preset.properties)) {
|
|
199
|
+
if (Object.hasOwn(preset, key)) {
|
|
200
|
+
presetValue[key] = preset[key]
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!('icon' in preset) || typeof preset.icon !== 'string') {
|
|
205
|
+
warnings.push(new Error(`Preset ${preset.name} doesn't have an icon`))
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
if (!iconFilenames.has(preset.icon)) {
|
|
209
|
+
warnings.push(
|
|
210
|
+
new Error(
|
|
211
|
+
`preset references icon with name ${preset.icon} but file doesn't exist`
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!validate('preset', presetValue)) {
|
|
218
|
+
warnings.push(new Error(`Invalid preset ${preset.name}`))
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
221
|
+
yield {
|
|
222
|
+
fieldNames:
|
|
223
|
+
'fields' in preset && Array.isArray(preset.fields)
|
|
224
|
+
? preset.fields
|
|
225
|
+
: [],
|
|
226
|
+
iconName:
|
|
227
|
+
'icon' in preset && typeof preset.icon === 'string'
|
|
228
|
+
? preset.icon
|
|
229
|
+
: undefined,
|
|
230
|
+
value: presetValue,
|
|
231
|
+
name,
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
/** @returns {Iterable<{ name: string, value:Omit<import('@comapeo/schema').TranslationValue, 'docRef'>}>} */
|
|
236
|
+
*translations() {
|
|
237
|
+
if (!translationsFile) return
|
|
238
|
+
for (const [lang, languageTranslations] of Object.entries(
|
|
239
|
+
translationsFile
|
|
240
|
+
)) {
|
|
241
|
+
if (!isRecord(languageTranslations)) {
|
|
242
|
+
throw new Error('invalid language translations object')
|
|
243
|
+
}
|
|
244
|
+
for (const { name, value } of translationsForLanguage(warnings)(
|
|
245
|
+
lang,
|
|
246
|
+
languageTranslations
|
|
247
|
+
)) {
|
|
248
|
+
yield { name, value }
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* @param {ReadonlyArray<Entry>} entries
|
|
257
|
+
* @rejects if the presets file cannot be found or is invalid
|
|
258
|
+
* @returns {Promise<PresetsFile>}
|
|
259
|
+
*/
|
|
260
|
+
async function findPresetsFile(entries) {
|
|
261
|
+
const presetsEntry = entries.find(
|
|
262
|
+
(entry) => entry.filename === 'presets.json'
|
|
263
|
+
)
|
|
264
|
+
assert(presetsEntry, 'Zip file does not contain presets.json')
|
|
265
|
+
|
|
266
|
+
/** @type {unknown} */
|
|
267
|
+
let result
|
|
268
|
+
try {
|
|
269
|
+
result = await json(await presetsEntry.openReadStream())
|
|
270
|
+
} catch (err) {
|
|
271
|
+
throw new Error('Could not parse presets.json')
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
assert(isRecord(result), 'Invalid presets.json file')
|
|
275
|
+
const { presets, fields } = result
|
|
276
|
+
assert(isRecord(presets) && isRecord(fields), 'Invalid presets.json file')
|
|
277
|
+
|
|
278
|
+
return { presets, fields }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @param {ReadonlyArray<Entry>} entries
|
|
283
|
+
* @returns {Promise<TranslationsFile | undefined>}
|
|
284
|
+
*/
|
|
285
|
+
async function findTranslationsFile(entries) {
|
|
286
|
+
const translationEntry = entries.find(
|
|
287
|
+
(entry) => entry.filename === 'translations.json'
|
|
288
|
+
)
|
|
289
|
+
if (!translationEntry) return
|
|
290
|
+
|
|
291
|
+
/** @type {unknown} */
|
|
292
|
+
let result
|
|
293
|
+
try {
|
|
294
|
+
result = await json(await translationEntry.openReadStream())
|
|
295
|
+
} catch (err) {
|
|
296
|
+
throw new Error('Could not parse translations.json')
|
|
297
|
+
}
|
|
298
|
+
assert(isRecord(result), 'Invalid translations.json file')
|
|
299
|
+
return result
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* @param {ReadonlyArray<Entry>} entries
|
|
304
|
+
* @returns {Promise<Omit<MetadataFile, 'importDate'>>}
|
|
305
|
+
*/
|
|
306
|
+
async function findMetadataFile(entries) {
|
|
307
|
+
const metadataEntry = entries.find(
|
|
308
|
+
(entry) => entry.filename === 'metadata.json'
|
|
309
|
+
)
|
|
310
|
+
assert(metadataEntry, 'Zip file does not contain metadata.json')
|
|
311
|
+
let result
|
|
312
|
+
try {
|
|
313
|
+
result = await json(await metadataEntry.openReadStream())
|
|
314
|
+
} catch (err) {
|
|
315
|
+
throw new Error('Could not parse metadata.json')
|
|
316
|
+
}
|
|
317
|
+
assert(isRecord(result), 'Invalid metadata.json file')
|
|
318
|
+
assert(isValidMetadataFile(result), 'Invalid structure of metadata file')
|
|
319
|
+
|
|
320
|
+
return result
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* @param {Error[]} warnings
|
|
325
|
+
*/
|
|
326
|
+
function translationsForLanguage(warnings) {
|
|
327
|
+
/**
|
|
328
|
+
* @param {string} lang
|
|
329
|
+
* @param {Record<string, unknown>} languageTranslations
|
|
330
|
+
*
|
|
331
|
+
*/
|
|
332
|
+
return function* (lang, languageTranslations) {
|
|
333
|
+
const parsed = parseBCP47(lang)
|
|
334
|
+
const { language: languageCode } = parsed
|
|
335
|
+
if (!languageCode) {
|
|
336
|
+
warnings.push(new Error(`invalid translation language ${lang}`))
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
let { region: regionCode } = parsed
|
|
340
|
+
regionCode ||= undefined
|
|
341
|
+
for (const [
|
|
342
|
+
schemaNamePlural,
|
|
343
|
+
languageTranslationsForDocType,
|
|
344
|
+
] of Object.entries(languageTranslations)) {
|
|
345
|
+
// TODO: remove categories check when removed from default config
|
|
346
|
+
if (!(schemaNamePlural === 'fields' || schemaNamePlural === 'presets')) {
|
|
347
|
+
if (schemaNamePlural !== 'categories') {
|
|
348
|
+
warnings.push(new Error(`invalid docRef.type ${schemaNamePlural}`))
|
|
349
|
+
}
|
|
350
|
+
continue
|
|
351
|
+
}
|
|
352
|
+
if (!isRecord(languageTranslationsForDocType)) {
|
|
353
|
+
warnings.push(new Error('invalid translation for docType object'))
|
|
354
|
+
continue
|
|
355
|
+
}
|
|
356
|
+
yield* translationsForDocType(warnings)({
|
|
357
|
+
languageCode,
|
|
358
|
+
regionCode,
|
|
359
|
+
docRefType: schemaNamePluralToDocRefType(schemaNamePlural),
|
|
360
|
+
languageTranslationsForDocType,
|
|
361
|
+
})
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* schemaNames in configs are in plural but in the schemas are in singular
|
|
367
|
+
* @param {ValidDocTypes} schemaNamePlural
|
|
368
|
+
* @returns {import('@comapeo/schema').TranslationValue['docRefType']}
|
|
369
|
+
*/
|
|
370
|
+
function schemaNamePluralToDocRefType(schemaNamePlural) {
|
|
371
|
+
if (schemaNamePlural === 'fields') return 'field'
|
|
372
|
+
if (schemaNamePlural === 'presets') return 'preset'
|
|
373
|
+
throw new Error(
|
|
374
|
+
`invalid schemaNamePlural ${schemaNamePlural} for config import`
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* @param {Error[]} warnings
|
|
380
|
+
*/
|
|
381
|
+
function translationsForDocType(warnings) {
|
|
382
|
+
/** @param {Object} opts
|
|
383
|
+
* @param {string} opts.languageCode
|
|
384
|
+
* @param {string | undefined} opts.regionCode
|
|
385
|
+
* @param {import('@comapeo/schema').TranslationValue['docRefType']} opts.docRefType
|
|
386
|
+
* @param {Record<ValidDocTypes, unknown>} opts.languageTranslationsForDocType
|
|
387
|
+
*/
|
|
388
|
+
return function* ({
|
|
389
|
+
languageCode,
|
|
390
|
+
regionCode,
|
|
391
|
+
docRefType,
|
|
392
|
+
languageTranslationsForDocType,
|
|
393
|
+
}) {
|
|
394
|
+
for (const [docName, fieldsToTranslate] of Object.entries(
|
|
395
|
+
languageTranslationsForDocType
|
|
396
|
+
)) {
|
|
397
|
+
if (!isRecord(fieldsToTranslate)) {
|
|
398
|
+
warnings.push(new Error(`invalid translation field`))
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
yield* translationForValue(warnings)({
|
|
402
|
+
languageCode,
|
|
403
|
+
regionCode,
|
|
404
|
+
docRefType,
|
|
405
|
+
docName,
|
|
406
|
+
fieldsToTranslate,
|
|
407
|
+
})
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* @param {Error[]} warnings
|
|
414
|
+
*/
|
|
415
|
+
function translationForValue(warnings) {
|
|
416
|
+
/**
|
|
417
|
+
* @param {Object} opts
|
|
418
|
+
* @param {string} opts.languageCode
|
|
419
|
+
* @param {string | undefined} opts.regionCode
|
|
420
|
+
* @param {import('@comapeo/schema').TranslationValue['docRefType']} opts.docRefType
|
|
421
|
+
* @param {string} opts.docName
|
|
422
|
+
* @param {Record<string,unknown>} opts.fieldsToTranslate
|
|
423
|
+
*/
|
|
424
|
+
return function* ({
|
|
425
|
+
languageCode,
|
|
426
|
+
regionCode,
|
|
427
|
+
docRefType,
|
|
428
|
+
docName,
|
|
429
|
+
fieldsToTranslate,
|
|
430
|
+
}) {
|
|
431
|
+
for (const [propertyRef, message] of Object.entries(fieldsToTranslate)) {
|
|
432
|
+
let value = {
|
|
433
|
+
/** @type {'translation'} */
|
|
434
|
+
schemaName: 'translation',
|
|
435
|
+
languageCode,
|
|
436
|
+
regionCode,
|
|
437
|
+
docRefType,
|
|
438
|
+
propertyRef: '',
|
|
439
|
+
message: '',
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (isRecord(message) && isFieldOptions(message)) {
|
|
443
|
+
yield* translateMessageObject(warnings)({ value, message, docName })
|
|
444
|
+
} else if (typeof message === 'string') {
|
|
445
|
+
value = { ...value, propertyRef, message }
|
|
446
|
+
yield { name: docName, value }
|
|
447
|
+
} else {
|
|
448
|
+
warnings.push(
|
|
449
|
+
new Error(`Invalid translation message type ${typeof message}`)
|
|
450
|
+
)
|
|
451
|
+
continue
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* @param {Error[]} warnings
|
|
459
|
+
*/
|
|
460
|
+
function translateMessageObject(warnings) {
|
|
461
|
+
/**
|
|
462
|
+
* @param {Object} opts
|
|
463
|
+
* @param {Omit<import('@comapeo/schema').TranslationValue, 'docRef'>} opts.value
|
|
464
|
+
* @param {string} opts.docName
|
|
465
|
+
* @param {Record<string,{label:string,value:string}>} opts.message
|
|
466
|
+
*/
|
|
467
|
+
return function* ({ value, message, docName }) {
|
|
468
|
+
let idx = 0
|
|
469
|
+
for (const translation of Object.values(message)) {
|
|
470
|
+
if (isRecord(translation)) {
|
|
471
|
+
for (const [key, msg] of Object.entries(translation)) {
|
|
472
|
+
if (typeof msg === 'string') {
|
|
473
|
+
value = {
|
|
474
|
+
...value,
|
|
475
|
+
propertyRef: `${value.propertyRef}[${idx}].${key}`,
|
|
476
|
+
message: msg,
|
|
477
|
+
}
|
|
478
|
+
if (
|
|
479
|
+
!validate('translation', {
|
|
480
|
+
...value,
|
|
481
|
+
docRef: { docId: 'placeholder', versionId: 'placeholder' },
|
|
482
|
+
})
|
|
483
|
+
) {
|
|
484
|
+
warnings.push(new Error(`Invalid translation ${value.message}`))
|
|
485
|
+
continue
|
|
486
|
+
}
|
|
487
|
+
yield {
|
|
488
|
+
value,
|
|
489
|
+
name: docName,
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
idx++
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* @param {ReadonlyArray<Entry>} entries
|
|
501
|
+
*/
|
|
502
|
+
function getIconEntries(entries) {
|
|
503
|
+
return entries
|
|
504
|
+
.filter((entry) => entry.filename.match(/^icons\/([^/]+)$/))
|
|
505
|
+
.sort((icon, nextIcon) => icon.filename.localeCompare(nextIcon.filename))
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* @param {string} filename
|
|
510
|
+
* @param {Buffer} buf
|
|
511
|
+
* @returns {{ name: string, variant: IconData['variants'][Number] }}}
|
|
512
|
+
*/
|
|
513
|
+
function parseIcon(filename, buf) {
|
|
514
|
+
const parsedFilename = path.parse(filename)
|
|
515
|
+
const matches = parsedFilename.base.match(ICON_NAME_REGEX)
|
|
516
|
+
if (!matches) {
|
|
517
|
+
throw new Error(`Unexpected icon filename ${filename}`)
|
|
518
|
+
}
|
|
519
|
+
/* eslint-disable no-unused-vars */
|
|
520
|
+
const [_, name, size, pixelDensityStr] = matches
|
|
521
|
+
const pixelDensity = Number(pixelDensityStr)
|
|
522
|
+
if (!(pixelDensity === 1 || pixelDensity === 2 || pixelDensity === 3)) {
|
|
523
|
+
throw new Error(`Error loading icon. invalid pixel density ${pixelDensity}`)
|
|
524
|
+
}
|
|
525
|
+
if (!(size === 'small' || size === 'medium' || size === 'large')) {
|
|
526
|
+
throw new Error(`Error loading icon. invalid size ${size}`)
|
|
527
|
+
}
|
|
528
|
+
if (!name) {
|
|
529
|
+
throw new Error('Error loading icon. missing name')
|
|
530
|
+
}
|
|
531
|
+
/** @type {'image/png' | 'image/svg+xml'} */
|
|
532
|
+
let mimeType
|
|
533
|
+
switch (parsedFilename.ext.toLowerCase()) {
|
|
534
|
+
case '.png':
|
|
535
|
+
mimeType = 'image/png'
|
|
536
|
+
break
|
|
537
|
+
case '.svg':
|
|
538
|
+
mimeType = 'image/svg+xml'
|
|
539
|
+
break
|
|
540
|
+
default:
|
|
541
|
+
throw new Error(`Unexpected icon extension ${parsedFilename.ext}`)
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
name,
|
|
545
|
+
variant: {
|
|
546
|
+
size,
|
|
547
|
+
mimeType,
|
|
548
|
+
pixelDensity,
|
|
549
|
+
blob: buf,
|
|
550
|
+
},
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* @param {Record<string,unknown>} obj
|
|
556
|
+
* @returns {obj is Omit<MetadataFile, 'importDate'>}
|
|
557
|
+
*/
|
|
558
|
+
function isValidMetadataFile(obj) {
|
|
559
|
+
// extra fields are valid
|
|
560
|
+
return (
|
|
561
|
+
'name' in obj &&
|
|
562
|
+
'buildDate' in obj &&
|
|
563
|
+
'fileVersion' in obj &&
|
|
564
|
+
typeof obj['name'] === 'string' &&
|
|
565
|
+
typeof obj['buildDate'] === 'string' &&
|
|
566
|
+
typeof obj['fileVersion'] === 'string'
|
|
567
|
+
)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* @param {Record<string, unknown>} message
|
|
572
|
+
* @returns {message is Record<string,{label:string, value:string}>}
|
|
573
|
+
*/
|
|
574
|
+
function isFieldOptions(message) {
|
|
575
|
+
return Object.values(message).every(
|
|
576
|
+
(val) => isRecord(val) && 'label' in val && 'value' in val
|
|
577
|
+
)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* @param {unknown} value
|
|
582
|
+
* @returns {value is Record<string, unknown>}
|
|
583
|
+
*/
|
|
584
|
+
function isRecord(value) {
|
|
585
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* @param {Object} obj
|
|
590
|
+
* @param {string | undefined} [obj.fileVersion]
|
|
591
|
+
* @returns {boolean}
|
|
592
|
+
*/
|
|
593
|
+
function isValidConfigFile({ fileVersion }) {
|
|
594
|
+
if (!fileVersion) return false
|
|
595
|
+
const regex = /^(\d+)\.(\d+)$/
|
|
596
|
+
const match = fileVersion.match(regex)
|
|
597
|
+
|
|
598
|
+
if (!match) return false
|
|
599
|
+
|
|
600
|
+
const major = parseInt(match[1], 10)
|
|
601
|
+
//const minor = parseInt(match[2], 10)
|
|
602
|
+
|
|
603
|
+
return major >= SUPPORTED_CONFIG_VERSION
|
|
604
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** @import { Namespace } from './types.js' */
|
|
2
|
+
|
|
3
|
+
// WARNING: Changing these will break things for existing apps, since namespaces
|
|
4
|
+
// are used for key derivation
|
|
5
|
+
export const NAMESPACES = /** @type {const} */ ([
|
|
6
|
+
'auth',
|
|
7
|
+
'config',
|
|
8
|
+
'data',
|
|
9
|
+
'blobIndex',
|
|
10
|
+
'blob',
|
|
11
|
+
])
|
|
12
|
+
|
|
13
|
+
/** @type {ReadonlyArray<Namespace>} */
|
|
14
|
+
export const PRESYNC_NAMESPACES = ['auth', 'config', 'blobIndex']
|
|
15
|
+
|
|
16
|
+
/** @type {ReadonlyArray<Namespace>} */
|
|
17
|
+
export const DATA_NAMESPACES = NAMESPACES.filter(
|
|
18
|
+
(namespace) => !PRESYNC_NAMESPACES.includes(namespace)
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
export const NAMESPACE_SCHEMAS = /** @type {const} */ ({
|
|
22
|
+
data: ['observation', 'track'],
|
|
23
|
+
config: [
|
|
24
|
+
'translation',
|
|
25
|
+
'preset',
|
|
26
|
+
'field',
|
|
27
|
+
'projectSettings',
|
|
28
|
+
'deviceInfo',
|
|
29
|
+
'icon',
|
|
30
|
+
],
|
|
31
|
+
auth: ['coreOwnership', 'role'],
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export const SUPPORTED_CONFIG_VERSION = 1
|