create-syncular-app 0.1.2 → 0.2.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/README.md +70 -27
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +131 -179
- package/dist/constants.d.ts +40 -0
- package/dist/constants.js +46 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/scaffold.d.ts +44 -0
- package/dist/scaffold.js +106 -0
- package/package.json +22 -15
- package/src/cli.ts +174 -0
- package/src/constants.ts +53 -0
- package/src/index.ts +3 -0
- package/src/scaffold.ts +161 -0
- package/template/minimal/README.md +45 -0
- package/template/minimal/gitignore +5 -0
- package/template/minimal/migrations/0001_initial/up.sql +9 -0
- package/template/minimal/package.json +22 -0
- package/template/minimal/src/clients.ts +54 -0
- package/template/minimal/src/make-client.ts +26 -0
- package/template/minimal/src/server.ts +39 -0
- package/template/minimal/src/smoke.test.ts +70 -0
- package/template/minimal/src/syncular.generated.ts +66 -0
- package/template/minimal/syncular.ir.json +66 -0
- package/template/minimal/syncular.json +17 -0
- package/template/minimal/tsconfig.json +16 -0
- package/template/web/README.md +63 -0
- package/template/web/gitignore +5 -0
- package/template/web/migrations/0001_initial/up.sql +11 -0
- package/template/web/package.json +22 -0
- package/template/web/src/frontend/index.html +37 -0
- package/template/web/src/frontend/main.ts +191 -0
- package/template/web/src/frontend/worker.ts +9 -0
- package/template/web/src/server.ts +183 -0
- package/template/web/src/smoke.test.ts +91 -0
- package/template/web/src/syncular.generated.ts +74 -0
- package/template/web/syncular.ir.json +76 -0
- package/template/web/syncular.json +17 -0
- package/template/web/tsconfig.json +16 -0
- package/template/README.md +0 -122
- package/template/_gitignore +0 -5
- package/template/generated/kotlin/SyncularApp.kt +0 -1005
- package/template/generated/rust/diesel_tables.rs +0 -142
- package/template/generated/rust/migrations.rs +0 -32
- package/template/generated/rust/schema.rs +0 -15
- package/template/generated/rust/syncular.rs +0 -926
- package/template/generated/swift/SyncularApp.swift +0 -1191
- package/template/generated/syncular.codegen.json +0 -19
- package/template/index.html +0 -13
- package/template/migrations/0001_initial/down.sql +0 -1
- package/template/migrations/0001_initial/up.sql +0 -8
- package/template/package.json +0 -33
- package/template/scripts/dev.ts +0 -42
- package/template/src/app.tsx +0 -231
- package/template/src/client/syncular.ts +0 -61
- package/template/src/generated/syncular.generated.ts +0 -769
- package/template/src/generated/syncular.server.generated.ts +0 -512
- package/template/src/main.tsx +0 -5
- package/template/src/server/sync-server.ts +0 -129
- package/template/src/styles.css +0 -233
- package/template/syncular.app.ts +0 -23
- package/template/syncular.schema.json +0 -145
- package/template/tsconfig.json +0 -18
- package/template/vite.config.ts +0 -9
|
@@ -1,1005 +0,0 @@
|
|
|
1
|
-
// @generated by `cargo run --manifest-path rust/Cargo.toml -p syncular-codegen --`
|
|
2
|
-
// Source: migrations/*.sql and generated Syncular codegen handoff
|
|
3
|
-
|
|
4
|
-
import kotlinx.serialization.json.Json
|
|
5
|
-
import kotlinx.serialization.json.JsonElement
|
|
6
|
-
import kotlinx.serialization.json.JsonNull
|
|
7
|
-
import kotlinx.serialization.json.JsonObject
|
|
8
|
-
import kotlinx.serialization.json.booleanOrNull
|
|
9
|
-
import kotlinx.serialization.json.doubleOrNull
|
|
10
|
-
import kotlinx.serialization.json.jsonArray
|
|
11
|
-
import kotlinx.serialization.json.jsonObject
|
|
12
|
-
import kotlinx.serialization.json.jsonPrimitive
|
|
13
|
-
import kotlinx.serialization.json.longOrNull
|
|
14
|
-
|
|
15
|
-
const val syncularNativeExpectedFfiAbiVersion: Int = 2
|
|
16
|
-
const val syncularNativeExpectedCrateVersion: String = "0.1.0"
|
|
17
|
-
const val syncularNativeGeneratedSchemaVersion: Int = 1
|
|
18
|
-
|
|
19
|
-
const val syncularNativeGeneratedAppSchemaJson: String = "{\"clientSchemaSupport\":{\"current\":1,\"minSupported\":1,\"supported\":[1]},\"localBaseSchema\":{\"tableSetupSql\":[\"CREATE TABLE IF NOT EXISTS \\\"tasks\\\" (\\n \\\"id\\\" TEXT PRIMARY KEY,\\n \\\"title\\\" TEXT NOT NULL,\\n \\\"completed\\\" INTEGER NOT NULL DEFAULT 0,\\n \\\"user_id\\\" TEXT NOT NULL,\\n \\\"created_at\\\" INTEGER NOT NULL DEFAULT 0,\\n \\\"server_version\\\" INTEGER NOT NULL DEFAULT 0\\n) WITHOUT ROWID\"]},\"migrations\":[{\"name\":\"initial\",\"schemaVersion\":1,\"upSql\":\"CREATE TABLE IF NOT EXISTS tasks (\\n id TEXT PRIMARY KEY,\\n title TEXT NOT NULL,\\n completed INTEGER NOT NULL DEFAULT 0,\\n user_id TEXT NOT NULL,\\n created_at BIGINT NOT NULL DEFAULT 0,\\n server_version BIGINT NOT NULL DEFAULT 0\\n) WITHOUT ROWID;\",\"version\":\"0001\"}],\"schemaVersion\":1,\"tables\":[{\"blobColumns\":[],\"columns\":[{\"name\":\"id\",\"notnullRequired\":false,\"primaryKey\":true,\"typeFamily\":\"text\"},{\"name\":\"title\",\"notnullRequired\":true,\"primaryKey\":false,\"typeFamily\":\"text\"},{\"name\":\"completed\",\"notnullRequired\":true,\"primaryKey\":false,\"typeFamily\":\"integer\"},{\"name\":\"user_id\",\"notnullRequired\":true,\"primaryKey\":false,\"typeFamily\":\"text\"},{\"name\":\"created_at\",\"notnullRequired\":true,\"primaryKey\":false,\"typeFamily\":\"integer\"},{\"name\":\"server_version\",\"notnullRequired\":true,\"primaryKey\":false,\"typeFamily\":\"integer\"}],\"crdtYjsFields\":[],\"encryptedFields\":[],\"name\":\"tasks\",\"primaryKeyColumn\":\"id\",\"scopes\":[{\"column\":\"user_id\",\"name\":\"user_id\",\"required\":true,\"source\":\"actorId\"}],\"serverVersionColumn\":\"server_version\",\"softDeleteColumn\":null,\"subscriptionId\":\"sub-tasks\"}]}"
|
|
20
|
-
|
|
21
|
-
data class SyncularNativeRuntimeManifest(
|
|
22
|
-
val ffiAbiVersion: Int,
|
|
23
|
-
val crateName: String,
|
|
24
|
-
val crateVersion: String,
|
|
25
|
-
val schemaVersion: Int,
|
|
26
|
-
val storageBackend: String,
|
|
27
|
-
val transportBackends: List<String> = emptyList(),
|
|
28
|
-
val capabilities: List<String> = emptyList(),
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
fun assertSyncularNativeRuntimeManifest(manifest: SyncularNativeRuntimeManifest) {
|
|
32
|
-
require(manifest.ffiAbiVersion == syncularNativeExpectedFfiAbiVersion) { "FFI ABI version ${manifest.ffiAbiVersion} does not match generated expectation $syncularNativeExpectedFfiAbiVersion" }
|
|
33
|
-
require(manifest.crateVersion == syncularNativeExpectedCrateVersion) { "Rust crate version ${manifest.crateVersion} does not match generated expectation $syncularNativeExpectedCrateVersion" }
|
|
34
|
-
require(manifest.storageBackend == "diesel-sqlite") { "Rust storage backend ${manifest.storageBackend} is not diesel-sqlite" }
|
|
35
|
-
require(manifest.capabilities.contains("generated-json-local-operations")) { "Rust native runtime is missing generated-json-local-operations" }
|
|
36
|
-
require(manifest.capabilities.contains("generated-json-mutations")) { "Rust native runtime is missing generated-json-mutations" }
|
|
37
|
-
require(manifest.capabilities.contains("generated-json-leased-mutations")) { "Rust native runtime is missing generated-json-leased-mutations" }
|
|
38
|
-
require(manifest.capabilities.contains("queued-json-leased-mutations")) { "Rust native runtime is missing queued-json-leased-mutations" }
|
|
39
|
-
require(manifest.capabilities.contains("auth-lease-issue")) { "Rust native runtime is missing auth-lease-issue" }
|
|
40
|
-
require(manifest.capabilities.contains("read-only-query-json")) { "Rust native runtime is missing read-only-query-json" }
|
|
41
|
-
require(manifest.capabilities.contains("query-observer-events")) { "Rust native runtime is missing query-observer-events" }
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
enum class SyncularGeneratedOperationKind(val wireValue: String) {
|
|
45
|
-
Upsert("upsert"),
|
|
46
|
-
Delete("delete"),
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
data class SyncularGeneratedOperation(
|
|
50
|
-
val table: String,
|
|
51
|
-
val rowId: String,
|
|
52
|
-
val op: SyncularGeneratedOperationKind,
|
|
53
|
-
val payload: Map<String, Any?>?,
|
|
54
|
-
val baseVersion: Long?,
|
|
55
|
-
) {
|
|
56
|
-
fun toJsonValue(): Map<String, Any?> = linkedMapOf(
|
|
57
|
-
"table" to table,
|
|
58
|
-
"row_id" to rowId,
|
|
59
|
-
"op" to op.wireValue,
|
|
60
|
-
"payload" to payload,
|
|
61
|
-
"base_version" to baseVersion,
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
fun toJsonString(): String = syncularJsonValue(toJsonValue())
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
data class SyncularBlobRef(
|
|
68
|
-
val hash: String,
|
|
69
|
-
val size: Long,
|
|
70
|
-
val mimeType: String,
|
|
71
|
-
val encrypted: Boolean? = null,
|
|
72
|
-
val keyId: String? = null,
|
|
73
|
-
) {
|
|
74
|
-
fun toJsonValue(): Map<String, Any?> {
|
|
75
|
-
val value = linkedMapOf<String, Any?>(
|
|
76
|
-
"hash" to hash,
|
|
77
|
-
"size" to size,
|
|
78
|
-
"mimeType" to mimeType,
|
|
79
|
-
)
|
|
80
|
-
encrypted?.let { value["encrypted"] = it }
|
|
81
|
-
keyId?.let { value["keyId"] = it }
|
|
82
|
-
return value
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
fun toJsonString(): String = syncularJsonValue(toJsonValue())
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
data class SyncularSubscriptionArgs(
|
|
89
|
-
val actorId: String,
|
|
90
|
-
val projectId: String? = null,
|
|
91
|
-
val bootstrapPhases: Map<String, Long> = emptyMap(),
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
private fun syncularBootstrapPhase(args: SyncularSubscriptionArgs, table: String, subscriptionId: String): Long =
|
|
95
|
-
args.bootstrapPhases[subscriptionId] ?: args.bootstrapPhases[table] ?: 0L
|
|
96
|
-
|
|
97
|
-
data class SyncularSubscriptionSpec(
|
|
98
|
-
val id: String,
|
|
99
|
-
val table: String,
|
|
100
|
-
val scopes: Map<String, Any?>,
|
|
101
|
-
val params: Map<String, Any?> = emptyMap(),
|
|
102
|
-
val bootstrapPhase: Long = 0,
|
|
103
|
-
) {
|
|
104
|
-
fun toJsonValue(): Map<String, Any?> = linkedMapOf(
|
|
105
|
-
"id" to id,
|
|
106
|
-
"table" to table,
|
|
107
|
-
"scopes" to scopes,
|
|
108
|
-
"params" to params,
|
|
109
|
-
"bootstrapPhase" to bootstrapPhase,
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
fun toJsonString(): String = syncularJsonValue(toJsonValue())
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
data class SyncularAuthLeaseScope(
|
|
116
|
-
val subscriptionId: String,
|
|
117
|
-
val table: String,
|
|
118
|
-
val values: Map<String, Any?>,
|
|
119
|
-
val operations: List<String>,
|
|
120
|
-
) {
|
|
121
|
-
fun toJsonValue(): Map<String, Any?> = linkedMapOf(
|
|
122
|
-
"subscriptionId" to subscriptionId,
|
|
123
|
-
"table" to table,
|
|
124
|
-
"values" to values,
|
|
125
|
-
"operations" to operations,
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
fun toJsonString(): String = syncularJsonValue(toJsonValue())
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
data class SyncularAuthLeaseIssueRequest(
|
|
132
|
-
val schemaVersion: Int = syncularNativeGeneratedSchemaVersion,
|
|
133
|
-
val ttlMs: Long? = null,
|
|
134
|
-
val scopes: List<SyncularAuthLeaseScope>,
|
|
135
|
-
) {
|
|
136
|
-
fun toJsonValue(): Map<String, Any?> = linkedMapOf(
|
|
137
|
-
"schemaVersion" to schemaVersion,
|
|
138
|
-
"ttlMs" to ttlMs,
|
|
139
|
-
"scopes" to scopes.map { it.toJsonValue() },
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
fun toJsonString(): String = syncularJsonValue(toJsonValue())
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
data class SyncularAuthLeaseRecord(
|
|
146
|
-
val leaseId: String,
|
|
147
|
-
val kid: String,
|
|
148
|
-
val actorId: String,
|
|
149
|
-
val issuedAtMs: Long,
|
|
150
|
-
val notBeforeMs: Long,
|
|
151
|
-
val expiresAtMs: Long,
|
|
152
|
-
val schemaVersion: Int,
|
|
153
|
-
val payloadJson: String,
|
|
154
|
-
val token: String,
|
|
155
|
-
val status: String,
|
|
156
|
-
val lastValidationError: String? = null,
|
|
157
|
-
val createdAtMs: Long,
|
|
158
|
-
val updatedAtMs: Long,
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
fun syncularDecodeAuthLeaseRecord(recordJson: String): SyncularAuthLeaseRecord {
|
|
162
|
-
val value = syncularGeneratedJson.parseToJsonElement(recordJson).jsonObject
|
|
163
|
-
return SyncularAuthLeaseRecord(
|
|
164
|
-
leaseId = value.syncularRequiredString("leaseId"),
|
|
165
|
-
kid = value.syncularRequiredString("kid"),
|
|
166
|
-
actorId = value.syncularRequiredString("actorId"),
|
|
167
|
-
issuedAtMs = value.syncularRequiredLong("issuedAtMs"),
|
|
168
|
-
notBeforeMs = value.syncularRequiredLong("notBeforeMs"),
|
|
169
|
-
expiresAtMs = value.syncularRequiredLong("expiresAtMs"),
|
|
170
|
-
schemaVersion = value.syncularRequiredLong("schemaVersion").toInt(),
|
|
171
|
-
payloadJson = value.syncularRequiredString("payloadJson"),
|
|
172
|
-
token = value.syncularRequiredString("token"),
|
|
173
|
-
status = value.syncularRequiredString("status"),
|
|
174
|
-
lastValidationError = value.syncularOptionalString("lastValidationError"),
|
|
175
|
-
createdAtMs = value.syncularRequiredLong("createdAtMs"),
|
|
176
|
-
updatedAtMs = value.syncularRequiredLong("updatedAtMs"),
|
|
177
|
-
)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
fun syncularSubscriptionsJson(subscriptions: List<SyncularSubscriptionSpec>): String =
|
|
181
|
-
syncularJsonValue(subscriptions.map { it.toJsonValue() })
|
|
182
|
-
|
|
183
|
-
fun syncularDefaultSubscriptionsJson(actorId: String, projectId: String? = null, bootstrapPhases: Map<String, Long> = emptyMap()): String =
|
|
184
|
-
syncularSubscriptionsJson(syncularDefaultSubscriptions(SyncularSubscriptionArgs(actorId = actorId, projectId = projectId, bootstrapPhases = bootstrapPhases)))
|
|
185
|
-
|
|
186
|
-
fun syncularDefaultSubscriptions(args: SyncularSubscriptionArgs): List<SyncularSubscriptionSpec> = listOf(
|
|
187
|
-
taskSubscription(args),
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
fun taskSubscription(args: SyncularSubscriptionArgs): SyncularSubscriptionSpec {
|
|
191
|
-
val scopes = linkedMapOf<String, Any?>()
|
|
192
|
-
scopes["user_id"] = args.actorId
|
|
193
|
-
return SyncularSubscriptionSpec(id = "sub-tasks", table = "tasks", scopes = scopes, params = emptyMap<String, Any?>(), bootstrapPhase = syncularBootstrapPhase(args, "tasks", "sub-tasks"))
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
data class SyncularFieldEncryptionRule(
|
|
197
|
-
val scope: String,
|
|
198
|
-
val table: String?,
|
|
199
|
-
val fields: List<String>,
|
|
200
|
-
val rowIdField: String?,
|
|
201
|
-
) {
|
|
202
|
-
fun toJsonValue(): Map<String, Any?> = linkedMapOf(
|
|
203
|
-
"scope" to scope,
|
|
204
|
-
"table" to table,
|
|
205
|
-
"fields" to fields,
|
|
206
|
-
"rowIdField" to rowIdField,
|
|
207
|
-
).filterValues { it != null }
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
val syncularGeneratedFieldEncryptionRules: List<SyncularFieldEncryptionRule> = listOf(
|
|
211
|
-
)
|
|
212
|
-
|
|
213
|
-
fun syncularGeneratedFieldEncryptionConfigJson(
|
|
214
|
-
keys: Map<String, String>,
|
|
215
|
-
encryptionKid: String? = null,
|
|
216
|
-
decryptionErrorMode: String? = null,
|
|
217
|
-
envelopePrefix: String? = null,
|
|
218
|
-
): String = syncularJsonValue(linkedMapOf(
|
|
219
|
-
"rules" to syncularGeneratedFieldEncryptionRules.map { it.toJsonValue() },
|
|
220
|
-
"keys" to keys,
|
|
221
|
-
"encryptionKid" to encryptionKid,
|
|
222
|
-
"decryptionErrorMode" to decryptionErrorMode,
|
|
223
|
-
"envelopePrefix" to envelopePrefix,
|
|
224
|
-
).filterValues { it != null })
|
|
225
|
-
|
|
226
|
-
data class SyncularReadonlyQuery(
|
|
227
|
-
val sql: String,
|
|
228
|
-
val params: List<Any?> = emptyList(),
|
|
229
|
-
val tables: List<String> = emptyList(),
|
|
230
|
-
) {
|
|
231
|
-
fun toJsonValue(): Map<String, Any?> = linkedMapOf(
|
|
232
|
-
"sql" to sql,
|
|
233
|
-
"params" to params,
|
|
234
|
-
"tables" to tables,
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
fun toJsonString(): String = syncularJsonValue(toJsonValue())
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
data class SyncularLiveQueryRegistration(
|
|
241
|
-
val id: String,
|
|
242
|
-
val tables: List<String>,
|
|
243
|
-
val label: String? = null,
|
|
244
|
-
) {
|
|
245
|
-
fun toJsonValue(): Map<String, Any?> = linkedMapOf(
|
|
246
|
-
"id" to id,
|
|
247
|
-
"tables" to tables,
|
|
248
|
-
"label" to label,
|
|
249
|
-
)
|
|
250
|
-
|
|
251
|
-
fun toJsonString(): String = syncularJsonValue(toJsonValue())
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
data class SyncularChangedCrdtField(
|
|
255
|
-
val field: String,
|
|
256
|
-
val stateColumn: String,
|
|
257
|
-
val containerKey: String,
|
|
258
|
-
val rowIdField: String,
|
|
259
|
-
val kind: String,
|
|
260
|
-
val syncMode: String,
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
data class SyncularChangedRow(
|
|
264
|
-
val table: String,
|
|
265
|
-
val rowId: String? = null,
|
|
266
|
-
val operation: String,
|
|
267
|
-
val changedFields: List<String> = emptyList(),
|
|
268
|
-
val crdtFields: List<String> = emptyList(),
|
|
269
|
-
val crdtFieldChanges: List<SyncularChangedCrdtField> = emptyList(),
|
|
270
|
-
val commitId: String? = null,
|
|
271
|
-
val commitSeq: Long? = null,
|
|
272
|
-
val subscriptionId: String? = null,
|
|
273
|
-
val serverVersion: Long? = null,
|
|
274
|
-
)
|
|
275
|
-
|
|
276
|
-
data class SyncularBootstrapState(
|
|
277
|
-
val asOfCommitSeq: Long = 0,
|
|
278
|
-
val tables: List<String> = emptyList(),
|
|
279
|
-
val tableIndex: Long = 0,
|
|
280
|
-
val rowCursor: String? = null,
|
|
281
|
-
)
|
|
282
|
-
|
|
283
|
-
data class SyncularBootstrapSubscriptionStatus(
|
|
284
|
-
val id: String,
|
|
285
|
-
val table: String,
|
|
286
|
-
val expected: Boolean,
|
|
287
|
-
val ready: Boolean,
|
|
288
|
-
val status: String? = null,
|
|
289
|
-
val phase: String,
|
|
290
|
-
val progressPercent: Long,
|
|
291
|
-
val cursor: Long? = null,
|
|
292
|
-
val bootstrapState: SyncularBootstrapState? = null,
|
|
293
|
-
val bootstrapPhase: Long,
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
data class SyncularBootstrapPhaseStatus(
|
|
297
|
-
val phase: Long,
|
|
298
|
-
val expectedSubscriptionIds: List<String> = emptyList(),
|
|
299
|
-
val readySubscriptionIds: List<String> = emptyList(),
|
|
300
|
-
val pendingSubscriptionIds: List<String> = emptyList(),
|
|
301
|
-
val isReady: Boolean,
|
|
302
|
-
val progressPercent: Long,
|
|
303
|
-
)
|
|
304
|
-
|
|
305
|
-
data class SyncularBootstrapStatus(
|
|
306
|
-
val channelPhase: String,
|
|
307
|
-
val progressPercent: Long,
|
|
308
|
-
val isBootstrapping: Boolean,
|
|
309
|
-
val criticalReady: Boolean,
|
|
310
|
-
val interactiveReady: Boolean,
|
|
311
|
-
val complete: Boolean,
|
|
312
|
-
val activePhase: Long? = null,
|
|
313
|
-
val expectedSubscriptionIds: List<String> = emptyList(),
|
|
314
|
-
val readySubscriptionIds: List<String> = emptyList(),
|
|
315
|
-
val pendingSubscriptionIds: List<String> = emptyList(),
|
|
316
|
-
val subscriptions: List<SyncularBootstrapSubscriptionStatus> = emptyList(),
|
|
317
|
-
val phases: List<SyncularBootstrapPhaseStatus> = emptyList(),
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
data class SyncularNativeErrorInfo(
|
|
321
|
-
val kind: String,
|
|
322
|
-
val code: String,
|
|
323
|
-
val category: String,
|
|
324
|
-
val retryable: Boolean,
|
|
325
|
-
val recommendedAction: String,
|
|
326
|
-
val message: String,
|
|
327
|
-
val debug: String? = null,
|
|
328
|
-
)
|
|
329
|
-
|
|
330
|
-
data class SyncularNativeLifecycleBootstrap(
|
|
331
|
-
val complete: Boolean = false,
|
|
332
|
-
val criticalReady: Boolean = false,
|
|
333
|
-
val interactiveReady: Boolean = false,
|
|
334
|
-
val isBootstrapping: Boolean = false,
|
|
335
|
-
val progressPercent: Long = 0,
|
|
336
|
-
)
|
|
337
|
-
|
|
338
|
-
data class SyncularNativeLifecycleOutbox(
|
|
339
|
-
val pending: Long = 0,
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
data class SyncularNativeLifecycleConflicts(
|
|
343
|
-
val unresolved: Long = 0,
|
|
344
|
-
)
|
|
345
|
-
|
|
346
|
-
data class SyncularNativeLifecycleBlobUploads(
|
|
347
|
-
val pending: Long = 0,
|
|
348
|
-
val uploading: Long = 0,
|
|
349
|
-
val failed: Long = 0,
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
data class SyncularNativeLifecycleState(
|
|
353
|
-
val phase: String,
|
|
354
|
-
val online: Boolean = false,
|
|
355
|
-
val requiresAction: Boolean = false,
|
|
356
|
-
val pendingRequests: Long = 0,
|
|
357
|
-
val bootstrap: SyncularNativeLifecycleBootstrap? = null,
|
|
358
|
-
val outbox: SyncularNativeLifecycleOutbox? = null,
|
|
359
|
-
val conflicts: SyncularNativeLifecycleConflicts? = null,
|
|
360
|
-
val blobUploads: SyncularNativeLifecycleBlobUploads? = null,
|
|
361
|
-
)
|
|
362
|
-
|
|
363
|
-
data class SyncularNativeEvent(
|
|
364
|
-
val eventSeq: Long = 0,
|
|
365
|
-
val kind: String,
|
|
366
|
-
val error: SyncularNativeErrorInfo? = null,
|
|
367
|
-
val tables: List<String> = emptyList(),
|
|
368
|
-
val queries: List<String> = emptyList(),
|
|
369
|
-
val changedRows: List<SyncularChangedRow> = emptyList(),
|
|
370
|
-
val commandId: String? = null,
|
|
371
|
-
val clientCommitId: String? = null,
|
|
372
|
-
val durationMs: Long? = null,
|
|
373
|
-
val droppedCount: Long? = null,
|
|
374
|
-
val bootstrap: SyncularBootstrapStatus? = null,
|
|
375
|
-
val lifecycle: SyncularNativeLifecycleState? = null,
|
|
376
|
-
val resyncRequired: Boolean = false,
|
|
377
|
-
) {
|
|
378
|
-
val eventStreamLost: Boolean get() = kind == "EventsOverflowed" || resyncRequired
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
fun syncularDecodeChangedCrdtField(field: JsonObject): SyncularChangedCrdtField = SyncularChangedCrdtField(
|
|
382
|
-
field = field["field"]?.jsonPrimitive?.content ?: "",
|
|
383
|
-
stateColumn = field["stateColumn"]?.jsonPrimitive?.content ?: "",
|
|
384
|
-
containerKey = field["containerKey"]?.jsonPrimitive?.content ?: "",
|
|
385
|
-
rowIdField = field["rowIdField"]?.jsonPrimitive?.content ?: "",
|
|
386
|
-
kind = field["kind"]?.jsonPrimitive?.content ?: "",
|
|
387
|
-
syncMode = field["syncMode"]?.jsonPrimitive?.content ?: "",
|
|
388
|
-
)
|
|
389
|
-
|
|
390
|
-
fun syncularDecodeChangedRow(row: JsonObject): SyncularChangedRow = SyncularChangedRow(
|
|
391
|
-
table = row["table"]?.jsonPrimitive?.content ?: "",
|
|
392
|
-
rowId = row["rowId"]?.jsonPrimitive?.content,
|
|
393
|
-
operation = row["operation"]?.jsonPrimitive?.content ?: "",
|
|
394
|
-
changedFields = row["changedFields"]?.jsonArray?.map { it.jsonPrimitive.content } ?: emptyList(),
|
|
395
|
-
crdtFields = row["crdtFields"]?.jsonArray?.map { it.jsonPrimitive.content } ?: emptyList(),
|
|
396
|
-
crdtFieldChanges = row["crdtFieldChanges"]?.jsonArray?.map { syncularDecodeChangedCrdtField(it.jsonObject) } ?: emptyList(),
|
|
397
|
-
commitId = row["commitId"]?.jsonPrimitive?.content,
|
|
398
|
-
commitSeq = row["commitSeq"]?.jsonPrimitive?.longOrNull,
|
|
399
|
-
subscriptionId = row["subscriptionId"]?.jsonPrimitive?.content,
|
|
400
|
-
serverVersion = row["serverVersion"]?.jsonPrimitive?.longOrNull,
|
|
401
|
-
)
|
|
402
|
-
|
|
403
|
-
private fun syncularOptionalString(value: JsonElement?): String? =
|
|
404
|
-
if (value == null || value is JsonNull) null else value.jsonPrimitive.content
|
|
405
|
-
|
|
406
|
-
private fun syncularJsonStringList(value: JsonElement?): List<String> =
|
|
407
|
-
if (value == null || value is JsonNull) emptyList() else value.jsonArray.map { it.jsonPrimitive.content }
|
|
408
|
-
|
|
409
|
-
private fun syncularDecodeBootstrapState(value: JsonElement?): SyncularBootstrapState? {
|
|
410
|
-
if (value == null || value is JsonNull) return null
|
|
411
|
-
val state = value.jsonObject
|
|
412
|
-
return SyncularBootstrapState(
|
|
413
|
-
asOfCommitSeq = state["asOfCommitSeq"]?.jsonPrimitive?.longOrNull ?: 0L,
|
|
414
|
-
tables = syncularJsonStringList(state["tables"]),
|
|
415
|
-
tableIndex = state["tableIndex"]?.jsonPrimitive?.longOrNull ?: 0L,
|
|
416
|
-
rowCursor = syncularOptionalString(state["rowCursor"]),
|
|
417
|
-
)
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
private fun syncularDecodeBootstrapSubscriptionStatus(value: JsonElement): SyncularBootstrapSubscriptionStatus {
|
|
421
|
-
val subscription = value.jsonObject
|
|
422
|
-
return SyncularBootstrapSubscriptionStatus(
|
|
423
|
-
id = subscription["id"]?.jsonPrimitive?.content ?: "",
|
|
424
|
-
table = subscription["table"]?.jsonPrimitive?.content ?: "",
|
|
425
|
-
expected = subscription["expected"]?.jsonPrimitive?.booleanOrNull ?: false,
|
|
426
|
-
ready = subscription["ready"]?.jsonPrimitive?.booleanOrNull ?: false,
|
|
427
|
-
status = syncularOptionalString(subscription["status"]),
|
|
428
|
-
phase = subscription["phase"]?.jsonPrimitive?.content ?: "pending",
|
|
429
|
-
progressPercent = subscription["progressPercent"]?.jsonPrimitive?.longOrNull ?: 0L,
|
|
430
|
-
cursor = subscription["cursor"]?.jsonPrimitive?.longOrNull,
|
|
431
|
-
bootstrapState = syncularDecodeBootstrapState(subscription["bootstrapState"]),
|
|
432
|
-
bootstrapPhase = subscription["bootstrapPhase"]?.jsonPrimitive?.longOrNull ?: 0L,
|
|
433
|
-
)
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
private fun syncularDecodeBootstrapPhaseStatus(value: JsonElement): SyncularBootstrapPhaseStatus {
|
|
437
|
-
val phase = value.jsonObject
|
|
438
|
-
return SyncularBootstrapPhaseStatus(
|
|
439
|
-
phase = phase["phase"]?.jsonPrimitive?.longOrNull ?: 0L,
|
|
440
|
-
expectedSubscriptionIds = syncularJsonStringList(phase["expectedSubscriptionIds"]),
|
|
441
|
-
readySubscriptionIds = syncularJsonStringList(phase["readySubscriptionIds"]),
|
|
442
|
-
pendingSubscriptionIds = syncularJsonStringList(phase["pendingSubscriptionIds"]),
|
|
443
|
-
isReady = phase["isReady"]?.jsonPrimitive?.booleanOrNull ?: false,
|
|
444
|
-
progressPercent = phase["progressPercent"]?.jsonPrimitive?.longOrNull ?: 0L,
|
|
445
|
-
)
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
private fun syncularDecodeBootstrapStatus(value: JsonElement?): SyncularBootstrapStatus? {
|
|
449
|
-
if (value == null || value is JsonNull) return null
|
|
450
|
-
val status = value.jsonObject
|
|
451
|
-
return SyncularBootstrapStatus(
|
|
452
|
-
channelPhase = status["channelPhase"]?.jsonPrimitive?.content ?: "idle",
|
|
453
|
-
progressPercent = status["progressPercent"]?.jsonPrimitive?.longOrNull ?: 0L,
|
|
454
|
-
isBootstrapping = status["isBootstrapping"]?.jsonPrimitive?.booleanOrNull ?: false,
|
|
455
|
-
criticalReady = status["criticalReady"]?.jsonPrimitive?.booleanOrNull ?: false,
|
|
456
|
-
interactiveReady = status["interactiveReady"]?.jsonPrimitive?.booleanOrNull ?: false,
|
|
457
|
-
complete = status["complete"]?.jsonPrimitive?.booleanOrNull ?: false,
|
|
458
|
-
activePhase = status["activePhase"]?.jsonPrimitive?.longOrNull,
|
|
459
|
-
expectedSubscriptionIds = syncularJsonStringList(status["expectedSubscriptionIds"]),
|
|
460
|
-
readySubscriptionIds = syncularJsonStringList(status["readySubscriptionIds"]),
|
|
461
|
-
pendingSubscriptionIds = syncularJsonStringList(status["pendingSubscriptionIds"]),
|
|
462
|
-
subscriptions = status["subscriptions"]?.jsonArray?.map { syncularDecodeBootstrapSubscriptionStatus(it) } ?: emptyList(),
|
|
463
|
-
phases = status["phases"]?.jsonArray?.map { syncularDecodeBootstrapPhaseStatus(it) } ?: emptyList(),
|
|
464
|
-
)
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
private fun syncularDecodeNativeErrorInfo(value: JsonElement?): SyncularNativeErrorInfo? {
|
|
468
|
-
if (value == null || value is JsonNull) return null
|
|
469
|
-
val error = value.jsonObject
|
|
470
|
-
return SyncularNativeErrorInfo(
|
|
471
|
-
kind = error["kind"]?.jsonPrimitive?.content ?: "",
|
|
472
|
-
code = error["code"]?.jsonPrimitive?.content ?: "",
|
|
473
|
-
category = error["category"]?.jsonPrimitive?.content ?: "",
|
|
474
|
-
retryable = error["retryable"]?.jsonPrimitive?.booleanOrNull ?: false,
|
|
475
|
-
recommendedAction = error["recommendedAction"]?.jsonPrimitive?.content ?: "",
|
|
476
|
-
message = error["message"]?.jsonPrimitive?.content ?: "",
|
|
477
|
-
debug = syncularOptionalString(error["debug"]),
|
|
478
|
-
)
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
private fun syncularDecodeNativeLifecycleState(value: JsonElement?): SyncularNativeLifecycleState? {
|
|
482
|
-
if (value == null || value is JsonNull) return null
|
|
483
|
-
val state = value.jsonObject
|
|
484
|
-
val bootstrap = state["bootstrap"]?.takeUnless { it is JsonNull }?.jsonObject
|
|
485
|
-
val outbox = state["outbox"]?.takeUnless { it is JsonNull }?.jsonObject
|
|
486
|
-
val conflicts = state["conflicts"]?.takeUnless { it is JsonNull }?.jsonObject
|
|
487
|
-
val blobUploads = state["blobUploads"]?.takeUnless { it is JsonNull }?.jsonObject
|
|
488
|
-
return SyncularNativeLifecycleState(
|
|
489
|
-
phase = state["phase"]?.jsonPrimitive?.content ?: "offline",
|
|
490
|
-
online = state["online"]?.jsonPrimitive?.booleanOrNull ?: false,
|
|
491
|
-
requiresAction = state["requiresAction"]?.jsonPrimitive?.booleanOrNull ?: false,
|
|
492
|
-
pendingRequests = state["pendingRequests"]?.jsonPrimitive?.longOrNull ?: 0L,
|
|
493
|
-
bootstrap = bootstrap?.let { SyncularNativeLifecycleBootstrap(
|
|
494
|
-
complete = it["complete"]?.jsonPrimitive?.booleanOrNull ?: false,
|
|
495
|
-
criticalReady = it["criticalReady"]?.jsonPrimitive?.booleanOrNull ?: false,
|
|
496
|
-
interactiveReady = it["interactiveReady"]?.jsonPrimitive?.booleanOrNull ?: false,
|
|
497
|
-
isBootstrapping = it["isBootstrapping"]?.jsonPrimitive?.booleanOrNull ?: false,
|
|
498
|
-
progressPercent = it["progressPercent"]?.jsonPrimitive?.longOrNull ?: 0L,
|
|
499
|
-
) },
|
|
500
|
-
outbox = outbox?.let { SyncularNativeLifecycleOutbox(pending = it["pending"]?.jsonPrimitive?.longOrNull ?: 0L) },
|
|
501
|
-
conflicts = conflicts?.let { SyncularNativeLifecycleConflicts(unresolved = it["unresolved"]?.jsonPrimitive?.longOrNull ?: 0L) },
|
|
502
|
-
blobUploads = blobUploads?.let { SyncularNativeLifecycleBlobUploads(
|
|
503
|
-
pending = it["pending"]?.jsonPrimitive?.longOrNull ?: 0L,
|
|
504
|
-
uploading = it["uploading"]?.jsonPrimitive?.longOrNull ?: 0L,
|
|
505
|
-
failed = it["failed"]?.jsonPrimitive?.longOrNull ?: 0L,
|
|
506
|
-
) },
|
|
507
|
-
)
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
fun syncularDecodeNativeEvent(eventJson: String): SyncularNativeEvent {
|
|
511
|
-
val event = Json.parseToJsonElement(eventJson).jsonObject
|
|
512
|
-
return SyncularNativeEvent(
|
|
513
|
-
eventSeq = event["event_seq"]?.jsonPrimitive?.longOrNull ?: 0L,
|
|
514
|
-
kind = event["kind"]?.jsonPrimitive?.content ?: "",
|
|
515
|
-
error = syncularDecodeNativeErrorInfo(event["error"]),
|
|
516
|
-
tables = event["tables"]?.jsonArray?.map { it.jsonPrimitive.content } ?: emptyList(),
|
|
517
|
-
queries = event["queries"]?.jsonArray?.map { it.jsonPrimitive.content } ?: emptyList(),
|
|
518
|
-
changedRows = event["changedRows"]?.jsonArray?.map { syncularDecodeChangedRow(it.jsonObject) } ?: emptyList(),
|
|
519
|
-
commandId = event["command_id"]?.jsonPrimitive?.content,
|
|
520
|
-
clientCommitId = event["client_commit_id"]?.jsonPrimitive?.content,
|
|
521
|
-
durationMs = event["duration_ms"]?.jsonPrimitive?.longOrNull,
|
|
522
|
-
droppedCount = event["droppedCount"]?.jsonPrimitive?.longOrNull,
|
|
523
|
-
bootstrap = syncularDecodeBootstrapStatus(event["bootstrap"]),
|
|
524
|
-
lifecycle = syncularDecodeNativeLifecycleState(event["lifecycle"]),
|
|
525
|
-
resyncRequired = event["resyncRequired"]?.jsonPrimitive?.booleanOrNull ?: (event["kind"]?.jsonPrimitive?.content == "EventsOverflowed"),
|
|
526
|
-
)
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
fun syncularNativeEventRequiresFullRefresh(event: SyncularNativeEvent): Boolean =
|
|
530
|
-
event.eventStreamLost
|
|
531
|
-
|
|
532
|
-
interface SyncularNativeEventJsonSource {
|
|
533
|
-
fun forEachEventJson(capacity: ULong = 256uL, handler: (String) -> Boolean)
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
fun SyncularNativeEventJsonSource.forEachNativeEvent(capacity: ULong = 256uL, handler: (SyncularNativeEvent) -> Boolean) {
|
|
537
|
-
forEachEventJson(capacity) { eventJson -> handler(syncularDecodeNativeEvent(eventJson)) }
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
fun syncularDecodeNativeEvents(eventJson: Iterable<String>): List<SyncularNativeEvent> =
|
|
541
|
-
eventJson.map(::syncularDecodeNativeEvent)
|
|
542
|
-
|
|
543
|
-
data class TaskChangedFields(val raw: Set<String>) {
|
|
544
|
-
constructor(fields: List<String>) : this(fields.toSet())
|
|
545
|
-
val id: Boolean = raw.contains("id")
|
|
546
|
-
val title: Boolean = raw.contains("title")
|
|
547
|
-
val completed: Boolean = raw.contains("completed")
|
|
548
|
-
val userId: Boolean = raw.contains("user_id")
|
|
549
|
-
val createdAt: Boolean = raw.contains("created_at")
|
|
550
|
-
val serverVersion: Boolean = raw.contains("server_version")
|
|
551
|
-
|
|
552
|
-
fun contains(column: String): Boolean = when (column) {
|
|
553
|
-
"id" -> id
|
|
554
|
-
"title" -> title
|
|
555
|
-
"completed" -> completed
|
|
556
|
-
"user_id" -> userId
|
|
557
|
-
"created_at" -> createdAt
|
|
558
|
-
"server_version" -> serverVersion
|
|
559
|
-
else -> false
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
data class TaskChangedRow(
|
|
564
|
-
val raw: SyncularChangedRow,
|
|
565
|
-
val changed: TaskChangedFields = TaskChangedFields(raw.changedFields),
|
|
566
|
-
val crdt: TaskChangedFields = TaskChangedFields(raw.crdtFields),
|
|
567
|
-
) {
|
|
568
|
-
val rowId: String? get() = raw.rowId
|
|
569
|
-
val operation: String get() = raw.operation
|
|
570
|
-
val isInsert: Boolean get() = raw.operation == "insert"
|
|
571
|
-
val isUpdate: Boolean get() = raw.operation == "update"
|
|
572
|
-
val isDelete: Boolean get() = raw.operation == "delete"
|
|
573
|
-
|
|
574
|
-
companion object {
|
|
575
|
-
fun from(row: SyncularChangedRow): TaskChangedRow? =
|
|
576
|
-
if (row.table == "tasks") TaskChangedRow(row) else null
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
fun taskChangedRows(rows: List<SyncularChangedRow>): List<TaskChangedRow> =
|
|
581
|
-
rows.mapNotNull { TaskChangedRow.from(it) }
|
|
582
|
-
|
|
583
|
-
fun taskChangedRows(event: SyncularNativeEvent): List<TaskChangedRow> =
|
|
584
|
-
taskChangedRows(event.changedRows)
|
|
585
|
-
|
|
586
|
-
interface SyncularNativeJsonClient {
|
|
587
|
-
fun applyMutationJson(mutationJson: String, localRowJson: String? = null): String
|
|
588
|
-
fun applyLeasedMutationJson(mutationJson: String, localRowJson: String? = null): String
|
|
589
|
-
fun enqueueMutationJson(mutationJson: String, localRowJson: String? = null): String
|
|
590
|
-
fun enqueueLeasedMutationJson(mutationJson: String, localRowJson: String? = null): String
|
|
591
|
-
fun issueAuthLeaseJson(requestJson: String): String
|
|
592
|
-
fun queryJson(requestJson: String): String
|
|
593
|
-
fun registerQueryJson(queryJson: String): String
|
|
594
|
-
fun unregisterQuery(id: String): Boolean
|
|
595
|
-
fun diagnosticSnapshotJson(): String
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
fun SyncularNativeJsonClient.apply(operation: SyncularGeneratedOperation, localRowJson: String? = null): String =
|
|
599
|
-
applyMutationJson(operation.toJsonString(), localRowJson)
|
|
600
|
-
|
|
601
|
-
fun SyncularNativeJsonClient.applyLeased(operation: SyncularGeneratedOperation, localRowJson: String? = null): String =
|
|
602
|
-
applyLeasedMutationJson(operation.toJsonString(), localRowJson)
|
|
603
|
-
|
|
604
|
-
fun SyncularNativeJsonClient.enqueue(operation: SyncularGeneratedOperation, localRowJson: String? = null): String =
|
|
605
|
-
enqueueMutationJson(operation.toJsonString(), localRowJson)
|
|
606
|
-
|
|
607
|
-
fun SyncularNativeJsonClient.enqueueLeased(operation: SyncularGeneratedOperation, localRowJson: String? = null): String =
|
|
608
|
-
enqueueLeasedMutationJson(operation.toJsonString(), localRowJson)
|
|
609
|
-
|
|
610
|
-
fun SyncularNativeJsonClient.issueAuthLease(request: SyncularAuthLeaseIssueRequest): SyncularAuthLeaseRecord =
|
|
611
|
-
syncularDecodeAuthLeaseRecord(issueAuthLeaseJson(request.toJsonString()))
|
|
612
|
-
|
|
613
|
-
fun SyncularNativeJsonClient.diagnosticSnapshot(): JsonObject =
|
|
614
|
-
Json.parseToJsonElement(diagnosticSnapshotJson()).jsonObject
|
|
615
|
-
|
|
616
|
-
fun SyncularNativeJsonClient.query(query: SyncularReadonlyQuery): List<JsonObject> =
|
|
617
|
-
syncularGeneratedQueryRows(queryJson(query.toJsonString()))
|
|
618
|
-
|
|
619
|
-
fun <Row> SyncularNativeJsonClient.query(query: SyncularReadonlyQuery, decode: (JsonObject) -> Row): List<Row> =
|
|
620
|
-
query(query).map(decode)
|
|
621
|
-
|
|
622
|
-
fun SyncularNativeJsonClient.registerLiveQuery(registration: SyncularLiveQueryRegistration): String =
|
|
623
|
-
registerQueryJson(registration.toJsonString())
|
|
624
|
-
|
|
625
|
-
class SyncularNativeLiveQuery<Row>(
|
|
626
|
-
val id: String,
|
|
627
|
-
val query: SyncularReadonlyQuery,
|
|
628
|
-
private val decode: (JsonObject) -> Row,
|
|
629
|
-
val label: String? = null,
|
|
630
|
-
) {
|
|
631
|
-
var rows: List<Row> = emptyList()
|
|
632
|
-
private set
|
|
633
|
-
|
|
634
|
-
fun start(client: SyncularNativeJsonClient): List<Row> {
|
|
635
|
-
client.registerLiveQuery(SyncularLiveQueryRegistration(id = id, tables = query.tables, label = label))
|
|
636
|
-
return refresh(client)
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
fun refresh(client: SyncularNativeJsonClient): List<Row> {
|
|
640
|
-
rows = client.query(query, decode)
|
|
641
|
-
return rows
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
fun stop(client: SyncularNativeJsonClient): Boolean = client.unregisterQuery(id)
|
|
645
|
-
|
|
646
|
-
fun matches(queryIds: Iterable<String>): Boolean = queryIds.any { it == id }
|
|
647
|
-
|
|
648
|
-
fun refreshIfChanged(event: SyncularNativeEvent, client: SyncularNativeJsonClient): List<Row>? {
|
|
649
|
-
if (syncularNativeEventRequiresFullRefresh(event)) return refresh(client)
|
|
650
|
-
if (event.kind != "QueriesChanged" || !matches(event.queries)) return null
|
|
651
|
-
return refresh(client)
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
fun refreshIfChanged(eventJson: String, client: SyncularNativeJsonClient): List<Row>? =
|
|
655
|
-
refreshIfChanged(syncularDecodeNativeEvent(eventJson), client)
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
data class SyncularQueryPredicate(
|
|
659
|
-
val sql: String,
|
|
660
|
-
val params: List<Any?> = emptyList(),
|
|
661
|
-
) {
|
|
662
|
-
infix fun and(other: SyncularQueryPredicate): SyncularQueryPredicate =
|
|
663
|
-
SyncularQueryPredicate(sql = "(($sql) and (${other.sql}))", params = params + other.params)
|
|
664
|
-
|
|
665
|
-
infix fun or(other: SyncularQueryPredicate): SyncularQueryPredicate =
|
|
666
|
-
SyncularQueryPredicate(sql = "(($sql) or (${other.sql}))", params = params + other.params)
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
data class SyncularQueryOrder(
|
|
670
|
-
val sql: String,
|
|
671
|
-
)
|
|
672
|
-
|
|
673
|
-
class SyncularQueryColumn<T>(
|
|
674
|
-
val table: String,
|
|
675
|
-
val name: String,
|
|
676
|
-
) {
|
|
677
|
-
fun eq(value: T): SyncularQueryPredicate =
|
|
678
|
-
SyncularQueryPredicate(sql = "${syncularQuoteIdentifier(name)} = ?", params = listOf(value))
|
|
679
|
-
|
|
680
|
-
fun notEq(value: T): SyncularQueryPredicate =
|
|
681
|
-
SyncularQueryPredicate(sql = "${syncularQuoteIdentifier(name)} != ?", params = listOf(value))
|
|
682
|
-
|
|
683
|
-
fun gt(value: T): SyncularQueryPredicate =
|
|
684
|
-
SyncularQueryPredicate(sql = "${syncularQuoteIdentifier(name)} > ?", params = listOf(value))
|
|
685
|
-
|
|
686
|
-
fun gte(value: T): SyncularQueryPredicate =
|
|
687
|
-
SyncularQueryPredicate(sql = "${syncularQuoteIdentifier(name)} >= ?", params = listOf(value))
|
|
688
|
-
|
|
689
|
-
fun lt(value: T): SyncularQueryPredicate =
|
|
690
|
-
SyncularQueryPredicate(sql = "${syncularQuoteIdentifier(name)} < ?", params = listOf(value))
|
|
691
|
-
|
|
692
|
-
fun lte(value: T): SyncularQueryPredicate =
|
|
693
|
-
SyncularQueryPredicate(sql = "${syncularQuoteIdentifier(name)} <= ?", params = listOf(value))
|
|
694
|
-
|
|
695
|
-
fun isNull(): SyncularQueryPredicate =
|
|
696
|
-
SyncularQueryPredicate(sql = "${syncularQuoteIdentifier(name)} is null")
|
|
697
|
-
|
|
698
|
-
fun isNotNull(): SyncularQueryPredicate =
|
|
699
|
-
SyncularQueryPredicate(sql = "${syncularQuoteIdentifier(name)} is not null")
|
|
700
|
-
|
|
701
|
-
fun isIn(values: Iterable<T>): SyncularQueryPredicate {
|
|
702
|
-
val list = values.toList()
|
|
703
|
-
if (list.isEmpty()) return SyncularQueryPredicate(sql = "0 = 1")
|
|
704
|
-
return SyncularQueryPredicate(sql = "${syncularQuoteIdentifier(name)} in (${list.joinToString(", ") { "?" }})", params = list)
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
fun notIn(values: Iterable<T>): SyncularQueryPredicate {
|
|
708
|
-
val list = values.toList()
|
|
709
|
-
if (list.isEmpty()) return SyncularQueryPredicate(sql = "1 = 1")
|
|
710
|
-
return SyncularQueryPredicate(sql = "${syncularQuoteIdentifier(name)} not in (${list.joinToString(", ") { "?" }})", params = list)
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
fun asc(): SyncularQueryOrder = SyncularQueryOrder("${syncularQuoteIdentifier(name)} asc")
|
|
714
|
-
|
|
715
|
-
fun desc(): SyncularQueryOrder = SyncularQueryOrder("${syncularQuoteIdentifier(name)} desc")
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
class SyncularQueryTable<Row>(
|
|
719
|
-
val name: String,
|
|
720
|
-
val columns: List<String>,
|
|
721
|
-
val decode: (JsonObject) -> Row,
|
|
722
|
-
) {
|
|
723
|
-
fun select(): SyncularSelectQuery<Row> = SyncularSelectQuery(table = this)
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
data class SyncularSelectQuery<Row>(
|
|
727
|
-
val table: SyncularQueryTable<Row>,
|
|
728
|
-
val predicates: List<SyncularQueryPredicate> = emptyList(),
|
|
729
|
-
val orders: List<SyncularQueryOrder> = emptyList(),
|
|
730
|
-
val limitValue: Int? = null,
|
|
731
|
-
) {
|
|
732
|
-
fun filter(predicate: SyncularQueryPredicate): SyncularSelectQuery<Row> =
|
|
733
|
-
copy(predicates = predicates + predicate)
|
|
734
|
-
|
|
735
|
-
fun orderBy(order: SyncularQueryOrder): SyncularSelectQuery<Row> =
|
|
736
|
-
copy(orders = orders + order)
|
|
737
|
-
|
|
738
|
-
fun limit(value: Int): SyncularSelectQuery<Row> = copy(limitValue = value)
|
|
739
|
-
|
|
740
|
-
fun readonlyQuery(): SyncularReadonlyQuery {
|
|
741
|
-
val columnSql = table.columns.joinToString(", ") { syncularQuoteIdentifier(it) }
|
|
742
|
-
val sql = buildString {
|
|
743
|
-
append("select ")
|
|
744
|
-
append(columnSql)
|
|
745
|
-
append(" from ")
|
|
746
|
-
append(syncularQuoteIdentifier(table.name))
|
|
747
|
-
if (predicates.isNotEmpty()) {
|
|
748
|
-
append(" where ")
|
|
749
|
-
append(predicates.joinToString(" and ") { it.sql })
|
|
750
|
-
}
|
|
751
|
-
if (orders.isNotEmpty()) {
|
|
752
|
-
append(" order by ")
|
|
753
|
-
append(orders.joinToString(", ") { it.sql })
|
|
754
|
-
}
|
|
755
|
-
limitValue?.let { append(" limit $it") }
|
|
756
|
-
}
|
|
757
|
-
return SyncularReadonlyQuery(
|
|
758
|
-
sql = sql,
|
|
759
|
-
params = predicates.flatMap { it.params },
|
|
760
|
-
tables = listOf(table.name),
|
|
761
|
-
)
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
fun fetch(client: SyncularNativeJsonClient): List<Row> = client.query(readonlyQuery(), table.decode)
|
|
765
|
-
|
|
766
|
-
fun liveQuery(id: String, label: String? = null): SyncularNativeLiveQuery<Row> =
|
|
767
|
-
SyncularNativeLiveQuery(id = id, query = readonlyQuery(), decode = table.decode, label = label)
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
private fun syncularQuoteIdentifier(identifier: String): String =
|
|
771
|
-
"\"" + identifier.replace("\"", "\"\"") + "\""
|
|
772
|
-
|
|
773
|
-
data class TaskRow(
|
|
774
|
-
val id: String,
|
|
775
|
-
val title: String,
|
|
776
|
-
val completed: Long,
|
|
777
|
-
val userId: String,
|
|
778
|
-
val createdAt: Long,
|
|
779
|
-
val serverVersion: Long,
|
|
780
|
-
)
|
|
781
|
-
|
|
782
|
-
data class NewTask(
|
|
783
|
-
val id: String,
|
|
784
|
-
val title: String,
|
|
785
|
-
val completed: Long? = null,
|
|
786
|
-
val userId: String,
|
|
787
|
-
val createdAt: Long? = null,
|
|
788
|
-
)
|
|
789
|
-
|
|
790
|
-
data class TaskPatch(
|
|
791
|
-
val title: String? = null,
|
|
792
|
-
val completed: Long? = null,
|
|
793
|
-
val userId: String? = null,
|
|
794
|
-
val createdAt: Long? = null,
|
|
795
|
-
)
|
|
796
|
-
|
|
797
|
-
object TaskQuery {
|
|
798
|
-
val table = SyncularQueryTable(name = "tasks", columns = listOf("id", "title", "completed", "user_id", "created_at", "server_version"), decode = ::syncularDecodeTaskRow)
|
|
799
|
-
val id = SyncularQueryColumn<String>(table = "tasks", name = "id")
|
|
800
|
-
val title = SyncularQueryColumn<String>(table = "tasks", name = "title")
|
|
801
|
-
val completed = SyncularQueryColumn<Long>(table = "tasks", name = "completed")
|
|
802
|
-
val userId = SyncularQueryColumn<String>(table = "tasks", name = "user_id")
|
|
803
|
-
val createdAt = SyncularQueryColumn<Long>(table = "tasks", name = "created_at")
|
|
804
|
-
val serverVersion = SyncularQueryColumn<Long>(table = "tasks", name = "server_version")
|
|
805
|
-
fun select(): SyncularSelectQuery<TaskRow> = table.select()
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
object SyncularAppOperations {
|
|
809
|
-
fun newTask(input: NewTask, baseVersion: Long? = 0): SyncularGeneratedOperation {
|
|
810
|
-
val payload = linkedMapOf<String, Any?>()
|
|
811
|
-
payload["title"] = input.title
|
|
812
|
-
payload["completed"] = input.completed ?: 0L
|
|
813
|
-
payload["user_id"] = input.userId
|
|
814
|
-
payload["created_at"] = input.createdAt ?: 0L
|
|
815
|
-
return SyncularGeneratedOperation(
|
|
816
|
-
table = "tasks",
|
|
817
|
-
rowId = input.id,
|
|
818
|
-
op = SyncularGeneratedOperationKind.Upsert,
|
|
819
|
-
payload = payload,
|
|
820
|
-
baseVersion = baseVersion,
|
|
821
|
-
)
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
fun patchTask(rowId: String, patch: TaskPatch, baseVersion: Long? = null): SyncularGeneratedOperation {
|
|
825
|
-
val payload = linkedMapOf<String, Any?>()
|
|
826
|
-
patch.title?.let { payload["title"] = it }
|
|
827
|
-
patch.completed?.let { payload["completed"] = it }
|
|
828
|
-
patch.userId?.let { payload["user_id"] = it }
|
|
829
|
-
patch.createdAt?.let { payload["created_at"] = it }
|
|
830
|
-
return SyncularGeneratedOperation(
|
|
831
|
-
table = "tasks",
|
|
832
|
-
rowId = rowId,
|
|
833
|
-
op = SyncularGeneratedOperationKind.Upsert,
|
|
834
|
-
payload = payload,
|
|
835
|
-
baseVersion = baseVersion,
|
|
836
|
-
)
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
fun deleteTask(rowId: String, baseVersion: Long? = null): SyncularGeneratedOperation = SyncularGeneratedOperation(
|
|
840
|
-
table = "tasks",
|
|
841
|
-
rowId = rowId,
|
|
842
|
-
op = SyncularGeneratedOperationKind.Delete,
|
|
843
|
-
payload = null,
|
|
844
|
-
baseVersion = baseVersion,
|
|
845
|
-
)
|
|
846
|
-
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
class SyncularAppMutations internal constructor(
|
|
850
|
-
private val client: SyncularNativeJsonClient,
|
|
851
|
-
private val queued: Boolean = false,
|
|
852
|
-
private val leased: Boolean = false,
|
|
853
|
-
) {
|
|
854
|
-
val tasks: TaskMutations get() = TaskMutations(client, queued, leased)
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
val SyncularNativeJsonClient.mutations: SyncularAppMutations
|
|
858
|
-
get() = SyncularAppMutations(this)
|
|
859
|
-
|
|
860
|
-
val SyncularNativeJsonClient.queuedMutations: SyncularAppMutations
|
|
861
|
-
get() = SyncularAppMutations(this, queued = true)
|
|
862
|
-
|
|
863
|
-
val SyncularNativeJsonClient.leasedMutations: SyncularAppMutations
|
|
864
|
-
get() = SyncularAppMutations(this, leased = true)
|
|
865
|
-
|
|
866
|
-
val SyncularNativeJsonClient.queuedLeasedMutations: SyncularAppMutations
|
|
867
|
-
get() = SyncularAppMutations(this, queued = true, leased = true)
|
|
868
|
-
|
|
869
|
-
class TaskMutations internal constructor(
|
|
870
|
-
private val client: SyncularNativeJsonClient,
|
|
871
|
-
private val queued: Boolean,
|
|
872
|
-
private val leased: Boolean,
|
|
873
|
-
) {
|
|
874
|
-
fun insert(input: NewTask, baseVersion: Long? = 0, localRowJson: String? = null): String {
|
|
875
|
-
val operation = SyncularAppOperations.newTask(input, baseVersion)
|
|
876
|
-
return when {
|
|
877
|
-
queued && leased -> client.enqueueLeased(operation, localRowJson)
|
|
878
|
-
queued -> client.enqueue(operation, localRowJson)
|
|
879
|
-
leased -> client.applyLeased(operation, localRowJson)
|
|
880
|
-
else -> client.apply(operation, localRowJson)
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
fun update(rowId: String, patch: TaskPatch, baseVersion: Long? = null, localRowJson: String? = null): String {
|
|
885
|
-
val operation = SyncularAppOperations.patchTask(rowId, patch, baseVersion)
|
|
886
|
-
return when {
|
|
887
|
-
queued && leased -> client.enqueueLeased(operation, localRowJson)
|
|
888
|
-
queued -> client.enqueue(operation, localRowJson)
|
|
889
|
-
leased -> client.applyLeased(operation, localRowJson)
|
|
890
|
-
else -> client.apply(operation, localRowJson)
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
fun delete(rowId: String, baseVersion: Long? = null): String {
|
|
895
|
-
val operation = SyncularAppOperations.deleteTask(rowId, baseVersion)
|
|
896
|
-
return when {
|
|
897
|
-
queued && leased -> client.enqueueLeased(operation)
|
|
898
|
-
queued -> client.enqueue(operation)
|
|
899
|
-
leased -> client.applyLeased(operation)
|
|
900
|
-
else -> client.apply(operation)
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
private val syncularGeneratedJson = Json { ignoreUnknownKeys = true }
|
|
906
|
-
|
|
907
|
-
private fun syncularGeneratedRows(json: String): List<JsonObject> =
|
|
908
|
-
syncularGeneratedJson.parseToJsonElement(json).jsonArray.map { it.jsonObject }
|
|
909
|
-
|
|
910
|
-
private fun syncularGeneratedQueryRows(json: String): List<JsonObject> =
|
|
911
|
-
syncularGeneratedJson.parseToJsonElement(json).jsonObject["rows"]?.jsonArray?.map { it.jsonObject } ?: emptyList()
|
|
912
|
-
|
|
913
|
-
private fun syncularDecodeTaskRows(json: String): List<TaskRow> =
|
|
914
|
-
syncularGeneratedRows(json).map(::syncularDecodeTaskRow)
|
|
915
|
-
|
|
916
|
-
private fun syncularDecodeTaskRow(row: JsonObject): TaskRow = TaskRow(
|
|
917
|
-
id = row.syncularRequiredString("id"),
|
|
918
|
-
title = row.syncularRequiredString("title"),
|
|
919
|
-
completed = row.syncularRequiredLong("completed"),
|
|
920
|
-
userId = row.syncularRequiredString("user_id"),
|
|
921
|
-
createdAt = row.syncularRequiredLong("created_at"),
|
|
922
|
-
serverVersion = row.syncularRequiredLong("server_version"),
|
|
923
|
-
)
|
|
924
|
-
|
|
925
|
-
private fun JsonObject.syncularRequiredString(name: String): String =
|
|
926
|
-
syncularOptionalString(name) ?: error("missing string field $name")
|
|
927
|
-
|
|
928
|
-
private fun JsonObject.syncularOptionalString(name: String): String? {
|
|
929
|
-
val element = this[name] ?: return null
|
|
930
|
-
if (element is JsonNull) return null
|
|
931
|
-
return element.jsonPrimitive.content
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
private fun JsonObject.syncularRequiredBlobRef(name: String): SyncularBlobRef =
|
|
935
|
-
syncularOptionalBlobRef(name) ?: error("missing blob ref field $name")
|
|
936
|
-
|
|
937
|
-
private fun JsonObject.syncularOptionalBlobRef(name: String): SyncularBlobRef? {
|
|
938
|
-
val element = this[name] ?: return null
|
|
939
|
-
if (element is JsonNull) return null
|
|
940
|
-
val objectValue = runCatching {
|
|
941
|
-
syncularGeneratedJson.parseToJsonElement(element.jsonPrimitive.content).jsonObject
|
|
942
|
-
}.getOrElse {
|
|
943
|
-
element.jsonObject
|
|
944
|
-
}
|
|
945
|
-
return SyncularBlobRef(
|
|
946
|
-
hash = objectValue.syncularRequiredString("hash"),
|
|
947
|
-
size = objectValue.syncularRequiredLong("size"),
|
|
948
|
-
mimeType = objectValue.syncularRequiredString("mimeType"),
|
|
949
|
-
encrypted = objectValue.syncularOptionalBoolean("encrypted"),
|
|
950
|
-
keyId = objectValue.syncularOptionalString("keyId"),
|
|
951
|
-
)
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
private fun JsonObject.syncularRequiredLong(name: String): Long =
|
|
955
|
-
syncularOptionalLong(name) ?: error("missing integer field $name")
|
|
956
|
-
|
|
957
|
-
private fun JsonObject.syncularOptionalLong(name: String): Long? {
|
|
958
|
-
val element = this[name] ?: return null
|
|
959
|
-
if (element is JsonNull) return null
|
|
960
|
-
return element.jsonPrimitive.longOrNull ?: error("field $name is not an integer")
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
private fun JsonObject.syncularRequiredDouble(name: String): Double =
|
|
964
|
-
syncularOptionalDouble(name) ?: error("missing double field $name")
|
|
965
|
-
|
|
966
|
-
private fun JsonObject.syncularOptionalDouble(name: String): Double? {
|
|
967
|
-
val element = this[name] ?: return null
|
|
968
|
-
if (element is JsonNull) return null
|
|
969
|
-
return element.jsonPrimitive.doubleOrNull ?: error("field $name is not a double")
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
private fun JsonObject.syncularRequiredBoolean(name: String): Boolean =
|
|
973
|
-
syncularOptionalBoolean(name) ?: error("missing boolean field $name")
|
|
974
|
-
|
|
975
|
-
private fun JsonObject.syncularOptionalBoolean(name: String): Boolean? {
|
|
976
|
-
val element = this[name] ?: return null
|
|
977
|
-
if (element is JsonNull) return null
|
|
978
|
-
return element.jsonPrimitive.booleanOrNull ?: error("field $name is not a boolean")
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
private fun syncularJsonValue(value: Any?): String = when (value) {
|
|
982
|
-
null -> "null"
|
|
983
|
-
is SyncularBlobRef -> syncularJsonString(value.toJsonString())
|
|
984
|
-
is String -> syncularJsonString(value)
|
|
985
|
-
is Number -> value.toString()
|
|
986
|
-
is Boolean -> value.toString()
|
|
987
|
-
is Map<*, *> -> value.entries.joinToString(prefix = "{", postfix = "}") { entry -> syncularJsonString(entry.key.toString()) + ":" + syncularJsonValue(entry.value) }
|
|
988
|
-
is Iterable<*> -> value.joinToString(prefix = "[", postfix = "]") { syncularJsonValue(it) }
|
|
989
|
-
else -> syncularJsonString(value.toString())
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
private fun syncularJsonString(value: String): String = buildString {
|
|
993
|
-
append('\"')
|
|
994
|
-
for (ch in value) {
|
|
995
|
-
when (ch) {
|
|
996
|
-
'\\' -> append("\\\\")
|
|
997
|
-
'\"' -> append("\\\"")
|
|
998
|
-
'\n' -> append("\\n")
|
|
999
|
-
'\r' -> append("\\r")
|
|
1000
|
-
'\t' -> append("\\t")
|
|
1001
|
-
else -> append(ch)
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
append('\"')
|
|
1005
|
-
}
|