@anysoftinc/anydb-sdk 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +336 -0
- package/dist/client.d.ts +167 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +397 -0
- package/dist/nextauth-adapter.d.ts +23 -0
- package/dist/nextauth-adapter.d.ts.map +1 -0
- package/dist/nextauth-adapter.js +340 -0
- package/dist/query-builder.d.ts +126 -0
- package/dist/query-builder.d.ts.map +1 -0
- package/dist/query-builder.js +207 -0
- package/package.json +65 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
// Core types for Datomic data model
|
|
2
|
+
import { parseEDNString as parseEdn } from "edn-data";
|
|
3
|
+
export const sym = (name) => ({ _type: 'symbol', value: name });
|
|
4
|
+
export const uuid = (id) => ({ _type: 'uuid', value: id });
|
|
5
|
+
export const kw = (name) => ({ _type: 'keyword', value: name });
|
|
6
|
+
/**
|
|
7
|
+
* Enhanced EDN stringifier with support for symbolic types
|
|
8
|
+
*/
|
|
9
|
+
export function stringifyEdn(obj) {
|
|
10
|
+
// Handle symbolic types first
|
|
11
|
+
if (typeof obj === "object" && obj !== null && "_type" in obj) {
|
|
12
|
+
const typed = obj;
|
|
13
|
+
switch (typed._type) {
|
|
14
|
+
case 'symbol':
|
|
15
|
+
return typed.value; // ?e, ?id, etc.
|
|
16
|
+
case 'uuid':
|
|
17
|
+
return `#uuid "${typed.value}"`;
|
|
18
|
+
case 'keyword':
|
|
19
|
+
return typed.value.startsWith(':') ? typed.value : `:${typed.value}`;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (typeof obj === "string") {
|
|
23
|
+
// Keywords and symbols don't get quoted, strings do
|
|
24
|
+
if (obj.startsWith(":") || obj.startsWith("?")) {
|
|
25
|
+
return obj;
|
|
26
|
+
}
|
|
27
|
+
return `"${obj.replace(/"/g, '\\"')}"`;
|
|
28
|
+
}
|
|
29
|
+
if (typeof obj === "number")
|
|
30
|
+
return String(obj);
|
|
31
|
+
if (typeof obj === "boolean")
|
|
32
|
+
return String(obj);
|
|
33
|
+
if (obj === null || obj === undefined)
|
|
34
|
+
return "nil";
|
|
35
|
+
if (obj instanceof Date)
|
|
36
|
+
return `#inst "${obj.toISOString()}"`;
|
|
37
|
+
if (Array.isArray(obj)) {
|
|
38
|
+
return "[" + obj.map(stringifyEdn).join(" ") + "]";
|
|
39
|
+
}
|
|
40
|
+
if (typeof obj === "object") {
|
|
41
|
+
const pairs = Object.entries(obj).map(([key, value]) => {
|
|
42
|
+
// Handle Datomic keyword keys
|
|
43
|
+
const ednKey = key.startsWith(":") ? key : `:${key}`;
|
|
44
|
+
return `${ednKey} ${stringifyEdn(value)}`;
|
|
45
|
+
});
|
|
46
|
+
return "{" + pairs.join(" ") + "}";
|
|
47
|
+
}
|
|
48
|
+
return String(obj);
|
|
49
|
+
}
|
|
50
|
+
// Main SDK Client
|
|
51
|
+
export class DatomicClient {
|
|
52
|
+
constructor(config) {
|
|
53
|
+
this.config = config;
|
|
54
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, ""); // Remove trailing slash
|
|
55
|
+
}
|
|
56
|
+
// Normalize EDN parsed values to JS-friendly shapes
|
|
57
|
+
normalizeEdn(value) {
|
|
58
|
+
if (value === null || value === undefined)
|
|
59
|
+
return value;
|
|
60
|
+
// edn-data often wraps symbols/keywords/strings as { key: '...' }
|
|
61
|
+
if (typeof value === 'object' && value && typeof value.key === 'string') {
|
|
62
|
+
return value.key;
|
|
63
|
+
}
|
|
64
|
+
// Sets -> Arrays
|
|
65
|
+
if (value instanceof Set) {
|
|
66
|
+
return Array.from(value, (v) => this.normalizeEdn(v));
|
|
67
|
+
}
|
|
68
|
+
// edn-data set representation: { set: [...] }
|
|
69
|
+
if (typeof value === 'object' && value && Array.isArray(value.set)) {
|
|
70
|
+
return value.set.map((v) => this.normalizeEdn(v));
|
|
71
|
+
}
|
|
72
|
+
// edn-data map representation: { map: [[k,v] ...] }
|
|
73
|
+
if (typeof value === 'object' && value && Array.isArray(value.map)) {
|
|
74
|
+
const out = {};
|
|
75
|
+
for (const [k, v] of value.map) {
|
|
76
|
+
const keyNorm = this.normalizeEdn(k);
|
|
77
|
+
const keyStr = typeof keyNorm === 'string' ? keyNorm : String(keyNorm);
|
|
78
|
+
out[keyStr] = this.normalizeEdn(v);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
// Arrays
|
|
83
|
+
if (Array.isArray(value)) {
|
|
84
|
+
return value.map((v) => this.normalizeEdn(v));
|
|
85
|
+
}
|
|
86
|
+
// Objects: attempt to descend shallowly
|
|
87
|
+
if (typeof value === 'object') {
|
|
88
|
+
// Convert common UUID representations to a consistent shape
|
|
89
|
+
if (value.tag === 'uuid' && typeof value.val === 'string') {
|
|
90
|
+
return { _type: 'uuid', value: value.val };
|
|
91
|
+
}
|
|
92
|
+
if (value.type === 'uuid' && typeof value.value === 'string') {
|
|
93
|
+
return value; // already in {_type:'uuid', value}
|
|
94
|
+
}
|
|
95
|
+
// Convert inst tag to Date
|
|
96
|
+
if (value.tag === 'inst' && typeof value.val === 'string') {
|
|
97
|
+
const d = new Date(value.val);
|
|
98
|
+
return isNaN(d.getTime()) ? value.val : d;
|
|
99
|
+
}
|
|
100
|
+
const out = Array.isArray(value) ? [] : {};
|
|
101
|
+
for (const [k, v] of Object.entries(value)) {
|
|
102
|
+
out[k] = this.normalizeEdn(v);
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
async request(endpoint, options = {}) {
|
|
109
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
110
|
+
// Convert body to EDN if present
|
|
111
|
+
const processedOptions = { ...options };
|
|
112
|
+
if (processedOptions.body && typeof processedOptions.body !== "string") {
|
|
113
|
+
processedOptions.body = stringifyEdn(processedOptions.body);
|
|
114
|
+
}
|
|
115
|
+
const response = await fetch(url, {
|
|
116
|
+
headers: {
|
|
117
|
+
"Content-Type": "application/edn",
|
|
118
|
+
Accept: "application/edn",
|
|
119
|
+
...this.config.headers,
|
|
120
|
+
...options.headers,
|
|
121
|
+
},
|
|
122
|
+
...processedOptions,
|
|
123
|
+
});
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
throw new Error(`Datomic API error: ${response.status} ${response.statusText}`);
|
|
126
|
+
}
|
|
127
|
+
// Parse EDN response
|
|
128
|
+
const responseText = await response.text();
|
|
129
|
+
const parsed = parseEdn(responseText);
|
|
130
|
+
return this.normalizeEdn(parsed);
|
|
131
|
+
}
|
|
132
|
+
// List databases in a storage
|
|
133
|
+
async listDatabases(storageAlias) {
|
|
134
|
+
return this.request(`/data/${storageAlias}/`);
|
|
135
|
+
}
|
|
136
|
+
// Create database
|
|
137
|
+
async createDatabase(storageAlias, dbName) {
|
|
138
|
+
return this.request(`/data/${storageAlias}/`, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
body: { ":db-name": dbName },
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// Delete database
|
|
144
|
+
async deleteDatabase(storageAlias, dbName) {
|
|
145
|
+
return this.request(`/data/${storageAlias}/${dbName}/`, {
|
|
146
|
+
method: "DELETE",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// Transaction operations
|
|
150
|
+
async transact(storageAlias, dbName, txData) {
|
|
151
|
+
return this.request(`/data/${storageAlias}/${dbName}/`, {
|
|
152
|
+
method: "POST",
|
|
153
|
+
body: { ":tx-data": txData },
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
// Retrieve database info
|
|
157
|
+
async databaseInfo(storageAlias, dbName, basisT = "-") {
|
|
158
|
+
return this.request(`/data/${storageAlias}/${dbName}/${basisT}/`);
|
|
159
|
+
}
|
|
160
|
+
// Datoms operations
|
|
161
|
+
async datoms(storageAlias, dbName, basisT, options) {
|
|
162
|
+
const params = new URLSearchParams();
|
|
163
|
+
// Required parameter
|
|
164
|
+
params.append("index", options.index);
|
|
165
|
+
// Optional parameters
|
|
166
|
+
if (options.e !== undefined)
|
|
167
|
+
params.append("e", String(options.e));
|
|
168
|
+
if (options.a !== undefined)
|
|
169
|
+
params.append("a", String(options.a));
|
|
170
|
+
if (options.v !== undefined)
|
|
171
|
+
params.append("v", String(options.v));
|
|
172
|
+
if (options.start !== undefined)
|
|
173
|
+
params.append("start", String(options.start));
|
|
174
|
+
if (options.end !== undefined)
|
|
175
|
+
params.append("end", String(options.end));
|
|
176
|
+
if (options.limit !== undefined)
|
|
177
|
+
params.append("limit", String(options.limit));
|
|
178
|
+
if (options.offset !== undefined)
|
|
179
|
+
params.append("offset", String(options.offset));
|
|
180
|
+
if (options["as-of"] !== undefined)
|
|
181
|
+
params.append("as-of", String(options["as-of"]));
|
|
182
|
+
if (options.since !== undefined)
|
|
183
|
+
params.append("since", String(options.since));
|
|
184
|
+
if (options.history !== undefined)
|
|
185
|
+
params.append("history", String(options.history));
|
|
186
|
+
return this.request(`/data/${storageAlias}/${dbName}/${basisT}/datoms?${params.toString()}`);
|
|
187
|
+
}
|
|
188
|
+
// Entity operations
|
|
189
|
+
async entity(storageAlias, dbName, basisT, entityId, options) {
|
|
190
|
+
const params = new URLSearchParams();
|
|
191
|
+
params.append("e", String(entityId));
|
|
192
|
+
if (options?.["as-of"] !== undefined)
|
|
193
|
+
params.append("as-of", String(options["as-of"]));
|
|
194
|
+
if (options?.since !== undefined)
|
|
195
|
+
params.append("since", String(options.since));
|
|
196
|
+
return this.request(`/data/${storageAlias}/${dbName}/${basisT}/entity?${params.toString()}`);
|
|
197
|
+
}
|
|
198
|
+
// Query operations (GET version for simple queries)
|
|
199
|
+
async queryGet(query, args, options) {
|
|
200
|
+
const params = new URLSearchParams();
|
|
201
|
+
params.append("q", stringifyEdn(query));
|
|
202
|
+
params.append("args", stringifyEdn(args));
|
|
203
|
+
if (options?.limit !== undefined)
|
|
204
|
+
params.append("limit", String(options.limit));
|
|
205
|
+
if (options?.offset !== undefined)
|
|
206
|
+
params.append("offset", String(options.offset));
|
|
207
|
+
return this.request(`/api/query?${params.toString()}`);
|
|
208
|
+
}
|
|
209
|
+
// Query operations (POST version for complex queries)
|
|
210
|
+
async query(query, args, options) {
|
|
211
|
+
const body = {
|
|
212
|
+
":q": query,
|
|
213
|
+
":args": args,
|
|
214
|
+
};
|
|
215
|
+
if (options?.limit !== undefined)
|
|
216
|
+
body[":limit"] = options.limit;
|
|
217
|
+
if (options?.offset !== undefined)
|
|
218
|
+
body[":offset"] = options.offset;
|
|
219
|
+
const res = await this.request("/api/query", {
|
|
220
|
+
method: "POST",
|
|
221
|
+
body: body,
|
|
222
|
+
});
|
|
223
|
+
// Ensure result is array of rows, not Set
|
|
224
|
+
if (res instanceof Set) {
|
|
225
|
+
return Array.from(res);
|
|
226
|
+
}
|
|
227
|
+
return res;
|
|
228
|
+
}
|
|
229
|
+
// Symbolic query operations
|
|
230
|
+
async querySymbolic(query, args = [], options) {
|
|
231
|
+
// Convert symbolic query to EDN vector format
|
|
232
|
+
const ednQuery = [":find", ...query.find];
|
|
233
|
+
if (query.in && query.in.length > 0) {
|
|
234
|
+
ednQuery.push(":in", ...query.in);
|
|
235
|
+
}
|
|
236
|
+
ednQuery.push(":where", ...query.where);
|
|
237
|
+
return this.query(ednQuery, args, options);
|
|
238
|
+
}
|
|
239
|
+
// Server-Sent Events for transaction reports
|
|
240
|
+
subscribeToEvents(storageAlias, dbName, onEvent, onError) {
|
|
241
|
+
const eventSource = new EventSource(`${this.baseUrl}/events/${storageAlias}/${dbName}`);
|
|
242
|
+
eventSource.onmessage = onEvent;
|
|
243
|
+
if (onError)
|
|
244
|
+
eventSource.onerror = onError;
|
|
245
|
+
return eventSource;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Utility functions for common Datomic operations
|
|
249
|
+
export class DatomicUtils {
|
|
250
|
+
static tempId(partition = "db.part/user") {
|
|
251
|
+
return `tempid:${partition}:${Math.random().toString(36).substr(2, 9)}`;
|
|
252
|
+
}
|
|
253
|
+
static keyword(namespace, name) {
|
|
254
|
+
return `:${namespace}/${name}`;
|
|
255
|
+
}
|
|
256
|
+
static createEntity(attributes, tempId) {
|
|
257
|
+
const id = tempId || this.tempId();
|
|
258
|
+
return Object.entries(attributes).map(([attr, value]) => ({
|
|
259
|
+
"db/id": id,
|
|
260
|
+
[attr]: value,
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
static retractEntity(entityId) {
|
|
264
|
+
return [[":db/retractEntity", entityId]];
|
|
265
|
+
}
|
|
266
|
+
static retractAttribute(entityId, attribute, value) {
|
|
267
|
+
if (value !== undefined) {
|
|
268
|
+
return [[":db/retract", entityId, attribute, value]];
|
|
269
|
+
}
|
|
270
|
+
return [[":db/retract", entityId, attribute]];
|
|
271
|
+
}
|
|
272
|
+
static addAttribute(entityId, attribute, value) {
|
|
273
|
+
return [[":db/add", entityId, attribute, value]];
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
export class SchemaBuilder {
|
|
277
|
+
static attribute(def) {
|
|
278
|
+
return {
|
|
279
|
+
"db/id": DatomicUtils.tempId(),
|
|
280
|
+
...def,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
static enum(ident, doc) {
|
|
284
|
+
const enumDef = {
|
|
285
|
+
"db/id": DatomicUtils.tempId(),
|
|
286
|
+
":db/ident": ident,
|
|
287
|
+
};
|
|
288
|
+
if (doc) {
|
|
289
|
+
enumDef[":db/doc"] = doc;
|
|
290
|
+
}
|
|
291
|
+
return enumDef;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// EDN template literal for natural Datalog queries
|
|
295
|
+
export function edn(strings, ...values) {
|
|
296
|
+
// Combine template strings and interpolated values
|
|
297
|
+
let ednString = '';
|
|
298
|
+
for (let i = 0; i < strings.length; i++) {
|
|
299
|
+
ednString += strings[i];
|
|
300
|
+
if (i < values.length) {
|
|
301
|
+
const value = values[i];
|
|
302
|
+
// Convert interpolated values to EDN strings
|
|
303
|
+
ednString += stringifyEdn(value);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Use the proper EDN parser
|
|
307
|
+
const parsed = parseEdn(ednString);
|
|
308
|
+
// Apply normalization similar to the client's normalizeEdn method
|
|
309
|
+
return normalizeEdnForQuery(parsed);
|
|
310
|
+
}
|
|
311
|
+
function normalizeEdnForQuery(value) {
|
|
312
|
+
if (value === null || value === undefined)
|
|
313
|
+
return value;
|
|
314
|
+
// Handle edn-data keyword representations: {key: "find"} -> ":find"
|
|
315
|
+
if (typeof value === 'object' && value && typeof value.key === 'string') {
|
|
316
|
+
const key = value.key;
|
|
317
|
+
return key.startsWith(':') ? key : `:${key}`;
|
|
318
|
+
}
|
|
319
|
+
// Handle edn-data symbol representations: {sym: "?e"} -> "?e"
|
|
320
|
+
if (typeof value === 'object' && value && typeof value.sym === 'string') {
|
|
321
|
+
return value.sym;
|
|
322
|
+
}
|
|
323
|
+
// Handle edn-data list representations: {list: [...]} -> [...]
|
|
324
|
+
if (typeof value === 'object' && value && Array.isArray(value.list)) {
|
|
325
|
+
return value.list.map((v) => normalizeEdnForQuery(v));
|
|
326
|
+
}
|
|
327
|
+
// Sets -> Arrays
|
|
328
|
+
if (value instanceof Set) {
|
|
329
|
+
return Array.from(value, (v) => normalizeEdnForQuery(v));
|
|
330
|
+
}
|
|
331
|
+
// edn-data set representation: { set: [...] }
|
|
332
|
+
if (typeof value === 'object' && value && Array.isArray(value.set)) {
|
|
333
|
+
return value.set.map((v) => normalizeEdnForQuery(v));
|
|
334
|
+
}
|
|
335
|
+
// Arrays
|
|
336
|
+
if (Array.isArray(value)) {
|
|
337
|
+
return value.map((v) => normalizeEdnForQuery(v));
|
|
338
|
+
}
|
|
339
|
+
// Convert common UUID representations
|
|
340
|
+
if (typeof value === 'object' && value && value.tag === 'uuid' && typeof value.val === 'string') {
|
|
341
|
+
return { _type: 'uuid', value: value.val };
|
|
342
|
+
}
|
|
343
|
+
return value;
|
|
344
|
+
}
|
|
345
|
+
// Convenience class for working with a specific database
|
|
346
|
+
export class DatomicDatabase {
|
|
347
|
+
constructor(client, storageAlias, dbName) {
|
|
348
|
+
this.client = client;
|
|
349
|
+
this.storageAlias = storageAlias;
|
|
350
|
+
this.dbName = dbName;
|
|
351
|
+
}
|
|
352
|
+
async transact(txData) {
|
|
353
|
+
return this.client.transact(this.storageAlias, this.dbName, txData);
|
|
354
|
+
}
|
|
355
|
+
async info(basisT = "-") {
|
|
356
|
+
return this.client.databaseInfo(this.storageAlias, this.dbName, basisT);
|
|
357
|
+
}
|
|
358
|
+
async datoms(index, basisT = "-", options) {
|
|
359
|
+
return this.client.datoms(this.storageAlias, this.dbName, basisT, {
|
|
360
|
+
index,
|
|
361
|
+
...options,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
async entity(entityId, basisT = "-", options) {
|
|
365
|
+
return this.client.entity(this.storageAlias, this.dbName, basisT, entityId, options);
|
|
366
|
+
}
|
|
367
|
+
async query(query, ...args) {
|
|
368
|
+
const dbDescriptor = {
|
|
369
|
+
"db/alias": `${this.storageAlias}/${this.dbName}`,
|
|
370
|
+
};
|
|
371
|
+
return this.client.query(query, [dbDescriptor, ...args]);
|
|
372
|
+
}
|
|
373
|
+
async querySymbolic(query, ...args) {
|
|
374
|
+
const dbDescriptor = {
|
|
375
|
+
"db/alias": `${this.storageAlias}/${this.dbName}`,
|
|
376
|
+
};
|
|
377
|
+
return this.client.querySymbolic(query, [dbDescriptor, ...args]);
|
|
378
|
+
}
|
|
379
|
+
subscribeToEvents(onEvent, onError) {
|
|
380
|
+
return this.client.subscribeToEvents(this.storageAlias, this.dbName, onEvent, onError);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// Factory functions
|
|
384
|
+
export function createDatomicClient(config) {
|
|
385
|
+
return new DatomicClient(config);
|
|
386
|
+
}
|
|
387
|
+
export function createDatomicDatabase(client, storageAlias, dbName) {
|
|
388
|
+
return new DatomicDatabase(client, storageAlias, dbName);
|
|
389
|
+
}
|
|
390
|
+
// Small helpers
|
|
391
|
+
export function pluckFirstColumn(rows) {
|
|
392
|
+
if (!Array.isArray(rows))
|
|
393
|
+
return [];
|
|
394
|
+
return rows.map((r) => (Array.isArray(r) ? r[0] : r));
|
|
395
|
+
}
|
|
396
|
+
// Export everything
|
|
397
|
+
export default DatomicClient;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Adapter } from "next-auth/adapters";
|
|
2
|
+
import type { DatomicDatabase } from "./client";
|
|
3
|
+
/**
|
|
4
|
+
* Creates a NextAuth.js adapter for AnyDB/Datomic
|
|
5
|
+
*
|
|
6
|
+
* @param db - DatomicDatabase instance
|
|
7
|
+
* @returns NextAuth.js Adapter
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { createDatomicDatabase } from '@anysoftinc/anydb-sdk';
|
|
12
|
+
* import { AnyDBAdapter } from '@anysoftinc/anydb-sdk/nextauth-adapter';
|
|
13
|
+
*
|
|
14
|
+
* const db = createDatomicDatabase(client, 'storage', 'auth-db');
|
|
15
|
+
*
|
|
16
|
+
* export default NextAuth({
|
|
17
|
+
* adapter: AnyDBAdapter(db),
|
|
18
|
+
* // ... other config
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function AnyDBAdapter(db: DatomicDatabase): Adapter;
|
|
23
|
+
//# sourceMappingURL=nextauth-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nextauth-adapter.d.ts","sourceRoot":"","sources":["../src/nextauth-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAkB,MAAM,oBAAoB,CAAC;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAmHhD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,eAAe,GAAG,OAAO,CA4PzD"}
|