@comapeo/core 4.3.0 → 5.0.0-next.3

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 (77) hide show
  1. package/dist/blob-store/downloader.d.ts +5 -2
  2. package/dist/blob-store/downloader.d.ts.map +1 -1
  3. package/dist/constants.d.ts +0 -1
  4. package/dist/constants.d.ts.map +1 -1
  5. package/dist/core-ownership.d.ts +2 -6
  6. package/dist/core-ownership.d.ts.map +1 -1
  7. package/dist/datatype/index.d.ts +30 -30
  8. package/dist/datatype/index.d.ts.map +1 -1
  9. package/dist/discovery/local-discovery.d.ts.map +1 -1
  10. package/dist/import-categories.d.ts +19 -0
  11. package/dist/import-categories.d.ts.map +1 -0
  12. package/dist/intl/iso639.d.ts +4 -0
  13. package/dist/intl/iso639.d.ts.map +1 -0
  14. package/dist/intl/parse-bcp-47.d.ts +22 -0
  15. package/dist/intl/parse-bcp-47.d.ts.map +1 -0
  16. package/dist/invite/invite-api.d.ts.map +1 -1
  17. package/dist/lib/drizzle-helpers.d.ts +19 -1
  18. package/dist/lib/drizzle-helpers.d.ts.map +1 -1
  19. package/dist/mapeo-manager.d.ts +15 -9
  20. package/dist/mapeo-manager.d.ts.map +1 -1
  21. package/dist/mapeo-project.d.ts +4969 -3017
  22. package/dist/mapeo-project.d.ts.map +1 -1
  23. package/dist/schema/client.d.ts +246 -232
  24. package/dist/schema/client.d.ts.map +1 -1
  25. package/dist/schema/comapeo-to-drizzle.d.ts +65 -0
  26. package/dist/schema/comapeo-to-drizzle.d.ts.map +1 -0
  27. package/dist/schema/json-schema-to-drizzle.d.ts +18 -0
  28. package/dist/schema/json-schema-to-drizzle.d.ts.map +1 -0
  29. package/dist/schema/project.d.ts +2711 -1835
  30. package/dist/schema/project.d.ts.map +1 -1
  31. package/dist/schema/types.d.ts +73 -66
  32. package/dist/schema/types.d.ts.map +1 -1
  33. package/dist/translation-api.d.ts +112 -192
  34. package/dist/translation-api.d.ts.map +1 -1
  35. package/dist/types.d.ts +9 -9
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/utils.d.ts +25 -3
  38. package/dist/utils.d.ts.map +1 -1
  39. package/drizzle/client/0004_glorious_shape.sql +1 -0
  40. package/drizzle/client/meta/0000_snapshot.json +13 -9
  41. package/drizzle/client/meta/0001_snapshot.json +13 -9
  42. package/drizzle/client/meta/0002_snapshot.json +13 -9
  43. package/drizzle/client/meta/0003_snapshot.json +13 -9
  44. package/drizzle/client/meta/0004_snapshot.json +239 -0
  45. package/drizzle/client/meta/_journal.json +7 -0
  46. package/drizzle/project/meta/0000_snapshot.json +43 -24
  47. package/drizzle/project/meta/0001_snapshot.json +47 -26
  48. package/drizzle/project/meta/0002_snapshot.json +47 -26
  49. package/package.json +17 -9
  50. package/src/constants.js +0 -3
  51. package/src/datatype/index.js +92 -39
  52. package/src/discovery/local-discovery.js +3 -2
  53. package/src/import-categories.js +368 -0
  54. package/src/index-writer/index.js +1 -1
  55. package/src/intl/iso639.js +8118 -0
  56. package/src/intl/parse-bcp-47.js +91 -0
  57. package/src/invite/invite-api.js +2 -0
  58. package/src/lib/drizzle-helpers.js +70 -18
  59. package/src/mapeo-manager.js +138 -88
  60. package/src/mapeo-project.js +72 -229
  61. package/src/roles.js +1 -1
  62. package/src/schema/client.js +22 -28
  63. package/src/schema/comapeo-to-drizzle.js +57 -0
  64. package/src/schema/{schema-to-drizzle.js → json-schema-to-drizzle.js} +25 -25
  65. package/src/schema/project.js +24 -37
  66. package/src/schema/types.ts +138 -99
  67. package/src/translation-api.js +65 -13
  68. package/src/types.ts +11 -20
  69. package/src/utils.js +37 -3
  70. package/dist/config-import.d.ts +0 -74
  71. package/dist/config-import.d.ts.map +0 -1
  72. package/dist/schema/schema-to-drizzle.d.ts +0 -20
  73. package/dist/schema/schema-to-drizzle.d.ts.map +0 -1
  74. package/dist/schema/utils.d.ts +0 -55
  75. package/dist/schema/utils.d.ts.map +0 -1
  76. package/src/config-import.js +0 -603
  77. package/src/schema/utils.js +0 -51
@@ -1,24 +1,24 @@
1
- import { text, integer, real } from 'drizzle-orm/sqlite-core'
1
+ import { text, integer, real, sqliteTable } from 'drizzle-orm/sqlite-core'
2
2
  import { ExhaustivenessError } from '../utils.js'
3
- import { customJson } from './utils.js'
4
- /** @import { MapeoDoc } from '@comapeo/schema' */
5
- /** @import { MapeoDocMap } from '../types.js' */
6
3
 
7
4
  /**
8
- Convert a JSONSchema definition to a Drizzle Columns Map (the parameter for
9
- `sqliteTable()`).
10
-
11
- **NOTE**: The return of this function is _not_ type-checked (it is coerced with
12
- `as`, because it's not possible to type-check what this function is doing), but
13
- the return type _should_ be correct when using this function.
14
- @template {import('./types.js').JSONSchema7WithProps} TSchema
15
- NB: The inline typescript checker often marks this next line as an error, but this seems to be a bug with JSDoc parsing - running `tsc` does not show this as an error.
16
- @template {import('type-fest').Get<TSchema, 'properties.schemaName.const'>} TSchemaName
17
- @template {TSchemaName extends MapeoDoc['schemaName'] ? MapeoDocMap[TSchemaName] : any} TObjectType
18
- @param {TSchema} schema
19
- @returns {import('./types.js').SchemaToDrizzleColumns<TSchema, TObjectType>}
5
+ * @template {{ [ K in keyof TSchema['properties'] ]?: any }} TObjectType
6
+ * @template {import('./types.js').JSONSchema7Object} TSchema
7
+ * @template {string} TTableName
8
+ * @template {Record<string, import('drizzle-orm').ColumnBuilderBase>} TColumnsMap
9
+ * @template {keyof TSchema['properties']} TPrimaryKey
10
+ * @param {TTableName} tableName
11
+ * @param {TSchema} schema
12
+ * @param {object} [opts]
13
+ * @param {TColumnsMap} [opts.additionalColumns]
14
+ * @param {TPrimaryKey} [opts.primaryKey] - Column name to use as primary key, if not specified in schema
15
+ * @returns {import('./types.js').JsonSchemaToDrizzleSqliteTable<TObjectType, TSchema, TTableName, TColumnsMap, TPrimaryKey>}
20
16
  */
21
- export function jsonSchemaToDrizzleColumns(schema) {
17
+ export function jsonSchemaToDrizzleSqliteTable(
18
+ tableName,
19
+ schema,
20
+ { additionalColumns, primaryKey } = {}
21
+ ) {
22
22
  if (schema.type !== 'object' || !schema.properties) {
23
23
  throw new Error('Cannot process JSONSchema as SQL table')
24
24
  }
@@ -46,14 +46,11 @@ export function jsonSchemaToDrizzleColumns(schema) {
46
46
  ? /** @type {[typeof value.const]} */ ([value.const])
47
47
  : undefined
48
48
  columns[key] = text(key, { enum: enumValue })
49
- if (key === 'docId') {
50
- columns[key] = columns[key].primaryKey()
51
- }
52
49
  break
53
50
  }
54
51
  case 'array':
55
52
  case 'object':
56
- columns[key] = customJson(key)
53
+ columns[key] = text(key, { mode: 'json' })
57
54
  break
58
55
  case 'null':
59
56
  // Skip handling this right now
@@ -69,10 +66,13 @@ export function jsonSchemaToDrizzleColumns(schema) {
69
66
  columns[key] = columns[key].default(defaultValue)
70
67
  }
71
68
  }
69
+ if (key === primaryKey) {
70
+ columns[key] = columns[key].primaryKey()
71
+ }
72
72
  }
73
- // Not yet in @comapeo/schema
74
- columns.forks = customJson('forks').notNull()
75
- return /** @type {any} */ (columns)
73
+ return /** @type {any} */ (
74
+ sqliteTable(tableName, { ...columns, ...additionalColumns })
75
+ )
76
76
  }
77
77
 
78
78
  /**
@@ -85,7 +85,7 @@ function getDefault(value) {
85
85
  }
86
86
 
87
87
  /**
88
- * @param {import('./types.js').JSONSchema7WithProps} schema
88
+ * @param {import('./types.js').JSONSchema7Object} schema
89
89
  * @param {string} key
90
90
  * @returns {boolean}
91
91
  */
@@ -3,47 +3,34 @@
3
3
  import { blob, sqliteTable, text } from 'drizzle-orm/sqlite-core'
4
4
  import { dereferencedDocSchemas as schemas } from '@comapeo/schema'
5
5
  import { NAMESPACES } from '../constants.js'
6
- import { jsonSchemaToDrizzleColumns as toColumns } from './schema-to-drizzle.js'
7
- import { backlinkTable } from './utils.js'
6
+ import {
7
+ comapeoSchemaToDrizzleTable as toDrizzle,
8
+ backlinkTable,
9
+ } from './comapeo-to-drizzle.js'
8
10
 
9
- export const translationTable = sqliteTable(
10
- 'translation',
11
- toColumns(schemas.translation)
12
- )
13
- export const observationTable = sqliteTable(
14
- 'observation',
15
- toColumns(schemas.observation)
16
- )
17
- export const trackTable = sqliteTable('track', toColumns(schemas.track))
18
- export const remoteDetectionAlertTable = sqliteTable(
19
- 'remoteDetectionAlert',
20
- toColumns(schemas.remoteDetectionAlert)
21
- )
22
- export const presetTable = sqliteTable('preset', toColumns(schemas.preset))
23
- export const fieldTable = sqliteTable('field', toColumns(schemas.field))
24
- export const coreOwnershipTable = sqliteTable(
25
- 'coreOwnership',
26
- toColumns(schemas.coreOwnership)
27
- )
28
- export const roleTable = sqliteTable('role', toColumns(schemas.role))
29
- export const deviceInfoTable = sqliteTable(
30
- 'deviceInfo',
31
- toColumns(schemas.deviceInfo)
32
- )
33
- export const iconTable = sqliteTable('icon', toColumns(schemas.icon))
11
+ export const translationTable = toDrizzle(schemas.translation)
12
+ export const observationTable = toDrizzle(schemas.observation)
13
+ export const trackTable = toDrizzle(schemas.track)
14
+ export const remoteDetectionAlertTable = toDrizzle(schemas.remoteDetectionAlert)
15
+ export const presetTable = toDrizzle(schemas.preset)
16
+ export const fieldTable = toDrizzle(schemas.field)
17
+ export const coreOwnershipTable = toDrizzle(schemas.coreOwnership)
18
+ export const roleTable = toDrizzle(schemas.role)
19
+ export const deviceInfoTable = toDrizzle(schemas.deviceInfo)
20
+ export const iconTable = toDrizzle(schemas.icon)
34
21
 
35
- export const translationBacklinkTable = backlinkTable(translationTable)
36
- export const observationBacklinkTable = backlinkTable(observationTable)
37
- export const trackBacklinkTable = backlinkTable(trackTable)
22
+ export const translationBacklinkTable = backlinkTable('translation')
23
+ export const observationBacklinkTable = backlinkTable('observation')
24
+ export const trackBacklinkTable = backlinkTable('track')
38
25
  export const remoteDetectionAlertBacklinkTable = backlinkTable(
39
- remoteDetectionAlertTable
26
+ 'remoteDetectionAlert'
40
27
  )
41
- export const presetBacklinkTable = backlinkTable(presetTable)
42
- export const fieldBacklinkTable = backlinkTable(fieldTable)
43
- export const coreOwnershipBacklinkTable = backlinkTable(coreOwnershipTable)
44
- export const roleBacklinkTable = backlinkTable(roleTable)
45
- export const deviceInfoBacklinkTable = backlinkTable(deviceInfoTable)
46
- export const iconBacklinkTable = backlinkTable(iconTable)
28
+ export const presetBacklinkTable = backlinkTable('preset')
29
+ export const fieldBacklinkTable = backlinkTable('field')
30
+ export const coreOwnershipBacklinkTable = backlinkTable('coreOwnership')
31
+ export const roleBacklinkTable = backlinkTable('role')
32
+ export const deviceInfoBacklinkTable = backlinkTable('deviceInfo')
33
+ export const iconBacklinkTable = backlinkTable('icon')
47
34
 
48
35
  export const coresTable = sqliteTable('cores', {
49
36
  publicKey: blob('publicKey', { mode: 'buffer' }).notNull(),
@@ -3,101 +3,86 @@ import {
3
3
  JSONSchema7 as JSONSchema7Writable,
4
4
  JSONSchema7Type,
5
5
  } from 'json-schema'
6
+ import type {
7
+ SQLiteBooleanBuilder,
8
+ SQLiteIntegerBuilder,
9
+ SQLiteRealBuilder,
10
+ SQLiteTableWithColumns,
11
+ SQLiteTextBuilder,
12
+ SQLiteTextJsonBuilder,
13
+ } from 'drizzle-orm/sqlite-core'
14
+ import type {
15
+ $Type,
16
+ BuildColumns,
17
+ ColumnBuilderBase,
18
+ HasDefault,
19
+ IsPrimaryKey,
20
+ NotNull,
21
+ } from 'drizzle-orm'
6
22
 
7
- /** Convert optional properties to nullable */
8
- export type OptionalToNull<T extends {}> = {
9
- [K in keyof T]-?: undefined extends T[K] ? T[K] | null : T[K]
10
- }
11
23
  /** Convert a readonly array/object to writeable */
12
24
  type Writable<T> = { -readonly [P in keyof T]: T[P] }
13
- /** Type returned by text(columnName, { enum: [] }) */
14
- type TextBuilder<
15
- TName extends string,
16
- TEnum extends readonly [string, ...string[]],
17
- TNotNull extends boolean,
18
- THasDefault extends boolean
19
- > = import('drizzle-orm/sqlite-core').SQLiteTextBuilder<{
20
- name: TName
21
- data: Writable<TEnum>[number]
22
- driverParam: string
23
- columnType: 'SQLiteText'
24
- dataType: 'string'
25
- enumValues: Writable<TEnum>
26
- notNull: TNotNull
27
- hasDefault: THasDefault
28
- }>
29
-
30
- /** Type returned by integer(columnName, { mode: 'boolean' }) */
31
- type BooleanBuilder<
32
- TName extends string,
33
- TNotNull extends boolean,
34
- THasDefault extends boolean
35
- > = import('drizzle-orm/sqlite-core').SQLiteBooleanBuilder<{
36
- name: TName
37
- data: boolean
38
- driverParam: number
39
- columnType: 'SQLiteBoolean'
40
- dataType: 'boolean'
41
- notNull: TNotNull
42
- hasDefault: THasDefault
43
- enumValues: undefined
44
- }>
45
-
46
- /** Type returned by real(columnName) */
47
- type RealBuilder<
48
- TName extends string,
49
- TNotNull extends boolean,
50
- THasDefault extends boolean
51
- > = import('drizzle-orm/sqlite-core').SQLiteRealBuilder<{
52
- name: TName
53
- data: number
54
- driverParam: number
55
- columnType: 'SQLiteReal'
56
- dataType: 'number'
57
- notNull: TNotNull
58
- hasDefault: THasDefault
59
- enumValues: undefined
60
- }>
61
-
62
- /** Type returned by integer(columnName) */
63
- type IntegerBuilder<
64
- TName extends string,
65
- TNotNull extends boolean,
66
- THasDefault extends boolean
67
- > = import('drizzle-orm/sqlite-core').SQLiteIntegerBuilder<{
68
- name: TName
69
- data: number
70
- driverParam: number
71
- columnType: 'SQLiteInteger'
72
- dataType: 'number'
73
- notNull: TNotNull
74
- hasDefault: THasDefault
75
- enumValues: undefined
76
- }>
77
-
78
- /** Type returned by the `customJson` custom type */
79
- type JsonBuilder<
80
- TName extends string,
81
- TData extends unknown,
82
- TNotNull extends boolean,
83
- THasDefault extends boolean
84
- > = import('drizzle-orm/sqlite-core').SQLiteCustomColumnBuilder<{
85
- name: TName
86
- data: TData
87
- dataType: 'custom'
88
- driverParam: string
89
- columnType: 'SQLiteCustomColumn'
90
- notNull: TNotNull
91
- hasDefault: THasDefault
92
- enumValues: undefined
93
- }>
94
25
 
95
26
  export type JSONSchema7 = ReadonlyDeep<JSONSchema7Writable>
96
27
  type JsonSchema7Properties = { readonly [K: string]: JSONSchema7 }
97
- export type JSONSchema7WithProps = Omit<JSONSchema7, 'properties'> & {
28
+ export type JSONSchema7Object = Omit<JSONSchema7, 'properties' | 'type'> & {
29
+ readonly type: 'object'
98
30
  readonly properties: JsonSchema7Properties
99
31
  }
100
32
 
33
+ /**
34
+ * Create a Drizzle SQLite table definition from a JSONSchema object. All
35
+ * top-level properties map to SQLite columns, with `required` properties marked
36
+ * as `NOT NULL`, and JSONSchema `default` will map to SQLite defaults.
37
+ *
38
+ * Any properties that are of type `object` or `array` in the JSONSchema will be
39
+ * mapped to a text field, which drizzle will parse and stringify. Types for
40
+ * `object` and `array` properties will be derived from `TObjectType`.
41
+ */
42
+ export type JsonSchemaToDrizzleSqliteTable<
43
+ /** Typescript type for the object defined in the JSONSchema */
44
+ TObjectType extends { [K in keyof TSchema['properties']]?: any },
45
+ /** The JSONSchema object schema */
46
+ TSchema extends JSONSchema7Object,
47
+ /** Name of the table to create */
48
+ TTableName extends string,
49
+ /** Additional columns to add to the table definition (e.g. not defined in JSONSchema ) */
50
+ TColumnsMap extends Record<string, ColumnBuilderBase> = {},
51
+ /** Name of the property to use as primary key */
52
+ TPrimaryKey extends keyof TSchema['properties'] | undefined = undefined
53
+ > = SQLiteTableWithColumns<{
54
+ name: TTableName
55
+ schema: undefined
56
+ columns: BuildColumns<
57
+ TTableName,
58
+ JsonSchemaToDrizzleColumns<TObjectType, TSchema, TPrimaryKey> & TColumnsMap,
59
+ 'sqlite'
60
+ >
61
+ dialect: 'sqlite'
62
+ }>
63
+
64
+ /**
65
+ * Convert a JSONSchema Object Schema to a Drizzle Columns map (e.g. parameter
66
+ * for `sqliteTable()`). All top-level properties map to SQLite columns, with
67
+ * `required` properties marked as `NOT NULL`, and JSONSchema `default` will map
68
+ * to SQLite defaults.
69
+ *
70
+ * Any properties that are of type `object` or `array` in the JSONSchema will be
71
+ * mapped to a text field, which drizzle will parse and stringify. Types for
72
+ * `object` and `array` properties will be derived from `TObjectType`.
73
+ */
74
+ type JsonSchemaToDrizzleColumns<
75
+ TObjectType extends { [K in keyof TSchema['properties']]?: any },
76
+ TSchema extends JSONSchema7Object,
77
+ TPrimaryKey extends keyof TSchema['properties'] | undefined = undefined
78
+ > = AddJSONSchemaDefaults<
79
+ TSchema,
80
+ AddJSONSchemaRequired<
81
+ TSchema,
82
+ SchemaToDrizzleColumnsBase<TSchema, TObjectType>
83
+ >
84
+ >
85
+
101
86
  /** Get the type of a JSONSchema string: array of constants for an enum,
102
87
  otherwise string[]. Strangeness is to convert it into the format expected by
103
88
  drizzle, which results in the correct type for the field from SQLite */
@@ -111,13 +96,12 @@ type Enum<
111
96
  : [string, ...string[]]
112
97
 
113
98
  /** True if JSONSchema object has a default */
114
- type HasDefault<T extends JSONSchema7> = T['default'] extends JSONSchema7Type
115
- ? true
116
- : false
99
+ type HasJSONSchemaDefault<T extends JSONSchema7> =
100
+ T['default'] extends JSONSchema7Type ? true : false
117
101
 
118
102
  /** True if JSONSchema value is required */
119
- type IsRequired<
120
- T extends JSONSchema7WithProps,
103
+ type IsJSONSchemaRequired<
104
+ T extends JSONSchema7Object,
121
105
  U extends string,
122
106
  V extends JSONSchema7['required'] = T['required']
123
107
  > = V extends readonly any[] ? Includes<V, U> : false
@@ -131,23 +115,78 @@ type IsRequired<
131
115
  * stringify. Types for parsed JSON will be derived from MapeoDoc types.
132
116
  */
133
117
  export type SchemaToDrizzleColumns<
134
- T extends JSONSchema7WithProps,
118
+ TSchema extends JSONSchema7Object,
119
+ /** This is the type matching the JSONSchema */
120
+ TObjectType extends { [K in keyof TSchema['properties']]?: any },
121
+ TPrimaryKey extends keyof TSchema['properties'] | undefined = undefined
122
+ > = AddPrimaryKey<
123
+ AddJSONSchemaDefaults<
124
+ TSchema,
125
+ AddJSONSchemaRequired<
126
+ TSchema,
127
+ SchemaToDrizzleColumnsBase<TSchema, TObjectType>
128
+ >
129
+ >,
130
+ TPrimaryKey
131
+ >
132
+
133
+ /**
134
+ * Add `HasDefault` to columns if the JSONSchema has a default for that property
135
+ */
136
+ type AddJSONSchemaDefaults<
137
+ TJSONSchema extends JSONSchema7Object,
138
+ TColumns extends Record<string, ColumnBuilderBase>,
139
+ U extends JsonSchema7Properties = TJSONSchema['properties']
140
+ > = {
141
+ [K in keyof TColumns]: K extends keyof U
142
+ ? HasJSONSchemaDefault<U[K]> extends true
143
+ ? HasDefault<TColumns[K]>
144
+ : TColumns[K]
145
+ : TColumns[K]
146
+ }
147
+
148
+ /**
149
+ * Mark columns as NotNull if they are required in the JSONSchema
150
+ */
151
+ type AddJSONSchemaRequired<
152
+ TJSONSchema extends JSONSchema7Object,
153
+ TColumns extends Record<string, ColumnBuilderBase>
154
+ > = {
155
+ [K in keyof TColumns]: K extends string
156
+ ? IsJSONSchemaRequired<TJSONSchema, K> extends true
157
+ ? NotNull<TColumns[K]>
158
+ : TColumns[K]
159
+ : TColumns[K]
160
+ }
161
+
162
+ type AddPrimaryKey<
163
+ TColumns extends Record<string, ColumnBuilderBase>,
164
+ TKey extends keyof TColumns | undefined
165
+ > = TKey extends string
166
+ ? Omit<TColumns, TKey> & { [K in TKey]: IsPrimaryKey<TColumns[TKey]> }
167
+ : TColumns
168
+
169
+ /**
170
+ * Map JSONSchema object properties to Drizzle column types.
171
+ */
172
+ type SchemaToDrizzleColumnsBase<
173
+ TSchema extends JSONSchema7Object,
135
174
  TObjectType extends { [K in keyof U]?: any },
136
- U extends JsonSchema7Properties = T['properties']
175
+ U extends JsonSchema7Properties = TSchema['properties']
137
176
  > = {
138
177
  [K in keyof U]: K extends string
139
178
  ? U[K]['type'] extends 'string'
140
- ? TextBuilder<K, Enum<U[K]>, IsRequired<T, K>, HasDefault<U[K]>>
179
+ ? SQLiteTextBuilder<Enum<U[K]>>
141
180
  : U[K]['type'] extends 'boolean'
142
- ? BooleanBuilder<K, IsRequired<T, K>, HasDefault<U[K]>>
181
+ ? SQLiteBooleanBuilder
143
182
  : U[K]['type'] extends 'number'
144
- ? RealBuilder<K, IsRequired<T, K>, HasDefault<U[K]>>
183
+ ? SQLiteRealBuilder
145
184
  : U[K]['type'] extends 'integer'
146
- ? IntegerBuilder<K, IsRequired<T, K>, HasDefault<U[K]>>
185
+ ? SQLiteIntegerBuilder
147
186
  : U[K]['type'] extends 'array' | 'object'
148
- ? JsonBuilder<K, TObjectType[K], IsRequired<T, K>, HasDefault<U[K]>>
187
+ ? $Type<SQLiteTextJsonBuilder, TObjectType[K]>
149
188
  : never
150
189
  : never
151
- } & { forks: JsonBuilder<'forks', string[], true, false> }
190
+ } & { forks: $Type<SQLiteTextJsonBuilder, string[]> }
152
191
 
153
192
  export type NonEmptyArray<T> = [T, ...T[]]
@@ -1,9 +1,11 @@
1
- import { and, sql } from 'drizzle-orm'
1
+ import { and, eq, inArray, sql } from 'drizzle-orm'
2
2
  import { kCreateWithDocId, kSelect } from './datatype/index.js'
3
3
  import { deNullify, hashObject } from './utils.js'
4
4
  import { nullIfNotFound } from './errors.js'
5
5
  import { omit } from './lib/omit.js'
6
- /** @import { Translation, TranslationValue } from '@comapeo/schema' */
6
+ import { iso6391To6393, iso6393To6391 } from './intl/iso639.js'
7
+ import { translationTable } from './schema/project.js'
8
+ /** @import { MapeoDoc, Translation, TranslationValue } from '@comapeo/schema' */
7
9
  /** @import { SetOptional } from 'type-fest' */
8
10
 
9
11
  export const ktranslatedLanguageCodeToSchemaNames = Symbol(
@@ -14,6 +16,9 @@ export default class TranslationApi {
14
16
  * TranslationValue['languageCode'],
15
17
  * Set<import('@comapeo/schema/dist/types.js').SchemaName>>} */
16
18
  #translatedLanguageCodeToSchemaNames = new Map()
19
+ // A bug in previous versions meant that translations were stored with ISO
20
+ // 639-1 codes, so we need to handle backwards compatibility for that case.
21
+ #hasLegacyIso6391Translations = false
17
22
  #dataType
18
23
  #indexPromise
19
24
 
@@ -35,7 +40,7 @@ export default class TranslationApi {
35
40
  docs.map((doc) => this.index(doc))
36
41
  })
37
42
  .catch((err) => {
38
- throw new Error(`error loading Translation cache: ${err}`)
43
+ console.error(`error loading Translation cache: ${err}`)
39
44
  })
40
45
  }
41
46
 
@@ -69,19 +74,48 @@ export default class TranslationApi {
69
74
  async get(value) {
70
75
  await this.ready()
71
76
 
72
- const docTypeIsTranslatedToLanguage =
73
- this.#translatedLanguageCodeToSchemaNames
74
- .get(value.languageCode)
75
- ?.has(
76
- /** @type {import('@comapeo/schema/dist/types.js').SchemaName} */ (
77
- value.docRefType
78
- )
77
+ // Allow this API to accept both ISO 639-1 and ISO 639-3 codes for languageCode
78
+ const normalizedLanguageCode =
79
+ value.languageCode.length === 2
80
+ ? iso6391To6393.get(value.languageCode)
81
+ : value.languageCode
82
+ if (!normalizedLanguageCode) return [] // invalid language code
83
+
84
+ const languageCodesToQuery = [normalizedLanguageCode]
85
+
86
+ // A bug in previous versions meant that translations could be stored with
87
+ // ISO 639-1 codes, so we need to query for both in this case by looking up
88
+ // the ISO 639-1 code for the langauge, and then checking whether there are
89
+ // translations for that language (looking up in our in-memory index saves
90
+ // an extra sqlite query when unnecessary)
91
+ if (this.#hasLegacyIso6391Translations) {
92
+ const iso6391LanguageCode =
93
+ value.languageCode.length === 2
94
+ ? value.languageCode
95
+ : iso6393To6391.get(value.languageCode)
96
+ const isTranslationStoredWithIso6391Code =
97
+ iso6391LanguageCode &&
98
+ this.#isTranslated(
99
+ /** @type {MapeoDoc['schemaName']} */
100
+ (value.docRefType),
101
+ iso6391LanguageCode
79
102
  )
103
+ if (isTranslationStoredWithIso6391Code) {
104
+ languageCodesToQuery.push(iso6391LanguageCode)
105
+ }
106
+ }
107
+
108
+ const docTypeIsTranslatedToLanguage =
109
+ this.#isTranslated(
110
+ /** @type {MapeoDoc['schemaName']} */
111
+ (value.docRefType),
112
+ normalizedLanguageCode
113
+ ) || languageCodesToQuery.length > 1
80
114
  if (!docTypeIsTranslatedToLanguage) return []
81
115
 
82
116
  const filters = [
83
- sql`docRefType = ${value.docRefType}`,
84
- sql`languageCode = ${value.languageCode}`,
117
+ eq(translationTable.docRefType, value.docRefType),
118
+ inArray(translationTable.languageCode, languageCodesToQuery),
85
119
  sql`json_extract(docRef, '$.docId') = ${value.docRef.docId}`,
86
120
  ]
87
121
 
@@ -94,7 +128,9 @@ export default class TranslationApi {
94
128
  filters.push(sql`propertyRef = ${value.propertyRef}`)
95
129
  }
96
130
  if (value.regionCode) {
97
- filters.push(sql`regionCode = ${value.regionCode}`)
131
+ // Use COLLATE NOCASE for case-insensitive matching because in previous
132
+ // versions we did not normalize regionCode to uppercase.
133
+ filters.push(sql`regionCode = ${value.regionCode} COLLATE NOCASE`)
98
134
  }
99
135
 
100
136
  return (await this.#dataType[kSelect]())
@@ -118,6 +154,9 @@ export default class TranslationApi {
118
154
  translatedSchemas
119
155
  )
120
156
  }
157
+ if (doc.languageCode.length === 2) {
158
+ this.#hasLegacyIso6391Translations = true
159
+ }
121
160
  translatedSchemas.add(
122
161
  /** @type {import('@comapeo/schema/dist/types.js').SchemaName} */ (
123
162
  doc.docRefType
@@ -125,6 +164,19 @@ export default class TranslationApi {
125
164
  )
126
165
  }
127
166
 
167
+ /**
168
+ * @param {MapeoDoc['schemaName']} docType
169
+ * @param {string} languageCode
170
+ * @returns {boolean}
171
+ */
172
+ #isTranslated(docType, languageCode) {
173
+ return (
174
+ this.#translatedLanguageCodeToSchemaNames
175
+ .get(languageCode)
176
+ ?.has(docType) || false
177
+ )
178
+ }
179
+
128
180
  // This should only be used by tests.
129
181
  get [ktranslatedLanguageCodeToSchemaNames]() {
130
182
  return this.#translatedLanguageCodeToSchemaNames
package/src/types.ts CHANGED
@@ -74,25 +74,6 @@ export type CoreOwnershipWithSignaturesValue = Omit<
74
74
  Exclude<keyof MapeoCommon, 'schemaName'>
75
75
  >
76
76
 
77
- type NullToOptional<T> = SetOptional<T, NullKeys<T>>
78
- type RemoveNull<T> = {
79
- [K in keyof T]: Exclude<T[K], null>
80
- }
81
-
82
- type NullKeys<Base> = NonNullable<
83
- // Wrap in `NonNullable` to strip away the `undefined` type from the produced union.
84
- {
85
- // Map through all the keys of the given base type.
86
- [Key in keyof Base]: null extends Base[Key] // Pick only keys with types extending the given `Condition` type.
87
- ? // Retain this key since the condition passes.
88
- Key
89
- : // Discard this key since the condition fails.
90
- never
91
-
92
- // Convert the produced object into a union type of the keys which passed the conditional test.
93
- }[keyof Base]
94
- >
95
-
96
77
  /**
97
78
  * Replace an object's `Buffer` values with `string`s. Useful for serialization.
98
79
  */
@@ -106,7 +87,17 @@ export type MapBuffers<T> = {
106
87
  * top-level optional props set to `null`) to the original types in
107
88
  * @comapeo/schema
108
89
  */
109
- export type NullableToOptional<T> = Simplify<RemoveNull<NullToOptional<T>>>
90
+ export type NullableToOptional<T> = Simplify<
91
+ {
92
+ [K in keyof T as null extends T[K] ? K : never]?: Exclude<T[K], null>
93
+ } & {
94
+ [K in keyof T as null extends T[K] ? never : K]: T[K]
95
+ }
96
+ >
97
+ export type OptionalToNullable<T> = Simplify<{
98
+ [K in keyof T]-?: T[K] | (undefined extends T[K] ? null : never)
99
+ }>
100
+
110
101
  export type KeyPair = {
111
102
  publicKey: PublicKey
112
103
  secretKey: SecretKey
package/src/utils.js CHANGED
@@ -87,7 +87,6 @@ export function isDefined(value) {
87
87
  * @param {T} obj
88
88
  * @returns {import('./types.js').NullableToOptional<T>}
89
89
  */
90
-
91
90
  export function deNullify(obj) {
92
91
  /** @type {Record<string, any>} */
93
92
  const objNoNulls = {}
@@ -98,9 +97,29 @@ export function deNullify(obj) {
98
97
  }
99
98
 
100
99
  /**
101
- * @template {import('@comapeo/schema').MapeoDoc & { forks?: string[] }} T
100
+ * __Mutating__
101
+ * When reading from SQLite, any optional properties are set to `null`. This
102
+ * converts `null` back to `undefined` to match the input types (e.g. the types
103
+ * defined in @comapeo/schema)
104
+ * @template {{}} T
105
+ * @param {T} obj
106
+ * @returns {import('./types.js').NullableToOptional<T>}
107
+ */
108
+ export function mutatingDeNullify(obj) {
109
+ for (const key of Object.keys(obj)) {
110
+ // @ts-expect-error
111
+ if (obj[key] === null) {
112
+ // @ts-expect-error
113
+ obj[key] = undefined
114
+ }
115
+ }
116
+ return /** @type {import('./types.js').NullableToOptional<T>} */ (obj)
117
+ }
118
+
119
+ /**
120
+ * @template {import('@comapeo/schema').MapeoDoc & { forks?: string[], createdBy?: string, updatedBy?: string }} T
102
121
  * @param {T} doc
103
- * @returns {Omit<T, 'docId' | 'versionId' | 'originalVersionId' | 'links' | 'forks' | 'createdAt' | 'updatedAt' | 'deleted'>}
122
+ * @returns {Omit<T, 'docId' | 'versionId' | 'originalVersionId' | 'links' | 'forks' | 'createdAt' | 'updatedAt' | 'createdBy' | 'updatedBy' | 'deleted' >}
104
123
  */
105
124
  export function valueOf(doc) {
106
125
  return omit(doc, [
@@ -111,6 +130,8 @@ export function valueOf(doc) {
111
130
  'forks',
112
131
  'createdAt',
113
132
  'updatedAt',
133
+ 'createdBy',
134
+ 'updatedBy',
114
135
  'deleted',
115
136
  ])
116
137
  }
@@ -219,3 +240,16 @@ export function buildBlobId(attachment, requestedVariant) {
219
240
  driveId: attachment.driveDiscoveryId,
220
241
  }
221
242
  }
243
+
244
+ /**
245
+ * Get typed entries from an object. Use this only on objects that you are
246
+ * certain have no extra properties - TS does not check for extra properties on
247
+ * an object, which is why Object.entries is untyped by default.
248
+ *
249
+ * @template {Record<string, unknown>} T
250
+ * @param {T} obj - The object to get entries from (must _not_ have extra properties)
251
+ * @returns {import('type-fest').Entries<T>}
252
+ */
253
+ export function typedEntries(obj) {
254
+ return /** @type {import('type-fest').Entries<T>} */ (Object.entries(obj))
255
+ }