@hatk/hatk 0.0.1-alpha.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/dist/backfill.d.ts +11 -0
- package/dist/backfill.d.ts.map +1 -0
- package/dist/backfill.js +328 -0
- package/dist/car.d.ts +5 -0
- package/dist/car.d.ts.map +1 -0
- package/dist/car.js +52 -0
- package/dist/cbor.d.ts +7 -0
- package/dist/cbor.d.ts.map +1 -0
- package/dist/cbor.js +89 -0
- package/dist/cid.d.ts +4 -0
- package/dist/cid.d.ts.map +1 -0
- package/dist/cid.js +39 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1663 -0
- package/dist/config.d.ts +47 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +43 -0
- package/dist/db.d.ts +134 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +1361 -0
- package/dist/feeds.d.ts +95 -0
- package/dist/feeds.d.ts.map +1 -0
- package/dist/feeds.js +144 -0
- package/dist/fts.d.ts +20 -0
- package/dist/fts.d.ts.map +1 -0
- package/dist/fts.js +762 -0
- package/dist/hydrate.d.ts +23 -0
- package/dist/hydrate.d.ts.map +1 -0
- package/dist/hydrate.js +75 -0
- package/dist/indexer.d.ts +14 -0
- package/dist/indexer.d.ts.map +1 -0
- package/dist/indexer.js +316 -0
- package/dist/labels.d.ts +29 -0
- package/dist/labels.d.ts.map +1 -0
- package/dist/labels.js +111 -0
- package/dist/lex-types.d.ts +401 -0
- package/dist/lex-types.d.ts.map +1 -0
- package/dist/lex-types.js +4 -0
- package/dist/lexicon-resolve.d.ts +14 -0
- package/dist/lexicon-resolve.d.ts.map +1 -0
- package/dist/lexicon-resolve.js +280 -0
- package/dist/logger.d.ts +4 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +23 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +148 -0
- package/dist/mst.d.ts +6 -0
- package/dist/mst.d.ts.map +1 -0
- package/dist/mst.js +30 -0
- package/dist/oauth/client.d.ts +16 -0
- package/dist/oauth/client.d.ts.map +1 -0
- package/dist/oauth/client.js +54 -0
- package/dist/oauth/crypto.d.ts +28 -0
- package/dist/oauth/crypto.d.ts.map +1 -0
- package/dist/oauth/crypto.js +101 -0
- package/dist/oauth/db.d.ts +47 -0
- package/dist/oauth/db.d.ts.map +1 -0
- package/dist/oauth/db.js +139 -0
- package/dist/oauth/discovery.d.ts +22 -0
- package/dist/oauth/discovery.d.ts.map +1 -0
- package/dist/oauth/discovery.js +50 -0
- package/dist/oauth/dpop.d.ts +11 -0
- package/dist/oauth/dpop.d.ts.map +1 -0
- package/dist/oauth/dpop.js +56 -0
- package/dist/oauth/hooks.d.ts +10 -0
- package/dist/oauth/hooks.d.ts.map +1 -0
- package/dist/oauth/hooks.js +40 -0
- package/dist/oauth/server.d.ts +86 -0
- package/dist/oauth/server.d.ts.map +1 -0
- package/dist/oauth/server.js +572 -0
- package/dist/opengraph.d.ts +34 -0
- package/dist/opengraph.d.ts.map +1 -0
- package/dist/opengraph.js +198 -0
- package/dist/schema.d.ts +51 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +358 -0
- package/dist/seed.d.ts +29 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +86 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +1024 -0
- package/dist/setup.d.ts +8 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +48 -0
- package/dist/test-browser.d.ts +14 -0
- package/dist/test-browser.d.ts.map +1 -0
- package/dist/test-browser.js +26 -0
- package/dist/test.d.ts +47 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +256 -0
- package/dist/views.d.ts +40 -0
- package/dist/views.d.ts.map +1 -0
- package/dist/views.js +178 -0
- package/dist/vite-plugin.d.ts +5 -0
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.js +86 -0
- package/dist/xrpc-client.d.ts +18 -0
- package/dist/xrpc-client.d.ts.map +1 -0
- package/dist/xrpc-client.js +54 -0
- package/dist/xrpc.d.ts +53 -0
- package/dist/xrpc.d.ts.map +1 -0
- package/dist/xrpc.js +139 -0
- package/fonts/Inter-Regular.woff +0 -0
- package/package.json +41 -0
- package/public/admin-auth.js +320 -0
- package/public/admin.html +2166 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { resolve } from 'node:path';
|
|
10
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
11
|
+
import { log } from "./logger.js";
|
|
12
|
+
import satori from 'satori';
|
|
13
|
+
import { Resvg } from '@resvg/resvg-js';
|
|
14
|
+
import { querySQL, runSQL, packCursor, unpackCursor, isTakendownDid, filterTakendownDids, searchRecords, findUriByFields, lookupByFieldBatch, countByFieldBatch, queryLabelsForUris, } from "./db.js";
|
|
15
|
+
import { resolveRecords } from "./hydrate.js";
|
|
16
|
+
import { blobUrl } from "./xrpc.js";
|
|
17
|
+
const handlers = [];
|
|
18
|
+
const pageRoutes = [];
|
|
19
|
+
let defaultFont = null;
|
|
20
|
+
const cache = new Map();
|
|
21
|
+
const CACHE_TTL = 5 * 60 * 1000;
|
|
22
|
+
const CACHE_MAX = 200;
|
|
23
|
+
function compilePath(path) {
|
|
24
|
+
const paramNames = [];
|
|
25
|
+
const re = path.replace(/:([^/]+)/g, (_, name) => {
|
|
26
|
+
paramNames.push(name);
|
|
27
|
+
return '([^/]+)';
|
|
28
|
+
});
|
|
29
|
+
return { pattern: new RegExp(`^${re}$`), paramNames };
|
|
30
|
+
}
|
|
31
|
+
export async function initOpengraph(ogDir) {
|
|
32
|
+
// Load default font
|
|
33
|
+
try {
|
|
34
|
+
const fontPath = resolve(import.meta.dirname, '..', 'fonts', 'Inter-Regular.woff');
|
|
35
|
+
const fontData = readFileSync(fontPath);
|
|
36
|
+
defaultFont = { name: 'Inter', data: fontData.buffer, weight: 400, style: 'normal' };
|
|
37
|
+
log('[opengraph] loaded default font: Inter');
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
console.warn('[opengraph] no default font found at fonts/Inter-Regular.woff — scripts must provide fonts');
|
|
41
|
+
}
|
|
42
|
+
let files;
|
|
43
|
+
try {
|
|
44
|
+
files = readdirSync(ogDir)
|
|
45
|
+
.filter((f) => (f.endsWith('.ts') || f.endsWith('.js')) && !f.startsWith('_'))
|
|
46
|
+
.sort();
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
const name = file.replace(/\.(ts|js)$/, '');
|
|
53
|
+
const scriptPath = resolve(ogDir, file);
|
|
54
|
+
const mod = await import(__rewriteRelativeImportExtension(scriptPath));
|
|
55
|
+
const handler = mod.default;
|
|
56
|
+
if (!handler.path) {
|
|
57
|
+
console.warn(`[opengraph] ${file} missing 'path' export, skipping`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const { pattern, paramNames } = compilePath(handler.path);
|
|
61
|
+
handlers.push({
|
|
62
|
+
name,
|
|
63
|
+
path: handler.path,
|
|
64
|
+
pattern,
|
|
65
|
+
paramNames,
|
|
66
|
+
execute: async (params) => {
|
|
67
|
+
const ctx = {
|
|
68
|
+
db: { query: querySQL, run: runSQL },
|
|
69
|
+
params,
|
|
70
|
+
input: {},
|
|
71
|
+
limit: 1,
|
|
72
|
+
viewer: null,
|
|
73
|
+
packCursor,
|
|
74
|
+
unpackCursor,
|
|
75
|
+
isTakendown: isTakendownDid,
|
|
76
|
+
filterTakendownDids,
|
|
77
|
+
search: searchRecords,
|
|
78
|
+
resolve: resolveRecords,
|
|
79
|
+
lookup: async (collection, field, values) => {
|
|
80
|
+
if (values.length === 0)
|
|
81
|
+
return new Map();
|
|
82
|
+
const unique = [...new Set(values.filter(Boolean))];
|
|
83
|
+
return lookupByFieldBatch(collection, field, unique);
|
|
84
|
+
},
|
|
85
|
+
count: async (collection, field, values) => {
|
|
86
|
+
if (values.length === 0)
|
|
87
|
+
return new Map();
|
|
88
|
+
const unique = [...new Set(values.filter(Boolean))];
|
|
89
|
+
return countByFieldBatch(collection, field, unique);
|
|
90
|
+
},
|
|
91
|
+
exists: async (collection, filters) => {
|
|
92
|
+
const conditions = Object.entries(filters).map(([field, value]) => ({ field, value }));
|
|
93
|
+
const uri = await findUriByFields(collection, conditions);
|
|
94
|
+
return uri !== null;
|
|
95
|
+
},
|
|
96
|
+
labels: queryLabelsForUris,
|
|
97
|
+
blobUrl,
|
|
98
|
+
};
|
|
99
|
+
ctx.fetchImage = async (url) => {
|
|
100
|
+
try {
|
|
101
|
+
const resp = await fetch(url, { redirect: 'follow' });
|
|
102
|
+
if (!resp.ok)
|
|
103
|
+
return null;
|
|
104
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
105
|
+
const contentType = resp.headers.get('content-type') || 'image/jpeg';
|
|
106
|
+
return `data:${contentType};base64,${buf.toString('base64')}`;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const result = await handler.generate(ctx);
|
|
113
|
+
const element = result.element;
|
|
114
|
+
const options = {
|
|
115
|
+
width: 1200,
|
|
116
|
+
height: 630,
|
|
117
|
+
...result.options,
|
|
118
|
+
fonts: [...(defaultFont ? [defaultFont] : []), ...(result.options?.fonts || [])],
|
|
119
|
+
};
|
|
120
|
+
const svg = await satori(element, options);
|
|
121
|
+
return { svg, meta: result.meta };
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
log(`[opengraph] discovered: ${name} → ${handler.path}`);
|
|
125
|
+
const pagePath = handler.path.replace(/^\/og/, '');
|
|
126
|
+
if (pagePath !== handler.path) {
|
|
127
|
+
const compiled = compilePath(pagePath);
|
|
128
|
+
pageRoutes.push({ ogPath: handler.path, pattern: compiled.pattern, paramNames: compiled.paramNames, name });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export async function handleOpengraphRequest(pathname) {
|
|
133
|
+
const cached = cache.get(pathname);
|
|
134
|
+
if (cached && cached.expires > Date.now())
|
|
135
|
+
return cached.png;
|
|
136
|
+
for (const handler of handlers) {
|
|
137
|
+
const match = pathname.match(handler.pattern);
|
|
138
|
+
if (!match)
|
|
139
|
+
continue;
|
|
140
|
+
const params = {};
|
|
141
|
+
handler.paramNames.forEach((name, i) => {
|
|
142
|
+
params[name] = decodeURIComponent(match[i + 1]);
|
|
143
|
+
});
|
|
144
|
+
try {
|
|
145
|
+
const { svg, meta } = await handler.execute(params);
|
|
146
|
+
const png = new Resvg(svg, { fitTo: { mode: 'width', value: 1200 } }).render().asPng();
|
|
147
|
+
if (cache.size >= CACHE_MAX) {
|
|
148
|
+
const oldest = cache.keys().next().value;
|
|
149
|
+
if (oldest)
|
|
150
|
+
cache.delete(oldest);
|
|
151
|
+
}
|
|
152
|
+
cache.set(pathname, { png, meta, expires: Date.now() + CACHE_TTL });
|
|
153
|
+
return png;
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
console.error(`[opengraph] error in ${handler.name}:`, err.message);
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
export function buildOgMeta(pathname, origin) {
|
|
163
|
+
for (const route of pageRoutes) {
|
|
164
|
+
const match = pathname.match(route.pattern);
|
|
165
|
+
if (!match)
|
|
166
|
+
continue;
|
|
167
|
+
let ogImagePath = route.ogPath;
|
|
168
|
+
for (let i = 0; i < route.paramNames.length; i++) {
|
|
169
|
+
ogImagePath = ogImagePath.replace(`:${route.paramNames[i]}`, match[i + 1]);
|
|
170
|
+
}
|
|
171
|
+
const params = {};
|
|
172
|
+
route.paramNames.forEach((name, i) => {
|
|
173
|
+
params[name] = decodeURIComponent(match[i + 1]);
|
|
174
|
+
});
|
|
175
|
+
const cached = cache.get(ogImagePath);
|
|
176
|
+
const cachedMeta = cached && cached.expires > Date.now() ? cached.meta : undefined;
|
|
177
|
+
const title = cachedMeta?.title || Object.values(params).join(' \u00b7 ');
|
|
178
|
+
const description = cachedMeta?.description || '';
|
|
179
|
+
const esc = (s) => s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');
|
|
180
|
+
const imageUrl = `${origin}${ogImagePath}`;
|
|
181
|
+
const pageUrl = `${origin}${pathname}`;
|
|
182
|
+
const tags = [
|
|
183
|
+
`<meta property="og:title" content="${esc(title)}">`,
|
|
184
|
+
...(description ? [`<meta property="og:description" content="${esc(description)}">`] : []),
|
|
185
|
+
`<meta property="og:image" content="${esc(imageUrl)}">`,
|
|
186
|
+
`<meta property="og:image:width" content="1200">`,
|
|
187
|
+
`<meta property="og:image:height" content="630">`,
|
|
188
|
+
`<meta property="og:url" content="${esc(pageUrl)}">`,
|
|
189
|
+
`<meta property="og:type" content="website">`,
|
|
190
|
+
`<meta name="twitter:card" content="summary_large_image">`,
|
|
191
|
+
`<meta name="twitter:title" content="${esc(title)}">`,
|
|
192
|
+
...(description ? [`<meta name="twitter:description" content="${esc(description)}">`] : []),
|
|
193
|
+
`<meta name="twitter:image" content="${esc(imageUrl)}">`,
|
|
194
|
+
];
|
|
195
|
+
return tags.join('\n ');
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface ColumnDef {
|
|
2
|
+
name: string;
|
|
3
|
+
originalName: string;
|
|
4
|
+
duckdbType: string;
|
|
5
|
+
notNull: boolean;
|
|
6
|
+
isRef: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface UnionBranchSchema {
|
|
9
|
+
type: string;
|
|
10
|
+
branchName: string;
|
|
11
|
+
tableName: string;
|
|
12
|
+
columns: ColumnDef[];
|
|
13
|
+
isArray: boolean;
|
|
14
|
+
arrayField?: string;
|
|
15
|
+
wrapperField?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface UnionFieldSchema {
|
|
18
|
+
fieldName: string;
|
|
19
|
+
branches: UnionBranchSchema[];
|
|
20
|
+
}
|
|
21
|
+
export interface TableSchema {
|
|
22
|
+
collection: string;
|
|
23
|
+
tableName: string;
|
|
24
|
+
columns: ColumnDef[];
|
|
25
|
+
refColumns: string[];
|
|
26
|
+
children: ChildTableSchema[];
|
|
27
|
+
unions: UnionFieldSchema[];
|
|
28
|
+
}
|
|
29
|
+
export interface ChildTableSchema {
|
|
30
|
+
parentCollection: string;
|
|
31
|
+
fieldName: string;
|
|
32
|
+
tableName: string;
|
|
33
|
+
columns: ColumnDef[];
|
|
34
|
+
}
|
|
35
|
+
export declare function toSnakeCase(str: string): string;
|
|
36
|
+
export declare function loadLexicons(lexiconsDir: string): Map<string, any>;
|
|
37
|
+
/**
|
|
38
|
+
* Discover collections by scanning lexicons for record-type definitions.
|
|
39
|
+
*/
|
|
40
|
+
export declare function discoverCollections(lexicons: Map<string, any>): string[];
|
|
41
|
+
export declare function storeLexicons(lexicons: Map<string, any>): void;
|
|
42
|
+
export declare function getLexicon(nsid: string): any | undefined;
|
|
43
|
+
export declare function getAllLexicons(): Array<{
|
|
44
|
+
nsid: string;
|
|
45
|
+
lexicon: any;
|
|
46
|
+
}>;
|
|
47
|
+
/** Get all stored lexicons as a flat array (for @bigmoves/lexicon validators) */
|
|
48
|
+
export declare function getLexiconArray(): any[];
|
|
49
|
+
export declare function generateTableSchema(nsid: string, lexicon: any, lexicons?: Map<string, any>): TableSchema;
|
|
50
|
+
export declare function generateCreateTableSQL(schema: TableSchema): string;
|
|
51
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,SAAS,EAAE,CAAA;IACpB,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,iBAAiB,EAAE,CAAA;CAC9B;AAED,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,SAAS,EAAE,CAAA;IACpB,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,QAAQ,EAAE,gBAAgB,EAAE,CAAA;IAC5B,MAAM,EAAE,gBAAgB,EAAE,CAAA;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,gBAAgB,EAAE,MAAM,CAAA;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,SAAS,EAAE,CAAA;CACrB;AAGD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE/C;AA8CD,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CASlE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,EAAE,CASxE;AAID,wBAAgB,aAAa,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAI9D;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS,CAExD;AAED,wBAAgB,cAAc,IAAI,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,CAEtE;AAED,iFAAiF;AACjF,wBAAgB,eAAe,IAAI,GAAG,EAAE,CAEvC;AAuHD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,WAAW,CA0GxG;AAGD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAoElE"}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
// Convert camelCase to snake_case
|
|
4
|
+
export function toSnakeCase(str) {
|
|
5
|
+
return str.replace(/([A-Z])/g, '_$1').toLowerCase();
|
|
6
|
+
}
|
|
7
|
+
function mapType(prop) {
|
|
8
|
+
if (prop.type === 'string') {
|
|
9
|
+
if (prop.format === 'datetime')
|
|
10
|
+
return { duckdbType: 'TIMESTAMP', isRef: false };
|
|
11
|
+
if (prop.format === 'at-uri')
|
|
12
|
+
return { duckdbType: 'TEXT', isRef: true };
|
|
13
|
+
return { duckdbType: 'TEXT', isRef: false };
|
|
14
|
+
}
|
|
15
|
+
if (prop.type === 'integer')
|
|
16
|
+
return { duckdbType: 'INTEGER', isRef: false };
|
|
17
|
+
if (prop.type === 'boolean')
|
|
18
|
+
return { duckdbType: 'BOOLEAN', isRef: false };
|
|
19
|
+
if (prop.type === 'bytes')
|
|
20
|
+
return { duckdbType: 'BLOB', isRef: false };
|
|
21
|
+
if (prop.type === 'cid-link')
|
|
22
|
+
return { duckdbType: 'TEXT', isRef: false };
|
|
23
|
+
if (prop.type === 'array')
|
|
24
|
+
return { duckdbType: 'JSON', isRef: false };
|
|
25
|
+
if (prop.type === 'blob')
|
|
26
|
+
return { duckdbType: 'JSON', isRef: false };
|
|
27
|
+
if (prop.type === 'union')
|
|
28
|
+
return { duckdbType: 'JSON', isRef: false };
|
|
29
|
+
if (prop.type === 'unknown')
|
|
30
|
+
return { duckdbType: 'JSON', isRef: false };
|
|
31
|
+
if (prop.type === 'object')
|
|
32
|
+
return { duckdbType: 'JSON', isRef: false };
|
|
33
|
+
if (prop.type === 'ref') {
|
|
34
|
+
// strongRef contains { uri, cid } — handled specially in generateTableSchema
|
|
35
|
+
if (prop.ref === 'com.atproto.repo.strongRef')
|
|
36
|
+
return { duckdbType: 'STRONG_REF', isRef: true };
|
|
37
|
+
return { duckdbType: 'JSON', isRef: false };
|
|
38
|
+
}
|
|
39
|
+
return { duckdbType: 'TEXT', isRef: false };
|
|
40
|
+
}
|
|
41
|
+
// Recursively find all .json files in a directory
|
|
42
|
+
function findJsonFiles(dir) {
|
|
43
|
+
const results = [];
|
|
44
|
+
for (const entry of readdirSync(dir)) {
|
|
45
|
+
const full = join(dir, entry);
|
|
46
|
+
if (statSync(full).isDirectory()) {
|
|
47
|
+
results.push(...findJsonFiles(full));
|
|
48
|
+
}
|
|
49
|
+
else if (entry.endsWith('.json')) {
|
|
50
|
+
results.push(full);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return results;
|
|
54
|
+
}
|
|
55
|
+
// Load all lexicon files and index by NSID
|
|
56
|
+
export function loadLexicons(lexiconsDir) {
|
|
57
|
+
const lexicons = new Map();
|
|
58
|
+
for (const file of findJsonFiles(lexiconsDir)) {
|
|
59
|
+
const content = JSON.parse(readFileSync(file, 'utf-8'));
|
|
60
|
+
if (content.lexicon === 1 && content.id) {
|
|
61
|
+
lexicons.set(content.id, content);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return lexicons;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Discover collections by scanning lexicons for record-type definitions.
|
|
68
|
+
*/
|
|
69
|
+
export function discoverCollections(lexicons) {
|
|
70
|
+
const collections = [];
|
|
71
|
+
for (const [nsid, lexicon] of lexicons) {
|
|
72
|
+
const mainDef = lexicon.defs?.main;
|
|
73
|
+
if (mainDef?.type === 'record') {
|
|
74
|
+
collections.push(nsid);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return collections.sort();
|
|
78
|
+
}
|
|
79
|
+
const storedLexicons = new Map();
|
|
80
|
+
export function storeLexicons(lexicons) {
|
|
81
|
+
for (const [nsid, lex] of lexicons) {
|
|
82
|
+
storedLexicons.set(nsid, lex);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function getLexicon(nsid) {
|
|
86
|
+
return storedLexicons.get(nsid);
|
|
87
|
+
}
|
|
88
|
+
export function getAllLexicons() {
|
|
89
|
+
return [...storedLexicons.entries()].map(([nsid, lexicon]) => ({ nsid, lexicon }));
|
|
90
|
+
}
|
|
91
|
+
/** Get all stored lexicons as a flat array (for @bigmoves/lexicon validators) */
|
|
92
|
+
export function getLexiconArray() {
|
|
93
|
+
return [...storedLexicons.values()];
|
|
94
|
+
}
|
|
95
|
+
function resolveArrayItemProperties(items, defs) {
|
|
96
|
+
if (!items)
|
|
97
|
+
return null;
|
|
98
|
+
// Inline object with properties
|
|
99
|
+
if (items.type === 'object' && items.properties) {
|
|
100
|
+
return items.properties;
|
|
101
|
+
}
|
|
102
|
+
// Ref to a named def (e.g., "#artist")
|
|
103
|
+
if (items.type === 'ref' && items.ref?.startsWith('#')) {
|
|
104
|
+
const defName = items.ref.slice(1);
|
|
105
|
+
const def = defs?.[defName];
|
|
106
|
+
if (def?.type === 'object' && def.properties) {
|
|
107
|
+
return def.properties;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
/** Resolve a ref string to its definition object */
|
|
113
|
+
function resolveRefDef(ref, defs, lexicons) {
|
|
114
|
+
if (ref.startsWith('#')) {
|
|
115
|
+
return defs?.[ref.slice(1)] || null;
|
|
116
|
+
}
|
|
117
|
+
if (ref.includes('#')) {
|
|
118
|
+
const [nsid, defName] = ref.split('#');
|
|
119
|
+
return lexicons?.get(nsid)?.defs?.[defName] || null;
|
|
120
|
+
}
|
|
121
|
+
return lexicons?.get(ref)?.defs?.main || null;
|
|
122
|
+
}
|
|
123
|
+
/** Resolve a single union ref to a branch schema */
|
|
124
|
+
function resolveUnionBranch(ref, collection, fieldName, defs, lexicons) {
|
|
125
|
+
let branchDef = null;
|
|
126
|
+
let branchName;
|
|
127
|
+
let fullType;
|
|
128
|
+
let branchDefs = defs; // defs context for resolving inner refs
|
|
129
|
+
if (ref.startsWith('#')) {
|
|
130
|
+
const defName = ref.slice(1);
|
|
131
|
+
branchDef = defs?.[defName];
|
|
132
|
+
branchName = toSnakeCase(defName);
|
|
133
|
+
fullType = `${collection}#${defName}`;
|
|
134
|
+
}
|
|
135
|
+
else if (ref.includes('#')) {
|
|
136
|
+
const [nsid, defName] = ref.split('#');
|
|
137
|
+
const lex = lexicons?.get(nsid);
|
|
138
|
+
branchDef = lex?.defs?.[defName];
|
|
139
|
+
branchName = toSnakeCase(defName);
|
|
140
|
+
fullType = ref;
|
|
141
|
+
branchDefs = lex?.defs || defs;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
const lex = lexicons?.get(ref);
|
|
145
|
+
branchDef = lex?.defs?.main;
|
|
146
|
+
branchName = ref.split('.').pop();
|
|
147
|
+
fullType = ref;
|
|
148
|
+
branchDefs = lex?.defs || defs;
|
|
149
|
+
}
|
|
150
|
+
if (!branchDef || branchDef.type !== 'object' || !branchDef.properties)
|
|
151
|
+
return null;
|
|
152
|
+
let isArray = false;
|
|
153
|
+
let arrayField;
|
|
154
|
+
let wrapperField;
|
|
155
|
+
let propSource = branchDef.properties;
|
|
156
|
+
const branchRequired = new Set(branchDef.required || []);
|
|
157
|
+
// Check for single-property wrapper patterns
|
|
158
|
+
const propEntries = Object.entries(branchDef.properties);
|
|
159
|
+
if (propEntries.length === 1) {
|
|
160
|
+
const [onlyField, onlyProp] = propEntries[0];
|
|
161
|
+
if (onlyProp.type === 'array' && onlyProp.items) {
|
|
162
|
+
// Single array property (like embed.images wrapping images[])
|
|
163
|
+
const items = onlyProp.items;
|
|
164
|
+
const itemDef = items.type === 'ref' && items.ref ? resolveRefDef(items.ref, branchDefs, lexicons) : items;
|
|
165
|
+
if (itemDef?.type === 'object' && itemDef.properties) {
|
|
166
|
+
isArray = true;
|
|
167
|
+
arrayField = onlyField;
|
|
168
|
+
propSource = itemDef.properties;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else if (onlyProp.type === 'ref' && onlyProp.ref) {
|
|
172
|
+
// Single ref property (like embed.external wrapping external{})
|
|
173
|
+
const refDef = resolveRefDef(onlyProp.ref, branchDefs, lexicons);
|
|
174
|
+
if (refDef?.type === 'object' && refDef.properties) {
|
|
175
|
+
wrapperField = onlyField;
|
|
176
|
+
propSource = refDef.properties;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const snakeField = toSnakeCase(fieldName);
|
|
181
|
+
const tableName = `"${collection}__${snakeField}_${branchName}"`;
|
|
182
|
+
const columns = [];
|
|
183
|
+
for (const [propName, prop] of Object.entries(propSource)) {
|
|
184
|
+
const { duckdbType, isRef } = mapType(prop);
|
|
185
|
+
// Skip STRONG_REF expansion in branch tables — treat as JSON
|
|
186
|
+
const finalType = duckdbType === 'STRONG_REF' ? 'JSON' : duckdbType;
|
|
187
|
+
columns.push({
|
|
188
|
+
name: toSnakeCase(propName),
|
|
189
|
+
originalName: propName,
|
|
190
|
+
duckdbType: finalType,
|
|
191
|
+
notNull: branchRequired.has(propName),
|
|
192
|
+
isRef: finalType !== 'JSON' && isRef,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return { type: fullType, branchName, tableName, columns, isArray, arrayField, wrapperField };
|
|
196
|
+
}
|
|
197
|
+
// Generate a TableSchema from a lexicon record definition
|
|
198
|
+
export function generateTableSchema(nsid, lexicon, lexicons) {
|
|
199
|
+
const mainDef = lexicon.defs?.main;
|
|
200
|
+
if (!mainDef || mainDef.type !== 'record') {
|
|
201
|
+
throw new Error(`Lexicon ${nsid} does not define a record type`);
|
|
202
|
+
}
|
|
203
|
+
const record = mainDef.record;
|
|
204
|
+
if (!record || record.type !== 'object') {
|
|
205
|
+
throw new Error(`Lexicon ${nsid} record is not an object type`);
|
|
206
|
+
}
|
|
207
|
+
const required = new Set(record.required || []);
|
|
208
|
+
const columns = [];
|
|
209
|
+
const children = [];
|
|
210
|
+
const unions = [];
|
|
211
|
+
for (const [fieldName, prop] of Object.entries(record.properties || {})) {
|
|
212
|
+
const p = prop;
|
|
213
|
+
// Check for union fields — decompose into branch child tables
|
|
214
|
+
if (p.type === 'union' && p.refs) {
|
|
215
|
+
const branches = [];
|
|
216
|
+
for (const ref of p.refs) {
|
|
217
|
+
const branch = resolveUnionBranch(ref, nsid, fieldName, lexicon.defs, lexicons);
|
|
218
|
+
if (branch)
|
|
219
|
+
branches.push(branch);
|
|
220
|
+
}
|
|
221
|
+
if (branches.length > 0) {
|
|
222
|
+
unions.push({ fieldName, branches });
|
|
223
|
+
}
|
|
224
|
+
// Still add the JSON column for the raw union value
|
|
225
|
+
columns.push({
|
|
226
|
+
name: toSnakeCase(fieldName),
|
|
227
|
+
originalName: fieldName,
|
|
228
|
+
duckdbType: 'JSON',
|
|
229
|
+
notNull: required.has(fieldName),
|
|
230
|
+
isRef: false,
|
|
231
|
+
});
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
// Check if this is a decomposable array (array of structured objects)
|
|
235
|
+
if (p.type === 'array') {
|
|
236
|
+
const itemProps = resolveArrayItemProperties(p.items, lexicon.defs);
|
|
237
|
+
if (itemProps) {
|
|
238
|
+
const childColumns = [];
|
|
239
|
+
const itemRequired = new Set(p.items?.required || lexicon.defs?.[p.items?.ref?.slice(1)]?.required || []);
|
|
240
|
+
for (const [itemField, itemProp] of Object.entries(itemProps)) {
|
|
241
|
+
const { duckdbType, isRef } = mapType(itemProp);
|
|
242
|
+
childColumns.push({
|
|
243
|
+
name: toSnakeCase(itemField),
|
|
244
|
+
originalName: itemField,
|
|
245
|
+
duckdbType,
|
|
246
|
+
notNull: itemRequired.has(itemField),
|
|
247
|
+
isRef,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
const snakeField = toSnakeCase(fieldName);
|
|
251
|
+
children.push({
|
|
252
|
+
parentCollection: nsid,
|
|
253
|
+
fieldName,
|
|
254
|
+
tableName: `"${nsid}__${snakeField}"`,
|
|
255
|
+
columns: childColumns,
|
|
256
|
+
});
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const { duckdbType, isRef } = mapType(p);
|
|
261
|
+
if (duckdbType === 'STRONG_REF') {
|
|
262
|
+
// Expand strongRef into two columns: {name}_uri and {name}_cid
|
|
263
|
+
columns.push({
|
|
264
|
+
name: toSnakeCase(fieldName) + '_uri',
|
|
265
|
+
originalName: fieldName,
|
|
266
|
+
duckdbType: 'TEXT',
|
|
267
|
+
notNull: required.has(fieldName),
|
|
268
|
+
isRef: true,
|
|
269
|
+
});
|
|
270
|
+
columns.push({
|
|
271
|
+
name: toSnakeCase(fieldName) + '_cid',
|
|
272
|
+
originalName: fieldName + '__cid',
|
|
273
|
+
duckdbType: 'TEXT',
|
|
274
|
+
notNull: required.has(fieldName),
|
|
275
|
+
isRef: false,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
columns.push({
|
|
280
|
+
name: toSnakeCase(fieldName),
|
|
281
|
+
originalName: fieldName,
|
|
282
|
+
duckdbType,
|
|
283
|
+
notNull: required.has(fieldName),
|
|
284
|
+
isRef,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const refColumns = columns.filter((c) => c.isRef).map((c) => c.name);
|
|
289
|
+
return {
|
|
290
|
+
collection: nsid,
|
|
291
|
+
tableName: `"${nsid}"`,
|
|
292
|
+
columns,
|
|
293
|
+
refColumns,
|
|
294
|
+
children,
|
|
295
|
+
unions,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
// Generate CREATE TABLE SQL from a TableSchema
|
|
299
|
+
export function generateCreateTableSQL(schema) {
|
|
300
|
+
const lines = [
|
|
301
|
+
' uri TEXT PRIMARY KEY',
|
|
302
|
+
' cid TEXT',
|
|
303
|
+
' did TEXT NOT NULL',
|
|
304
|
+
' indexed_at TIMESTAMP NOT NULL',
|
|
305
|
+
];
|
|
306
|
+
for (const col of schema.columns) {
|
|
307
|
+
const nullable = col.notNull ? ' NOT NULL' : '';
|
|
308
|
+
lines.push(` ${col.name} ${col.duckdbType}${nullable}`);
|
|
309
|
+
}
|
|
310
|
+
const createTable = `CREATE TABLE IF NOT EXISTS ${schema.tableName} (\n${lines.join(',\n')}\n);`;
|
|
311
|
+
const prefix = schema.collection.replace(/\./g, '_');
|
|
312
|
+
const indexes = [
|
|
313
|
+
`CREATE INDEX IF NOT EXISTS idx_${prefix}_indexed ON ${schema.tableName}(indexed_at DESC);`,
|
|
314
|
+
`CREATE INDEX IF NOT EXISTS idx_${prefix}_author ON ${schema.tableName}(did);`,
|
|
315
|
+
];
|
|
316
|
+
// Index ref columns for hydration lookups
|
|
317
|
+
for (const refCol of schema.refColumns) {
|
|
318
|
+
indexes.push(`CREATE INDEX IF NOT EXISTS idx_${prefix}_${refCol} ON ${schema.tableName}(${refCol});`);
|
|
319
|
+
}
|
|
320
|
+
// Child table DDL
|
|
321
|
+
const childDDL = [];
|
|
322
|
+
for (const child of schema.children) {
|
|
323
|
+
const childLines = [' parent_uri TEXT NOT NULL', ' parent_did TEXT NOT NULL'];
|
|
324
|
+
for (const col of child.columns) {
|
|
325
|
+
const nullable = col.notNull ? ' NOT NULL' : '';
|
|
326
|
+
childLines.push(` ${col.name} ${col.duckdbType}${nullable}`);
|
|
327
|
+
}
|
|
328
|
+
childDDL.push(`CREATE TABLE IF NOT EXISTS ${child.tableName} (\n${childLines.join(',\n')}\n);`);
|
|
329
|
+
const childPrefix = `${prefix}__${toSnakeCase(child.fieldName)}`;
|
|
330
|
+
childDDL.push(`CREATE INDEX IF NOT EXISTS idx_${childPrefix}_parent ON ${child.tableName}(parent_uri);`);
|
|
331
|
+
childDDL.push(`CREATE INDEX IF NOT EXISTS idx_${childPrefix}_did ON ${child.tableName}(parent_did);`);
|
|
332
|
+
for (const col of child.columns) {
|
|
333
|
+
if (col.duckdbType === 'JSON' || col.duckdbType === 'BLOB')
|
|
334
|
+
continue;
|
|
335
|
+
childDDL.push(`CREATE INDEX IF NOT EXISTS idx_${childPrefix}_${col.name} ON ${child.tableName}(${col.name});`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// Union branch table DDL
|
|
339
|
+
for (const union of schema.unions) {
|
|
340
|
+
for (const branch of union.branches) {
|
|
341
|
+
const branchLines = [' parent_uri TEXT NOT NULL', ' parent_did TEXT NOT NULL'];
|
|
342
|
+
for (const col of branch.columns) {
|
|
343
|
+
const nullable = col.notNull ? ' NOT NULL' : '';
|
|
344
|
+
branchLines.push(` ${col.name} ${col.duckdbType}${nullable}`);
|
|
345
|
+
}
|
|
346
|
+
childDDL.push(`CREATE TABLE IF NOT EXISTS ${branch.tableName} (\n${branchLines.join(',\n')}\n);`);
|
|
347
|
+
const branchPrefix = branch.tableName.replace(/"/g, '').replace(/\./g, '_');
|
|
348
|
+
childDDL.push(`CREATE INDEX IF NOT EXISTS idx_${branchPrefix}_parent ON ${branch.tableName}(parent_uri);`);
|
|
349
|
+
childDDL.push(`CREATE INDEX IF NOT EXISTS idx_${branchPrefix}_did ON ${branch.tableName}(parent_did);`);
|
|
350
|
+
for (const col of branch.columns) {
|
|
351
|
+
if (col.duckdbType === 'JSON' || col.duckdbType === 'BLOB')
|
|
352
|
+
continue;
|
|
353
|
+
childDDL.push(`CREATE INDEX IF NOT EXISTS idx_${branchPrefix}_${col.name} ON ${branch.tableName}(${col.name});`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return [createTable, ...indexes, ...childDDL].join('\n');
|
|
358
|
+
}
|
package/dist/seed.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type Session = {
|
|
2
|
+
did: string;
|
|
3
|
+
accessJwt: string;
|
|
4
|
+
handle: string;
|
|
5
|
+
};
|
|
6
|
+
export type BlobRef = {
|
|
7
|
+
$type: 'blob';
|
|
8
|
+
ref: {
|
|
9
|
+
$link: string;
|
|
10
|
+
};
|
|
11
|
+
mimeType: string;
|
|
12
|
+
size: number;
|
|
13
|
+
};
|
|
14
|
+
export type SeedOpts = {
|
|
15
|
+
pds?: string;
|
|
16
|
+
password?: string;
|
|
17
|
+
lexicons?: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function seed<R extends Record<string, unknown> = Record<string, unknown>>(opts?: SeedOpts): {
|
|
20
|
+
createAccount: (handle: string) => Promise<Session>;
|
|
21
|
+
createRecord: <K extends keyof R & string>(session: Session, collection: K, record: R[K] extends Record<string, unknown> ? R[K] : Record<string, unknown>, opts: {
|
|
22
|
+
rkey: string;
|
|
23
|
+
}) => Promise<{
|
|
24
|
+
uri: string;
|
|
25
|
+
cid: string;
|
|
26
|
+
}>;
|
|
27
|
+
uploadBlob: (session: Session, filePath: string) => Promise<BlobRef>;
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=seed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed.d.ts","sourceRoot":"","sources":["../src/seed.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,OAAO,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AACxE,MAAM,MAAM,OAAO,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAC/F,MAAM,MAAM,QAAQ,GAAG;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE7E,wBAAgB,IAAI,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,EAAE,QAAQ;4BAM1D,MAAM,KAAG,OAAO,CAAC,OAAO,CAAC;mBA4BlC,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,WAC3C,OAAO,cACJ,CAAC,UACL,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,QACvE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,KACrB,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;0BA4BL,OAAO,YAAY,MAAM,KAAG,OAAO,CAAC,OAAO,CAAC;EA4BhF"}
|