okmodel 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -1131
- package/dist/adapters/capabilities.d.ts +3 -0
- package/dist/adapters/failure.d.ts +13 -2
- package/dist/adapters/lazy-errors.d.ts +22 -0
- package/dist/adapters/pg/bunsql-extra.d.ts +54 -3
- package/dist/adapters/pg/bunsql.d.ts +14 -1
- package/dist/adapters/pg/cursor.d.ts +17 -0
- package/dist/adapters/pg/no-inflight-timeout.d.ts +11 -8
- package/dist/adapters/pg/nodepostgres-extra.d.ts +22 -1
- package/dist/adapters/pg/nodepostgres.d.ts +12 -0
- package/dist/adapters/pg/pglite-wire.d.ts +24 -0
- package/dist/adapters/pg/pglite.d.ts +10 -0
- package/dist/adapters/pg/postgres-extra.d.ts +27 -11
- package/dist/adapters/pg/reserved.d.ts +42 -0
- package/dist/adapters/pg/session.d.ts +0 -18
- package/dist/contracts/connection.d.ts +7 -6
- package/dist/dialects/pg/floor.d.ts +68 -0
- package/dist/dialects/pg/index.d.ts +6 -1
- package/dist/dialects/pg/index.js +49 -159
- package/dist/dialects/pg/keys.d.ts +11 -8
- package/dist/dialects/pg/model.d.ts +40 -0
- package/dist/dialects/pg/morph.d.ts +43 -0
- package/dist/dialects/pg/operator-fit.d.ts +2 -2
- package/dist/dialects/pg/operators.d.ts +14 -7
- package/dist/dialects/pg/ops/bucket.d.ts +65 -0
- package/dist/dialects/pg/ops/inc.d.ts +2 -2
- package/dist/dialects/pg/ops/matches.d.ts +9 -12
- package/dist/dialects/pg/ops/relevance.d.ts +13 -0
- package/dist/dialects/pg/ops/search-query.d.ts +22 -0
- package/dist/dialects/pg/ops/window.d.ts +103 -0
- package/dist/dialects/pg/preset.d.ts +1 -1
- package/dist/dialects/pg/relations.d.ts +100 -3
- package/dist/dialects/pg/schema.d.ts +31 -3
- package/dist/dialects/pg/view/index.js +1 -1
- package/dist/runtime/aggregate.d.ts +8 -2
- package/dist/runtime/bucket-group.d.ts +70 -0
- package/dist/runtime/conflict.d.ts +22 -4
- package/dist/runtime/include.d.ts +0 -8
- package/dist/runtime/inflight.d.ts +1 -1
- package/dist/runtime/page.d.ts +4 -1
- package/dist/runtime/pg/bun.js +12 -6
- package/dist/runtime/pg/pg.js +5 -5
- package/dist/runtime/pg/pglite.d.ts +1 -0
- package/dist/runtime/pg/pglite.js +9 -9
- package/dist/runtime/pg/postgresjs.js +5 -5
- package/dist/runtime/plan.d.ts +95 -5
- package/dist/runtime/read-stream.d.ts +13 -16
- package/dist/runtime/result-name.d.ts +17 -0
- package/dist/runtime/server-check.d.ts +3 -3
- package/dist/runtime/sort-bind.d.ts +21 -0
- package/dist/runtime/sort.d.ts +56 -0
- package/dist/runtime/sync.d.ts +70 -0
- package/dist/runtime/tenancy/index.js +7 -5
- package/dist/runtime/text-search.d.ts +20 -0
- package/dist/runtime/traits/index.d.ts +80 -0
- package/dist/runtime/traits/index.js +175 -11
- package/dist/runtime/tree.d.ts +38 -0
- package/dist/runtime/types.d.ts +218 -22
- package/dist/runtime/validate/index.js +2 -2
- package/dist/runtime/version.d.ts +39 -0
- package/dist/runtime/window.d.ts +21 -0
- package/dist/runtime/write.d.ts +54 -2
- package/dist/shared/{wkme6tep.js → 14vx95q4.js} +8 -16
- package/dist/shared/1est4ezg.js +313 -0
- package/dist/shared/{4az561fn.js → 1qa0qajz.js} +8 -23
- package/dist/shared/{2cg3wnxc.js → 1w508jbr.js} +593 -130
- package/dist/shared/2c13q96h.js +18 -0
- package/dist/shared/{b4v57v3x.js → 2d3xq4ps.js} +2 -2
- package/dist/shared/{1nw1f3vs.js → 2shdbn8q.js} +7 -7
- package/dist/shared/3v7t1yrv.js +208 -0
- package/dist/shared/{x3jc53kg.js → 5e3tf63j.js} +2 -2
- package/dist/shared/5fe3kbsh.js +10 -0
- package/dist/shared/5v3xf80k.js +37 -0
- package/dist/shared/{c58secgj.js → 63qtkrca.js} +38 -4
- package/dist/shared/{2z7wv5tj.js → 67txcrcw.js} +55 -34
- package/dist/shared/79dt02vc.js +12 -0
- package/dist/shared/{8x72s86k.js → 7k9grvf6.js} +10 -4
- package/dist/shared/7kms6fvj.js +20 -0
- package/dist/shared/7mnme5tn.js +8 -0
- package/dist/shared/7n7ddwt4.js +6 -0
- package/dist/shared/{5j36z8dn.js → 81ebdx9p.js} +39 -70
- package/dist/shared/{knmes2kh.js → 8skf0cam.js} +1 -1
- package/dist/shared/9a4ae4p8.js +31 -0
- package/dist/shared/9y3mmmh1.js +436 -0
- package/dist/shared/ab9gmbwy.js +213 -0
- package/dist/shared/{f0f1n281.js → ax7qv55k.js} +2 -2
- package/dist/shared/{g90m9b0g.js → aytqh5y2.js} +6 -5
- package/dist/shared/{vjpnzwjz.js → b5pftnn2.js} +1 -1
- package/dist/shared/{ag4c7516.js → b76b8d95.js} +2 -2
- package/dist/shared/{vmfkhszw.js → bg3rym3g.js} +33 -26
- package/dist/shared/{mb2ycs1p.js → bm22fvzr.js} +15 -15
- package/dist/shared/{q3j1tq7a.js → bz84tkzm.js} +148 -23
- package/dist/shared/{31vm914h.js → cm3w0ywp.js} +15 -16
- package/dist/shared/cn957f8d.js +179 -0
- package/dist/shared/{gwn6v667.js → cnkky55j.js} +25 -14
- package/dist/shared/{ys0xzyaj.js → dkxgzaqd.js} +224 -19
- package/dist/shared/dzj09cye.js +10 -0
- package/dist/shared/exn3sd90.js +359 -0
- package/dist/shared/g6evj1j3.js +66 -0
- package/dist/shared/ga9j92qj.js +25 -0
- package/dist/shared/{t3g0k88x.js → gs74dbj5.js} +16 -12
- package/dist/shared/gzaveazc.js +285 -0
- package/dist/shared/{2jd1sn0e.js → hyh73qk6.js} +1 -1
- package/dist/shared/{r1fdd0f6.js → je4axnm8.js} +5 -2
- package/dist/shared/{1f0xa3cb.js → jvd0mqag.js} +3 -3
- package/dist/shared/jznfax0y.js +155 -0
- package/dist/shared/{cgbamx3e.js → kb2sdhfb.js} +7 -4
- package/dist/shared/{wt5rzv1h.js → kr0jzbw2.js} +5 -5
- package/dist/shared/{p6bjzq9q.js → ktz42yez.js} +12 -4
- package/dist/shared/{p248q79y.js → m6p12kbr.js} +2 -2
- package/dist/shared/{r6nwkxnt.js → m7n9h2a5.js} +4 -4
- package/dist/shared/{672t5vym.js → mss6ctgv.js} +2 -2
- package/dist/shared/{929h19xp.js → mtaszv5a.js} +40 -10
- package/dist/shared/{b3awxn7h.js → n816peh1.js} +1 -1
- package/dist/shared/nbw1z4yd.js +18 -0
- package/dist/shared/{nx9hh281.js → p6qc5gcp.js} +12 -11
- package/dist/shared/{pjr6xrm5.js → pbp1ss85.js} +20 -17
- package/dist/shared/pt94kz7f.js +6 -0
- package/dist/shared/q5c6ngn3.js +13 -0
- package/dist/shared/{xq0mvhts.js → rs73fca9.js} +4 -4
- package/dist/shared/{r11n8grd.js → sfz1ksn6.js} +7 -4
- package/dist/shared/{wynzeggs.js → svncdeh5.js} +6 -6
- package/dist/shared/{j60w0dvr.js → tbrvgw6z.js} +57 -38
- package/dist/shared/te5as7z7.js +622 -0
- package/dist/shared/{ttmg09vd.js → tsvww22q.js} +1 -1
- package/dist/shared/{y2t3jktd.js → ttsqgp4p.js} +19 -12
- package/dist/shared/{x72jb7ad.js → v8g1vgv8.js} +1 -1
- package/dist/shared/{sbdme3vy.js → vb6qfyve.js} +60 -10
- package/dist/shared/{5d8t84hg.js → vczs8gd4.js} +14 -14
- package/dist/shared/{5yv4k6a6.js → vqrjpcqn.js} +144 -2
- package/dist/shared/{3fqwmr09.js → w72kgg4q.js} +4 -4
- package/dist/shared/{1z04fa85.js → w79hsckn.js} +12 -57
- package/dist/shared/{09p049xa.js → wa07eas9.js} +42 -14
- package/dist/shared/{32cnm7kn.js → x933k9a3.js} +74 -38
- package/dist/shared/{n076ydsc.js → xhfsfkgk.js} +62 -28
- package/dist/shared/xjk7hnn5.js +272 -0
- package/dist/shared/{hazqqecq.js → xnxsed62.js} +50 -21
- package/dist/shared/y6njgyda.js +249 -0
- package/dist/shared/{8etn7d8w.js → z7s6fpxv.js} +1 -1
- package/dist/shared/{n7mcc1gk.js → zchaghkf.js} +4 -3
- package/dist/shared/zm0ptry0.js +40 -0
- package/dist/shared/{nsva2a6b.js → znm0bpp5.js} +3 -0
- package/dist/shared/zp9fmend.js +145 -0
- package/dist/tooling/cli.js +11 -11
- package/dist/tooling/migrate/engine.d.ts +3 -2
- package/dist/tooling/migrate/index.js +7 -7
- package/dist/tooling/testing/index.js +1 -1
- package/package.json +4 -2
- package/dist/shared/3qnhc7w5.js +0 -38
- package/dist/shared/7m88x3e9.js +0 -45
- package/dist/shared/8dqhth8c.js +0 -12
- package/dist/shared/d5azex6s.js +0 -6
- package/dist/shared/j3rkk835.js +0 -99
- package/dist/shared/k68eedmy.js +0 -85
- package/dist/shared/venqrjna.js +0 -216
- package/dist/shared/wxr8cwns.js +0 -6
- package/dist/shared/zm0mafk4.js +0 -92
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import {
|
|
2
|
+
identityColumn2
|
|
3
|
+
} from "./xjk7hnn5.js";
|
|
4
|
+
import {
|
|
5
|
+
runWrite
|
|
6
|
+
} from "./n4jqhpqz.js";
|
|
7
|
+
import {
|
|
8
|
+
queryHandle,
|
|
9
|
+
settleCall
|
|
10
|
+
} from "./tbrvgw6z.js";
|
|
11
|
+
import {
|
|
12
|
+
withRowFilters,
|
|
13
|
+
archiveRules,
|
|
14
|
+
emitWhere,
|
|
15
|
+
rejectKeys,
|
|
16
|
+
indexes,
|
|
17
|
+
quote,
|
|
18
|
+
list,
|
|
19
|
+
isRecord,
|
|
20
|
+
fail2
|
|
21
|
+
} from "./67txcrcw.js";
|
|
22
|
+
import {
|
|
23
|
+
safetyInstalled,
|
|
24
|
+
runSafety
|
|
25
|
+
} from "./a5sxz52y.js";
|
|
26
|
+
import {
|
|
27
|
+
OkmError2,
|
|
28
|
+
throwNamed2
|
|
29
|
+
} from "./52szzakm.js";
|
|
30
|
+
|
|
31
|
+
// src/runtime/sync.ts
|
|
32
|
+
var OPTIONS = ["expect", "signal", "timeout", "route"];
|
|
33
|
+
async function execute(session, table, id, relation, ids, options, mods) {
|
|
34
|
+
try {
|
|
35
|
+
const planned = await prepare(session, table, id, relation, ids, options, mods);
|
|
36
|
+
const results = await runWrite(session, [planned.lock, planned.statement], planned.call);
|
|
37
|
+
return readResult(planned, results[1]?.rows ?? []);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return settleCall(session, error);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function explain(session, table, id, relation, ids, options, mods) {
|
|
43
|
+
try {
|
|
44
|
+
const planned = await prepare(session, table, id, relation, ids, options, mods);
|
|
45
|
+
return { statements: [planned.lock, planned.statement] };
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return settleCall(session, error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function build(session, table, input, mods, view) {
|
|
51
|
+
const packed = input;
|
|
52
|
+
const expect = mods.expect;
|
|
53
|
+
const applied = {
|
|
54
|
+
...typeof expect === "number" ? { expect } : {},
|
|
55
|
+
archive: view,
|
|
56
|
+
uses: mods.uses
|
|
57
|
+
};
|
|
58
|
+
return queryHandle(() => execute(session, table, packed.id, packed.relation, packed.ids, packed.options, applied), {
|
|
59
|
+
sql: () => explain(session, table, packed.id, packed.relation, packed.ids, packed.options, applied)
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async function prepare(session, table, id, relationName, ids, options, mods) {
|
|
63
|
+
const record = options === undefined ? {} : options;
|
|
64
|
+
if (!isRecord(record))
|
|
65
|
+
fail2("OKM1120", "sync options must be an object.");
|
|
66
|
+
rejectKeys(record, OPTIONS, "sync");
|
|
67
|
+
const owner = indexes(session.schema).get(table);
|
|
68
|
+
if (owner === undefined)
|
|
69
|
+
fail2("OKM1120", `Table ${table} is not in the schema.`);
|
|
70
|
+
const relation = requireRelation(owner, relationName);
|
|
71
|
+
if (relation.morph !== undefined) {
|
|
72
|
+
fail2("OKM1120", `sync does not follow ${relation.name}. Set ${relation.morph.typeField} and ${relation.morph.idField} with morphTo.`);
|
|
73
|
+
}
|
|
74
|
+
const related = indexes(session.schema).get(relation.table);
|
|
75
|
+
if (related === undefined) {
|
|
76
|
+
fail2("OKM1120", `Relation ${relation.name} names ${relation.table}, which is not in the schema.`);
|
|
77
|
+
}
|
|
78
|
+
const identity = identityColumn2(session, owner);
|
|
79
|
+
const relatedId = identityColumn2(session, related);
|
|
80
|
+
const parent = userValue(identity, id);
|
|
81
|
+
const wanted = readIds(relatedId, ids);
|
|
82
|
+
const own = String(relatedId.encode(parent));
|
|
83
|
+
if (relation.kind === "many" && relation.through === undefined && owner.model.name === related.model.name && wanted.some((item) => String(item) === own)) {
|
|
84
|
+
fail2("OKM1120", `sync on ${owner.model.name}.${relation.name} cannot make a row its own child in a tree.`, "A row cannot be its own child in a tree. Leave its id out of the list.");
|
|
85
|
+
}
|
|
86
|
+
const where = await presetWhere(session.schema, table, mods.uses);
|
|
87
|
+
noteSafety(session, table, mods.archive);
|
|
88
|
+
const statement = withRowFilters(session.scope, mods.archive, () => relation.through === undefined ? oneToMany(session, owner, related, relation, identity, relatedId, parent, wanted, where) : manyThrough(session, owner, related, relation, identity, relatedId, parent, wanted, where));
|
|
89
|
+
const lock = withRowFilters(session.scope, mods.archive, () => parentLock(session, owner, identity, parent, where));
|
|
90
|
+
return {
|
|
91
|
+
lock,
|
|
92
|
+
statement,
|
|
93
|
+
call: callOf(record),
|
|
94
|
+
expect: expectOf(mods.expect ?? record.expect),
|
|
95
|
+
decode: relatedId.decode,
|
|
96
|
+
table,
|
|
97
|
+
relation: relation.name,
|
|
98
|
+
related: related.model.name
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function oneToMany(session, owner, related, relation, identity, relatedId, parent, wanted, where) {
|
|
102
|
+
const tenant = tenantSql(session, owner);
|
|
103
|
+
const clearing = [];
|
|
104
|
+
for (let index = 0;index < relation.local.length; index += 1) {
|
|
105
|
+
const parentSql = relation.local[index];
|
|
106
|
+
const childSql = relation.remote[index];
|
|
107
|
+
if (parentSql === undefined || childSql === undefined)
|
|
108
|
+
continue;
|
|
109
|
+
if (tenant.has(parentSql))
|
|
110
|
+
continue;
|
|
111
|
+
clearing.push({ child: childSql, parent: parentSql });
|
|
112
|
+
}
|
|
113
|
+
if (clearing.length === 0) {
|
|
114
|
+
fail2("OKM1120", `sync on ${owner.model.name}.${relation.name} cannot clear a link.`, "The foreign key is only the tenant key.");
|
|
115
|
+
}
|
|
116
|
+
const catalog = catalogOf(session.schema);
|
|
117
|
+
for (const link of clearing) {
|
|
118
|
+
const column = related.model.columns.find((item) => item.sql === link.child);
|
|
119
|
+
const stored = storedColumn(catalog, related.model.sql, link.child);
|
|
120
|
+
if (stored === undefined || stored.nullable !== true) {
|
|
121
|
+
fail2("OKM1120", `sync on ${owner.model.name}.${relation.name} would delete rows to drop a link. ${related.model.name}.${column?.field ?? link.child} is not nullable.`, "Use a nullable foreign key, or a join table.");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const sink = text();
|
|
125
|
+
sink.text("with locked as materialized (select ");
|
|
126
|
+
emitSelectLocked(sink, relation.local);
|
|
127
|
+
sink.text(" from ");
|
|
128
|
+
sink.text(quote(owner.model.sql));
|
|
129
|
+
sink.text(" p where p.");
|
|
130
|
+
sink.text(quote(identity.sql));
|
|
131
|
+
sink.text(" = ");
|
|
132
|
+
sink.param(identity.encode(parent));
|
|
133
|
+
emitWhere(session.schema, owner, where, sink, "p", 0, true);
|
|
134
|
+
sink.text(" for update), ");
|
|
135
|
+
emitDesired(sink, relatedId, wanted);
|
|
136
|
+
sink.text(", missing as (select d.id from desired d where not exists (select 1 from ");
|
|
137
|
+
sink.text(quote(related.model.sql));
|
|
138
|
+
sink.text(" r where r.");
|
|
139
|
+
sink.text(quote(relatedId.sql));
|
|
140
|
+
sink.text(" = d.id");
|
|
141
|
+
emitWhere(session.schema, related, undefined, sink, "r", 0, true);
|
|
142
|
+
sink.text(")), guard as (select (select count(*) from locked) as parents, (select count(*) from missing) as missing_n), held as materialized (select c.");
|
|
143
|
+
sink.text(quote(relatedId.sql));
|
|
144
|
+
sink.text(" as id from ");
|
|
145
|
+
sink.text(quote(related.model.sql));
|
|
146
|
+
sink.text(" c where (");
|
|
147
|
+
emitChildMatch(sink, "c", clearing);
|
|
148
|
+
sink.text(" or c.");
|
|
149
|
+
sink.text(quote(relatedId.sql));
|
|
150
|
+
sink.text(" in (select d.id from desired d))");
|
|
151
|
+
emitGuard(sink);
|
|
152
|
+
emitWhere(session.schema, related, undefined, sink, "c", 0, true);
|
|
153
|
+
sink.text(" order by c.");
|
|
154
|
+
sink.text(quote(relatedId.sql));
|
|
155
|
+
sink.text(" for update), changed as (update ");
|
|
156
|
+
sink.text(quote(related.model.sql));
|
|
157
|
+
sink.text(" c set ");
|
|
158
|
+
emitSetCase(sink, "c", relatedId, clearing);
|
|
159
|
+
sink.text(" where c.");
|
|
160
|
+
sink.text(quote(relatedId.sql));
|
|
161
|
+
sink.text(" in (select h.id from held h) and ");
|
|
162
|
+
emitWillChange(sink, "c", relatedId, clearing);
|
|
163
|
+
emitWhere(session.schema, related, undefined, sink, "c", 0, true);
|
|
164
|
+
emitGuard(sink);
|
|
165
|
+
sink.text(" returning c.");
|
|
166
|
+
sink.text(quote(relatedId.sql));
|
|
167
|
+
sink.text(" as id, case when c.");
|
|
168
|
+
sink.text(quote(relatedId.sql));
|
|
169
|
+
sink.text(" in (select d.id from desired d) then 'a' else 'r' end as kind) ");
|
|
170
|
+
emitReport(sink, "kind");
|
|
171
|
+
return sink.finish();
|
|
172
|
+
}
|
|
173
|
+
function manyThrough(session, owner, related, relation, identity, relatedId, parent, wanted, where) {
|
|
174
|
+
const through = relation.through;
|
|
175
|
+
if (through === undefined) {
|
|
176
|
+
fail2("OKM1120", `Relation ${relation.name} has no join table.`);
|
|
177
|
+
}
|
|
178
|
+
const join = indexes(session.schema).get(through.table);
|
|
179
|
+
if (join === undefined) {
|
|
180
|
+
fail2("OKM1120", `Relation ${relation.name} names ${through.table}, which is not in the schema.`);
|
|
181
|
+
}
|
|
182
|
+
const slots = joinSlots(relation, relatedId);
|
|
183
|
+
const catalog = catalogOf(session.schema);
|
|
184
|
+
for (const column of join.model.columns) {
|
|
185
|
+
if (slots.some((slot) => slot.join === column.sql))
|
|
186
|
+
continue;
|
|
187
|
+
const stored = storedColumn(catalog, join.model.sql, column.sql);
|
|
188
|
+
if (stored !== undefined && canOmit(stored))
|
|
189
|
+
continue;
|
|
190
|
+
fail2("OKM1120", `sync on ${owner.model.name}.${relation.name} cannot fill ${join.model.name}.${column.field}.`, "A database default can fill it. sync does not.");
|
|
191
|
+
}
|
|
192
|
+
const link = slots.find((slot) => slot.from === "desired");
|
|
193
|
+
if (link === undefined) {
|
|
194
|
+
fail2("OKM1120", `sync on ${owner.model.name}.${relation.name} cannot see the related id.`);
|
|
195
|
+
}
|
|
196
|
+
const ownerSlots = slots.filter((slot) => slot.from === "locked");
|
|
197
|
+
if (ownerSlots.length === 0) {
|
|
198
|
+
fail2("OKM1120", `sync on ${owner.model.name}.${relation.name} cannot see the parent row.`);
|
|
199
|
+
}
|
|
200
|
+
const sink = text();
|
|
201
|
+
sink.text("with locked as materialized (select ");
|
|
202
|
+
emitSelectLocked(sink, relation.local);
|
|
203
|
+
sink.text(" from ");
|
|
204
|
+
sink.text(quote(owner.model.sql));
|
|
205
|
+
sink.text(" p where p.");
|
|
206
|
+
sink.text(quote(identity.sql));
|
|
207
|
+
sink.text(" = ");
|
|
208
|
+
sink.param(identity.encode(parent));
|
|
209
|
+
emitWhere(session.schema, owner, where, sink, "p", 0, true);
|
|
210
|
+
sink.text(" for update), ");
|
|
211
|
+
emitDesired(sink, relatedId, wanted);
|
|
212
|
+
sink.text(", missing as (select d.id from desired d where not exists (select 1 from ");
|
|
213
|
+
sink.text(quote(related.model.sql));
|
|
214
|
+
sink.text(" r where r.");
|
|
215
|
+
sink.text(quote(relatedId.sql));
|
|
216
|
+
sink.text(" = d.id");
|
|
217
|
+
emitWhere(session.schema, related, undefined, sink, "r", 0, true);
|
|
218
|
+
sink.text(")), guard as (select (select count(*) from locked) as parents, (select count(*) from missing) as missing_n), removed as (delete from ");
|
|
219
|
+
sink.text(quote(join.model.sql));
|
|
220
|
+
sink.text(" j where ");
|
|
221
|
+
emitJoinLocked(sink, "j", ownerSlots);
|
|
222
|
+
sink.text(" and not exists (select 1 from desired d where d.id = j.");
|
|
223
|
+
sink.text(quote(link.join));
|
|
224
|
+
sink.text(")");
|
|
225
|
+
emitWhere(session.schema, join, undefined, sink, "j", 0, true);
|
|
226
|
+
emitGuard(sink);
|
|
227
|
+
sink.text(" returning j.");
|
|
228
|
+
sink.text(quote(link.join));
|
|
229
|
+
sink.text(" as id), added as (insert into ");
|
|
230
|
+
sink.text(quote(join.model.sql));
|
|
231
|
+
sink.text(" (");
|
|
232
|
+
for (let index = 0;index < slots.length; index += 1) {
|
|
233
|
+
const slot = slots[index];
|
|
234
|
+
if (slot === undefined)
|
|
235
|
+
continue;
|
|
236
|
+
if (index > 0)
|
|
237
|
+
sink.text(", ");
|
|
238
|
+
sink.text(quote(slot.join));
|
|
239
|
+
}
|
|
240
|
+
sink.text(") select ");
|
|
241
|
+
for (let index = 0;index < slots.length; index += 1) {
|
|
242
|
+
const slot = slots[index];
|
|
243
|
+
if (slot === undefined)
|
|
244
|
+
continue;
|
|
245
|
+
if (index > 0)
|
|
246
|
+
sink.text(", ");
|
|
247
|
+
if (slot.from === "desired")
|
|
248
|
+
sink.text("d.id");
|
|
249
|
+
else
|
|
250
|
+
emitLocked(sink, slot.locked ?? "");
|
|
251
|
+
}
|
|
252
|
+
sink.text(" from desired d where not exists (select 1 from ");
|
|
253
|
+
sink.text(quote(join.model.sql));
|
|
254
|
+
sink.text(" j where ");
|
|
255
|
+
emitJoinLocked(sink, "j", ownerSlots);
|
|
256
|
+
sink.text(" and j.");
|
|
257
|
+
sink.text(quote(link.join));
|
|
258
|
+
sink.text(" = d.id");
|
|
259
|
+
emitWhere(session.schema, join, undefined, sink, "j", 0, true);
|
|
260
|
+
sink.text(")");
|
|
261
|
+
emitGuard(sink);
|
|
262
|
+
sink.text(" returning ");
|
|
263
|
+
sink.text(quote(link.join));
|
|
264
|
+
sink.text(" as id) ");
|
|
265
|
+
emitReport(sink, "split");
|
|
266
|
+
return sink.finish();
|
|
267
|
+
}
|
|
268
|
+
function joinSlots(relation, relatedId) {
|
|
269
|
+
const through = relation.through;
|
|
270
|
+
if (through === undefined)
|
|
271
|
+
return [];
|
|
272
|
+
const slots = [];
|
|
273
|
+
const seen = new Set;
|
|
274
|
+
const add = (slot) => {
|
|
275
|
+
if (seen.has(slot.join))
|
|
276
|
+
return;
|
|
277
|
+
seen.add(slot.join);
|
|
278
|
+
slots.push(slot);
|
|
279
|
+
};
|
|
280
|
+
for (let index = 0;index < relation.remote.length; index += 1) {
|
|
281
|
+
const join = relation.remote[index];
|
|
282
|
+
const locked = relation.local[index];
|
|
283
|
+
if (join === undefined || locked === undefined)
|
|
284
|
+
continue;
|
|
285
|
+
add({ join, from: "locked", locked });
|
|
286
|
+
}
|
|
287
|
+
for (let index = 0;index < through.local.length; index += 1) {
|
|
288
|
+
const join = through.local[index];
|
|
289
|
+
const remote = through.remote[index];
|
|
290
|
+
if (join === undefined || remote === undefined)
|
|
291
|
+
continue;
|
|
292
|
+
if (remote === relatedId.sql)
|
|
293
|
+
add({ join, from: "desired" });
|
|
294
|
+
else if (relation.local.includes(remote))
|
|
295
|
+
add({ join, from: "locked", locked: remote });
|
|
296
|
+
else {
|
|
297
|
+
fail2("OKM1120", `sync cannot copy ${remote} onto the join table.`, "The join's foreign key has a column this row does not.");
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return slots;
|
|
301
|
+
}
|
|
302
|
+
function parentLock(session, owner, identity, parent, where) {
|
|
303
|
+
const sink = text();
|
|
304
|
+
sink.text("select p.");
|
|
305
|
+
sink.text(quote(identity.sql));
|
|
306
|
+
sink.text(" from ");
|
|
307
|
+
sink.text(quote(owner.model.sql));
|
|
308
|
+
sink.text(" p where p.");
|
|
309
|
+
sink.text(quote(identity.sql));
|
|
310
|
+
sink.text(" = ");
|
|
311
|
+
sink.param(identity.encode(parent));
|
|
312
|
+
emitWhere(session.schema, owner, where, sink, "p", 0, true);
|
|
313
|
+
sink.text(" for update");
|
|
314
|
+
return sink.finish();
|
|
315
|
+
}
|
|
316
|
+
function emitSelectLocked(sink, columns) {
|
|
317
|
+
for (let index = 0;index < columns.length; index += 1) {
|
|
318
|
+
const name = columns[index];
|
|
319
|
+
if (name === undefined)
|
|
320
|
+
continue;
|
|
321
|
+
if (index > 0)
|
|
322
|
+
sink.text(", ");
|
|
323
|
+
sink.text("p.");
|
|
324
|
+
sink.text(quote(name));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function emitDesired(sink, column, ids) {
|
|
328
|
+
sink.text("desired as (");
|
|
329
|
+
if (ids.length === 0) {
|
|
330
|
+
sink.text("select cast(null as ");
|
|
331
|
+
sink.text(column.dataType);
|
|
332
|
+
sink.text(") as id where false");
|
|
333
|
+
} else {
|
|
334
|
+
sink.text("select distinct v.id from (values ");
|
|
335
|
+
for (let index = 0;index < ids.length; index += 1) {
|
|
336
|
+
const encoded = ids[index];
|
|
337
|
+
if (encoded === undefined)
|
|
338
|
+
continue;
|
|
339
|
+
if (index > 0)
|
|
340
|
+
sink.text(", ");
|
|
341
|
+
sink.text("(cast(");
|
|
342
|
+
sink.param(encoded);
|
|
343
|
+
sink.text(" as ");
|
|
344
|
+
sink.text(column.dataType);
|
|
345
|
+
sink.text("))");
|
|
346
|
+
}
|
|
347
|
+
sink.text(") as v(id)");
|
|
348
|
+
}
|
|
349
|
+
sink.text(")");
|
|
350
|
+
}
|
|
351
|
+
function emitChildMatch(sink, alias, links) {
|
|
352
|
+
for (let index = 0;index < links.length; index += 1) {
|
|
353
|
+
const link = links[index];
|
|
354
|
+
if (link === undefined)
|
|
355
|
+
continue;
|
|
356
|
+
if (index > 0)
|
|
357
|
+
sink.text(" and ");
|
|
358
|
+
sink.text(alias);
|
|
359
|
+
sink.text(".");
|
|
360
|
+
sink.text(quote(link.child));
|
|
361
|
+
sink.text(" = ");
|
|
362
|
+
emitLocked(sink, link.parent);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function emitSetCase(sink, alias, identity, links) {
|
|
366
|
+
for (let index = 0;index < links.length; index += 1) {
|
|
367
|
+
const link = links[index];
|
|
368
|
+
if (link === undefined)
|
|
369
|
+
continue;
|
|
370
|
+
if (index > 0)
|
|
371
|
+
sink.text(", ");
|
|
372
|
+
sink.text(quote(link.child));
|
|
373
|
+
sink.text(" = case when ");
|
|
374
|
+
sink.text(alias);
|
|
375
|
+
sink.text(".");
|
|
376
|
+
sink.text(quote(identity.sql));
|
|
377
|
+
sink.text(" in (select d.id from desired d) then ");
|
|
378
|
+
emitLocked(sink, link.parent);
|
|
379
|
+
sink.text(" else null end");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function emitWillChange(sink, alias, identity, links) {
|
|
383
|
+
sink.text("((");
|
|
384
|
+
emitChildMatch(sink, alias, links);
|
|
385
|
+
sink.text(" and not exists (select 1 from desired d where d.id = ");
|
|
386
|
+
sink.text(alias);
|
|
387
|
+
sink.text(".");
|
|
388
|
+
sink.text(quote(identity.sql));
|
|
389
|
+
sink.text(")) or (");
|
|
390
|
+
sink.text(alias);
|
|
391
|
+
sink.text(".");
|
|
392
|
+
sink.text(quote(identity.sql));
|
|
393
|
+
sink.text(" in (select d.id from desired d) and (");
|
|
394
|
+
emitDistinct(sink, alias, links);
|
|
395
|
+
sink.text(")))");
|
|
396
|
+
}
|
|
397
|
+
function emitDistinct(sink, alias, links) {
|
|
398
|
+
for (let index = 0;index < links.length; index += 1) {
|
|
399
|
+
const link = links[index];
|
|
400
|
+
if (link === undefined)
|
|
401
|
+
continue;
|
|
402
|
+
if (index > 0)
|
|
403
|
+
sink.text(" or ");
|
|
404
|
+
sink.text(alias);
|
|
405
|
+
sink.text(".");
|
|
406
|
+
sink.text(quote(link.child));
|
|
407
|
+
sink.text(" is distinct from ");
|
|
408
|
+
emitLocked(sink, link.parent);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function emitJoinLocked(sink, alias, slots) {
|
|
412
|
+
for (let index = 0;index < slots.length; index += 1) {
|
|
413
|
+
const slot = slots[index];
|
|
414
|
+
if (slot === undefined || slot.locked === undefined)
|
|
415
|
+
continue;
|
|
416
|
+
if (index > 0)
|
|
417
|
+
sink.text(" and ");
|
|
418
|
+
sink.text(alias);
|
|
419
|
+
sink.text(".");
|
|
420
|
+
sink.text(quote(slot.join));
|
|
421
|
+
sink.text(" = ");
|
|
422
|
+
emitLocked(sink, slot.locked);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function emitLocked(sink, column) {
|
|
426
|
+
sink.text("(select l.");
|
|
427
|
+
sink.text(quote(column));
|
|
428
|
+
sink.text(" from locked l)");
|
|
429
|
+
}
|
|
430
|
+
function emitGuard(sink) {
|
|
431
|
+
sink.text(" and (select g.parents from guard g) = 1 and (select g.missing_n from guard g) = 0");
|
|
432
|
+
}
|
|
433
|
+
function emitReport(sink, changes) {
|
|
434
|
+
sink.text(changes === "kind" ? "select 'm'::text, d.id::text from missing d union all select ch.kind, ch.id::text from changed ch union all select 'p'::text, g.parents::text from guard g" : "select 'm'::text, d.id::text from missing d union all select 'a'::text, a.id::text from added a union all select 'r'::text, r.id::text from removed r union all select 'p'::text, g.parents::text from guard g");
|
|
435
|
+
}
|
|
436
|
+
function readResult(planned, rows) {
|
|
437
|
+
const added = [];
|
|
438
|
+
const removed = [];
|
|
439
|
+
let parents = 0;
|
|
440
|
+
let missing = 0;
|
|
441
|
+
for (const row of rows) {
|
|
442
|
+
const kind = row[0];
|
|
443
|
+
const value = row[1];
|
|
444
|
+
if (kind === "p")
|
|
445
|
+
parents = Number(value);
|
|
446
|
+
else if (kind === "m")
|
|
447
|
+
missing += 1;
|
|
448
|
+
else if (value === null || value === undefined)
|
|
449
|
+
continue;
|
|
450
|
+
else if (kind === "a")
|
|
451
|
+
added.push(decode(planned.decode, value));
|
|
452
|
+
else if (kind === "r")
|
|
453
|
+
removed.push(decode(planned.decode, value));
|
|
454
|
+
}
|
|
455
|
+
if (parents !== 1) {
|
|
456
|
+
throw new OkmError2("not_found", `sync on ${planned.table} did not find that id.`, {
|
|
457
|
+
kind: "not_found",
|
|
458
|
+
table: planned.table,
|
|
459
|
+
fix: { summary: "Pass an id that is in the table." }
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
if (missing > 0) {
|
|
463
|
+
throw new OkmError2("not_found", `sync on ${planned.table}.${planned.relation} is missing ${String(missing)} ids.`, {
|
|
464
|
+
kind: "not_found",
|
|
465
|
+
table: planned.table,
|
|
466
|
+
fix: { summary: `Pass ids that exist on ${planned.related}.` }
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
const changed = added.length + removed.length;
|
|
470
|
+
if (planned.expect !== undefined && changed !== planned.expect) {
|
|
471
|
+
throw new OkmError2("not_found", `sync on ${planned.table}.${planned.relation} changed ${String(changed)} links. expect was ${String(planned.expect)}.`, {
|
|
472
|
+
kind: "not_found",
|
|
473
|
+
table: planned.table,
|
|
474
|
+
fix: { summary: "Change the ids, or pass the number of links this sync changed." }
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
added.sort(byText);
|
|
478
|
+
removed.sort(byText);
|
|
479
|
+
return { added, removed };
|
|
480
|
+
}
|
|
481
|
+
function decode(fn, wire) {
|
|
482
|
+
return fn === undefined ? wire : fn(wire);
|
|
483
|
+
}
|
|
484
|
+
function byText(left, right) {
|
|
485
|
+
return String(left) < String(right) ? -1 : String(left) > String(right) ? 1 : 0;
|
|
486
|
+
}
|
|
487
|
+
function requireRelation(table, name) {
|
|
488
|
+
if (typeof name !== "string" || name.length === 0)
|
|
489
|
+
fail2("OKM1120", "sync relation must be a name.");
|
|
490
|
+
const relation = table.relations.get(name);
|
|
491
|
+
if (relation === undefined) {
|
|
492
|
+
throwNamed2("OKM1120", name, [...table.relations.keys()], `Relation ${name} is not on ${table.model.name}. Accepted names: ${list([...table.relations.keys()])}.`);
|
|
493
|
+
}
|
|
494
|
+
if (relation.kind !== "many") {
|
|
495
|
+
fail2("OKM1120", `sync on ${table.model.name}.${name} is not a to-many relation.`, "Pass a many or manyThrough relation.");
|
|
496
|
+
}
|
|
497
|
+
return relation;
|
|
498
|
+
}
|
|
499
|
+
function readIds(column, ids) {
|
|
500
|
+
if (!Array.isArray(ids))
|
|
501
|
+
fail2("OKM1120", "sync ids must be a list.");
|
|
502
|
+
const unique = [];
|
|
503
|
+
const seen = new Set;
|
|
504
|
+
for (const id of ids) {
|
|
505
|
+
if (id === undefined || id === null)
|
|
506
|
+
fail2("OKM1121", "sync ids cannot include null.");
|
|
507
|
+
const encoded = column.encode(id);
|
|
508
|
+
if (seen.has(encoded))
|
|
509
|
+
continue;
|
|
510
|
+
seen.add(encoded);
|
|
511
|
+
unique.push(encoded);
|
|
512
|
+
}
|
|
513
|
+
return unique;
|
|
514
|
+
}
|
|
515
|
+
function userValue(column, id) {
|
|
516
|
+
if (id === undefined || id === null)
|
|
517
|
+
fail2("OKM1121", `${column.field} needs a value.`);
|
|
518
|
+
return id;
|
|
519
|
+
}
|
|
520
|
+
function expectOf(value) {
|
|
521
|
+
if (value === undefined)
|
|
522
|
+
return;
|
|
523
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
524
|
+
throw new OkmError2("invalid", "expect must be an integer from 0 up.", { kind: "invalid" });
|
|
525
|
+
}
|
|
526
|
+
return value;
|
|
527
|
+
}
|
|
528
|
+
function callOf(options) {
|
|
529
|
+
const signal = options.signal;
|
|
530
|
+
const timeout = options.timeout;
|
|
531
|
+
const route = options.route;
|
|
532
|
+
const hasSignal = typeof signal === "object" && signal !== null && "aborted" in signal;
|
|
533
|
+
const hasTimeout = typeof timeout === "number";
|
|
534
|
+
const hasRoute = route === "primary" || route === "replica";
|
|
535
|
+
if (!hasSignal && !hasTimeout && !hasRoute)
|
|
536
|
+
return;
|
|
537
|
+
return {
|
|
538
|
+
...hasSignal ? { signal } : {},
|
|
539
|
+
...hasTimeout ? { timeout } : {},
|
|
540
|
+
...hasRoute ? { route } : {}
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
async function presetWhere(schema, table, uses) {
|
|
544
|
+
if (uses === undefined)
|
|
545
|
+
return;
|
|
546
|
+
const { resolve } = await import("./m7n9h2a5.js");
|
|
547
|
+
const { stack } = await import("./7n7ddwt4.js");
|
|
548
|
+
return stack(undefined, resolve(schema, table, uses).wheres);
|
|
549
|
+
}
|
|
550
|
+
function noteSafety(session, table, view) {
|
|
551
|
+
if (!safetyInstalled())
|
|
552
|
+
return;
|
|
553
|
+
const model = session.schema.model[table];
|
|
554
|
+
const rules = [];
|
|
555
|
+
const tenancy = session.schema.tenancy;
|
|
556
|
+
if (tenancy !== undefined) {
|
|
557
|
+
for (const rule of tenancy.rules(table, model?.source, session.scope))
|
|
558
|
+
rules.push(rule);
|
|
559
|
+
}
|
|
560
|
+
const archive = archiveRules(model, view);
|
|
561
|
+
if (archive !== undefined) {
|
|
562
|
+
for (const rule of archive)
|
|
563
|
+
rules.push(rule);
|
|
564
|
+
}
|
|
565
|
+
const scope = session.scope;
|
|
566
|
+
const hatches = scope !== undefined && "unscoped" in scope ? [{ name: "unscoped", reason: scope.unscoped }] : undefined;
|
|
567
|
+
runSafety(rules, hatches);
|
|
568
|
+
}
|
|
569
|
+
function tenantSql(session, table) {
|
|
570
|
+
const tenancy = session.schema.tenancy;
|
|
571
|
+
const fields = tenancy === undefined ? [] : tenancy.keys ?? [tenancy.key];
|
|
572
|
+
const sql = new Set;
|
|
573
|
+
for (const field of fields) {
|
|
574
|
+
const column = table.columns.get(field);
|
|
575
|
+
if (column !== undefined)
|
|
576
|
+
sql.add(column.sql);
|
|
577
|
+
}
|
|
578
|
+
return sql;
|
|
579
|
+
}
|
|
580
|
+
function catalogOf(schema) {
|
|
581
|
+
const catalog = schema.catalog;
|
|
582
|
+
if (catalog === undefined)
|
|
583
|
+
fail2("OKM1120", "sync needs the schema catalog.");
|
|
584
|
+
return catalog;
|
|
585
|
+
}
|
|
586
|
+
function storedColumn(catalog, tableSql, columnSql) {
|
|
587
|
+
for (const object of catalog.objects) {
|
|
588
|
+
if (object.kind !== "column")
|
|
589
|
+
continue;
|
|
590
|
+
if (object.identity.parent.name !== tableSql || object.identity.name !== columnSql)
|
|
591
|
+
continue;
|
|
592
|
+
return object.definition;
|
|
593
|
+
}
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
function canOmit(definition) {
|
|
597
|
+
return definition.nullable || definition.defaultExpression !== undefined || definition.identity !== undefined || definition.generated !== undefined;
|
|
598
|
+
}
|
|
599
|
+
function text() {
|
|
600
|
+
const parts = [];
|
|
601
|
+
const params = [];
|
|
602
|
+
let n = 0;
|
|
603
|
+
return {
|
|
604
|
+
text(value) {
|
|
605
|
+
parts.push(value);
|
|
606
|
+
},
|
|
607
|
+
param(encoded) {
|
|
608
|
+
n += 1;
|
|
609
|
+
params.push(encoded);
|
|
610
|
+
parts.push(`$${String(n)}`);
|
|
611
|
+
},
|
|
612
|
+
mark() {},
|
|
613
|
+
finish() {
|
|
614
|
+
return { text: parts.join(""), params };
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
export {
|
|
619
|
+
build,
|
|
620
|
+
execute,
|
|
621
|
+
explain
|
|
622
|
+
};
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
POSTGRES_FLOOR,
|
|
3
|
+
isPostgresMajor,
|
|
4
|
+
parsePostgresFloor,
|
|
5
|
+
serverNeedsPostgres
|
|
6
|
+
} from "./ga9j92qj.js";
|
|
1
7
|
import {
|
|
2
8
|
withHttp,
|
|
3
9
|
attachHttp
|
|
4
|
-
} from "./
|
|
10
|
+
} from "./tbrvgw6z.js";
|
|
5
11
|
import {
|
|
6
12
|
OkmError2
|
|
7
13
|
} from "./52szzakm.js";
|
|
@@ -11,7 +17,7 @@ var versionQuery = "select current_setting('server_version_num'), version(), (se
|
|
|
11
17
|
async function acceptServer(pool, schema, rows, http, requireMeta, options, source) {
|
|
12
18
|
for (const name in schema.model) {
|
|
13
19
|
if (schema.model[name]?.presets !== undefined) {
|
|
14
|
-
(await import("./
|
|
20
|
+
(await import("./m7n9h2a5.js")).checkSchema(schema);
|
|
15
21
|
break;
|
|
16
22
|
}
|
|
17
23
|
}
|
|
@@ -41,20 +47,21 @@ async function acceptServer(pool, schema, rows, http, requireMeta, options, sour
|
|
|
41
47
|
}
|
|
42
48
|
}
|
|
43
49
|
const major = Math.floor(Number(rows[0]?.[0] ?? "0") / 1e4);
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
46
|
-
if (major <
|
|
47
|
-
throw new OkmError2("OKM1803",
|
|
48
|
-
fix: {
|
|
50
|
+
const raw = schema.requires?.postgres;
|
|
51
|
+
if (raw === undefined) {
|
|
52
|
+
if (major < POSTGRES_FLOOR) {
|
|
53
|
+
throw new OkmError2("OKM1803", serverNeedsPostgres(major, POSTGRES_FLOOR), withHttp(http, {
|
|
54
|
+
fix: {
|
|
55
|
+
summary: `Upgrade to PostgreSQL ${String(POSTGRES_FLOOR)} or higher.`
|
|
56
|
+
}
|
|
49
57
|
}));
|
|
50
58
|
}
|
|
51
59
|
return;
|
|
52
60
|
}
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
fix: { summary: "Upgrade the server, or lower requires to a version the server meets." }
|
|
61
|
+
const need = parsePostgresFloor(raw);
|
|
62
|
+
if (!isPostgresMajor(need) || major < need) {
|
|
63
|
+
throw new OkmError2("OKM1802", serverNeedsPostgres(major, isPostgresMajor(need) ? need : POSTGRES_FLOOR), withHttp(http, {
|
|
64
|
+
fix: { summary: "Upgrade the server, or lower minPostgres to a major the server meets." }
|
|
58
65
|
}));
|
|
59
66
|
}
|
|
60
67
|
}
|