@hatk/hatk 0.0.1-alpha.9 → 0.0.1-alpha.90
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/adapter.d.ts +19 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +113 -0
- package/dist/backfill.d.ts +29 -2
- package/dist/backfill.d.ts.map +1 -1
- package/dist/backfill.js +137 -51
- package/dist/car.d.ts +20 -0
- package/dist/car.d.ts.map +1 -1
- package/dist/car.js +108 -0
- package/dist/cli.js +247 -1043
- package/dist/config.d.ts +145 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +61 -8
- package/dist/database/adapter-factory.d.ts +6 -0
- package/dist/database/adapter-factory.d.ts.map +1 -0
- package/dist/database/adapter-factory.js +20 -0
- package/dist/database/adapters/duckdb-search.d.ts +12 -0
- package/dist/database/adapters/duckdb-search.d.ts.map +1 -0
- package/dist/database/adapters/duckdb-search.js +27 -0
- package/dist/database/adapters/duckdb.d.ts +25 -0
- package/dist/database/adapters/duckdb.d.ts.map +1 -0
- package/dist/database/adapters/duckdb.js +161 -0
- package/dist/database/adapters/sqlite-search.d.ts +16 -0
- package/dist/database/adapters/sqlite-search.d.ts.map +1 -0
- package/dist/database/adapters/sqlite-search.js +76 -0
- package/dist/database/adapters/sqlite.d.ts +18 -0
- package/dist/database/adapters/sqlite.d.ts.map +1 -0
- package/dist/database/adapters/sqlite.js +88 -0
- package/dist/{db.d.ts → database/db.d.ts} +68 -6
- package/dist/database/db.d.ts.map +1 -0
- package/dist/database/db.js +1775 -0
- package/dist/database/dialect.d.ts +45 -0
- package/dist/database/dialect.d.ts.map +1 -0
- package/dist/database/dialect.js +72 -0
- package/dist/{fts.d.ts → database/fts.d.ts} +7 -0
- package/dist/database/fts.d.ts.map +1 -0
- package/dist/{fts.js → database/fts.js} +122 -34
- package/dist/database/index.d.ts +7 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +6 -0
- package/dist/database/ports.d.ts +50 -0
- package/dist/database/ports.d.ts.map +1 -0
- package/dist/database/ports.js +1 -0
- package/dist/{schema.d.ts → database/schema.d.ts} +16 -3
- package/dist/database/schema.d.ts.map +1 -0
- package/dist/{schema.js → database/schema.js} +126 -44
- package/dist/dev-entry.d.ts +8 -0
- package/dist/dev-entry.d.ts.map +1 -0
- package/dist/dev-entry.js +127 -0
- package/dist/feeds.d.ts +26 -8
- package/dist/feeds.d.ts.map +1 -1
- package/dist/feeds.js +72 -16
- package/dist/hooks.d.ts +102 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +186 -0
- package/dist/hydrate.d.ts +49 -6
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +21 -20
- package/dist/indexer.d.ts +156 -3
- package/dist/indexer.d.ts.map +1 -1
- package/dist/indexer.js +539 -94
- package/dist/jetstream.d.ts +118 -0
- package/dist/jetstream.d.ts.map +1 -0
- package/dist/jetstream.js +225 -0
- package/dist/labels.d.ts +36 -0
- package/dist/labels.d.ts.map +1 -1
- package/dist/labels.js +71 -6
- package/dist/lex-types.d.ts +2 -0
- package/dist/lex-types.d.ts.map +1 -1
- package/dist/lexicon-resolve.d.ts.map +1 -1
- package/dist/lexicon-resolve.js +27 -112
- package/dist/lexicons/com/atproto/label/defs.json +75 -0
- package/dist/lexicons/com/atproto/moderation/defs.json +30 -0
- package/dist/lexicons/com/atproto/repo/strongRef.json +21 -0
- package/dist/lexicons/dev/hatk/applyWrites.json +87 -0
- package/dist/lexicons/dev/hatk/createRecord.json +40 -0
- package/dist/lexicons/dev/hatk/createReport.json +48 -0
- package/dist/lexicons/dev/hatk/deleteRecord.json +25 -0
- package/dist/lexicons/dev/hatk/describeCollections.json +41 -0
- package/dist/lexicons/dev/hatk/describeFeeds.json +29 -0
- package/dist/lexicons/dev/hatk/describeLabels.json +45 -0
- package/dist/lexicons/dev/hatk/getFeed.json +30 -0
- package/dist/lexicons/dev/hatk/getPreferences.json +19 -0
- package/dist/lexicons/dev/hatk/getRecord.json +26 -0
- package/dist/lexicons/dev/hatk/getRecords.json +32 -0
- package/dist/lexicons/dev/hatk/putPreference.json +28 -0
- package/dist/lexicons/dev/hatk/putRecord.json +41 -0
- package/dist/lexicons/dev/hatk/searchRecords.json +32 -0
- package/dist/lexicons/dev/hatk/uploadBlob.json +23 -0
- package/dist/logger.d.ts +29 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +29 -0
- package/dist/main.js +198 -75
- package/dist/mst.d.ts +15 -0
- package/dist/mst.d.ts.map +1 -1
- package/dist/mst.js +13 -0
- package/dist/oauth/db.d.ts +16 -1
- package/dist/oauth/db.d.ts.map +1 -1
- package/dist/oauth/db.js +74 -19
- package/dist/oauth/discovery.d.ts +10 -0
- package/dist/oauth/discovery.d.ts.map +1 -1
- package/dist/oauth/discovery.js +54 -2
- package/dist/oauth/server.d.ts +93 -1
- package/dist/oauth/server.d.ts.map +1 -1
- package/dist/oauth/server.js +416 -39
- package/dist/oauth/service-describe.d.ts +42 -0
- package/dist/oauth/service-describe.d.ts.map +1 -0
- package/dist/oauth/service-describe.js +108 -0
- package/dist/oauth/session.d.ts +32 -0
- package/dist/oauth/session.d.ts.map +1 -0
- package/dist/oauth/session.js +131 -0
- package/dist/opengraph.d.ts +10 -0
- package/dist/opengraph.d.ts.map +1 -1
- package/dist/opengraph.js +94 -42
- package/dist/pds-proxy.d.ts +82 -0
- package/dist/pds-proxy.d.ts.map +1 -0
- package/dist/pds-proxy.js +356 -0
- package/dist/private-collections.d.ts +13 -0
- package/dist/private-collections.d.ts.map +1 -0
- package/dist/private-collections.js +27 -0
- package/dist/push.d.ts +54 -0
- package/dist/push.d.ts.map +1 -0
- package/dist/push.js +367 -0
- package/dist/renderer.d.ts +27 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +46 -0
- package/dist/resolve-hatk.d.ts +6 -0
- package/dist/resolve-hatk.d.ts.map +1 -0
- package/dist/resolve-hatk.js +20 -0
- package/dist/response.d.ts +16 -0
- package/dist/response.d.ts.map +1 -0
- package/dist/response.js +69 -0
- package/dist/scanner.d.ts +21 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +88 -0
- package/dist/seed.d.ts +19 -0
- package/dist/seed.d.ts.map +1 -1
- package/dist/seed.js +43 -4
- package/dist/server-init.d.ts +8 -0
- package/dist/server-init.d.ts.map +1 -0
- package/dist/server-init.js +62 -0
- package/dist/server.d.ts +34 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1021 -649
- package/dist/setup.d.ts +28 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +50 -3
- package/dist/spaces/blob.d.ts +51 -0
- package/dist/spaces/blob.d.ts.map +1 -0
- package/dist/spaces/blob.js +111 -0
- package/dist/spaces/credential.d.ts +62 -0
- package/dist/spaces/credential.d.ts.map +1 -0
- package/dist/spaces/credential.js +184 -0
- package/dist/spaces/engine.d.ts +97 -0
- package/dist/spaces/engine.d.ts.map +1 -0
- package/dist/spaces/engine.js +559 -0
- package/dist/spaces/guard.d.ts +41 -0
- package/dist/spaces/guard.d.ts.map +1 -0
- package/dist/spaces/guard.js +79 -0
- package/dist/spaces/identity.d.ts +37 -0
- package/dist/spaces/identity.d.ts.map +1 -0
- package/dist/spaces/identity.js +88 -0
- package/dist/spaces/index.d.ts +55 -0
- package/dist/spaces/index.d.ts.map +1 -0
- package/dist/spaces/index.js +133 -0
- package/dist/spaces/notify.d.ts +51 -0
- package/dist/spaces/notify.d.ts.map +1 -0
- package/dist/spaces/notify.js +121 -0
- package/dist/spaces/records.d.ts +37 -0
- package/dist/spaces/records.d.ts.map +1 -0
- package/dist/spaces/records.js +63 -0
- package/dist/spaces/store.d.ts +51 -0
- package/dist/spaces/store.d.ts.map +1 -0
- package/dist/spaces/store.js +99 -0
- package/dist/spaces/uri.d.ts +60 -0
- package/dist/spaces/uri.d.ts.map +1 -0
- package/dist/spaces/uri.js +97 -0
- package/dist/spaces/verify.d.ts +39 -0
- package/dist/spaces/verify.d.ts.map +1 -0
- package/dist/spaces/verify.js +87 -0
- package/dist/spaces/viewer.d.ts +51 -0
- package/dist/spaces/viewer.d.ts.map +1 -0
- package/dist/spaces/viewer.js +167 -0
- package/dist/spaces/visibility.d.ts +57 -0
- package/dist/spaces/visibility.d.ts.map +1 -0
- package/dist/spaces/visibility.js +81 -0
- package/dist/templates/feed.tpl +18 -0
- package/dist/templates/hook.tpl +5 -0
- package/dist/templates/label.tpl +15 -0
- package/dist/templates/og.tpl +17 -0
- package/dist/templates/seed.tpl +11 -0
- package/dist/templates/setup.tpl +5 -0
- package/dist/templates/test-feed.tpl +19 -0
- package/dist/templates/test-xrpc.tpl +19 -0
- package/dist/templates/xrpc.tpl +53 -0
- package/dist/test.d.ts +3 -1
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +60 -36
- package/dist/views.js +1 -1
- package/dist/vite-plugin.d.ts +1 -1
- package/dist/vite-plugin.d.ts.map +1 -1
- package/dist/vite-plugin.js +254 -66
- package/dist/xrpc.d.ts +101 -12
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +255 -55
- package/package.json +21 -9
- package/public/admin-auth.js +271 -306
- package/public/admin.html +177 -61
- package/dist/db.d.ts.map +0 -1
- package/dist/db.js +0 -1361
- package/dist/fts.d.ts.map +0 -1
- package/dist/oauth/hooks.d.ts +0 -10
- package/dist/oauth/hooks.d.ts.map +0 -1
- package/dist/oauth/hooks.js +0 -40
- package/dist/schema.d.ts.map +0 -1
- package/dist/test-browser.d.ts +0 -14
- package/dist/test-browser.d.ts.map +0 -1
- package/dist/test-browser.js +0 -26
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visibility.d.ts","sourceRoot":"","sources":["../../src/spaces/visibility.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAaH;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAE9E;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,IAAI,CAElE;AAED,+DAA+D;AAC/D,wBAAgB,cAAc,IAAI,WAAW,CAAC,MAAM,CAAC,CAEpD;AAED,6DAA6D;AAC7D,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAGzE;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAUlH"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which permissioned spaces the read in flight is allowed to serve.
|
|
3
|
+
*
|
|
4
|
+
* Space records are not public data. A space's authority decides who may read
|
|
5
|
+
* it, and it decides that one caller at a time — so a row indexed out of a
|
|
6
|
+
* space carries an audience, and serving it to a request that has not proven
|
|
7
|
+
* it belongs to that audience discloses private data no matter how the query
|
|
8
|
+
* was written.
|
|
9
|
+
*
|
|
10
|
+
* hatk has no other per-row visibility concept. The takedown join is
|
|
11
|
+
* viewer-independent and `privateCollections` is all-or-nothing, so neither
|
|
12
|
+
* could carry this. The rule here is instead:
|
|
13
|
+
*
|
|
14
|
+
* a row with `space IS NULL` is public repo data and always readable;
|
|
15
|
+
* a row with a space is readable only inside a scope that names that space.
|
|
16
|
+
*
|
|
17
|
+
* The default is an empty scope, which serves no space rows at all. That is
|
|
18
|
+
* deliberate: a read path nobody remembered to scope fails closed and shows a
|
|
19
|
+
* member nothing, rather than failing open and showing a stranger everything.
|
|
20
|
+
* Every query over a record table applies `spaceFilterSql`; the scope is
|
|
21
|
+
* established per request by whoever established the viewer.
|
|
22
|
+
*/
|
|
23
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
24
|
+
const EMPTY = new Set();
|
|
25
|
+
/**
|
|
26
|
+
* Async-local rather than a module global: a server answers requests
|
|
27
|
+
* concurrently, and one viewer's readable set must never be observable from
|
|
28
|
+
* another's request while an await is outstanding.
|
|
29
|
+
*/
|
|
30
|
+
const store = new AsyncLocalStorage();
|
|
31
|
+
/**
|
|
32
|
+
* Run `fn` with exactly these spaces readable.
|
|
33
|
+
*
|
|
34
|
+
* Replaces rather than extends any enclosing scope: widening what a nested read
|
|
35
|
+
* may see is never what a caller means, and a scope that only ever narrows is
|
|
36
|
+
* one that can be reasoned about locally.
|
|
37
|
+
*/
|
|
38
|
+
export function withReadableSpaces(spaces, fn) {
|
|
39
|
+
return store.run(new Set(spaces), fn);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Set the readable spaces for the rest of the current async context.
|
|
43
|
+
*
|
|
44
|
+
* `withReadableSpaces` wants a callback, and a request handler is a long body
|
|
45
|
+
* rather than something to wrap — restructuring it around a closure to hold a
|
|
46
|
+
* scope would be the tail wagging the dog. A server handler already runs in its
|
|
47
|
+
* own async context per request, which is the case `enterWith` exists for.
|
|
48
|
+
*/
|
|
49
|
+
export function enterReadableSpaces(spaces) {
|
|
50
|
+
store.enterWith(new Set(spaces));
|
|
51
|
+
}
|
|
52
|
+
/** The spaces this read may serve. Empty outside any scope. */
|
|
53
|
+
export function readableSpaces() {
|
|
54
|
+
return store.getStore() ?? EMPTY;
|
|
55
|
+
}
|
|
56
|
+
/** Whether a space would be served to the read in flight. */
|
|
57
|
+
export function isSpaceReadable(space) {
|
|
58
|
+
if (space == null)
|
|
59
|
+
return true;
|
|
60
|
+
return readableSpaces().has(space);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The space gate as a SQL predicate, for a table aliased `alias` (pass '' for
|
|
64
|
+
* an unaliased table).
|
|
65
|
+
*
|
|
66
|
+
* `startIdx` is the next free `$N` placeholder; the returned `nextIdx` is the
|
|
67
|
+
* next one after this predicate's own. With nothing readable the predicate
|
|
68
|
+
* binds no parameters at all, which is the shape almost every request takes.
|
|
69
|
+
*/
|
|
70
|
+
export function spaceFilterSql(alias, startIdx) {
|
|
71
|
+
const col = alias ? `${alias}.space` : 'space';
|
|
72
|
+
const spaces = [...readableSpaces()];
|
|
73
|
+
if (spaces.length === 0)
|
|
74
|
+
return { sql: `${col} IS NULL`, params: [], nextIdx: startIdx };
|
|
75
|
+
const placeholders = spaces.map((_, i) => `$${startIdx + i}`).join(', ');
|
|
76
|
+
return {
|
|
77
|
+
sql: `(${col} IS NULL OR ${col} IN (${placeholders}))`,
|
|
78
|
+
params: spaces,
|
|
79
|
+
nextIdx: startIdx + spaces.length,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineFeed } from '$hatk'
|
|
2
|
+
|
|
3
|
+
export default defineFeed({
|
|
4
|
+
collection: 'your.collection.here',
|
|
5
|
+
label: '{{Name}}',
|
|
6
|
+
|
|
7
|
+
async generate(ctx) {
|
|
8
|
+
// ctx.paginate applies the permissioned-space gate for you, so this page
|
|
9
|
+
// shows a member the spaces they may read and everyone else the public
|
|
10
|
+
// rows alone. A feed that builds its own SQL over a space-backed table is
|
|
11
|
+
// refused with an error naming the table; see ctx.spaceFilter.
|
|
12
|
+
const { rows, cursor } = await ctx.paginate<{ uri: string }>(
|
|
13
|
+
`SELECT uri, cid, indexed_at FROM "your.collection.here"`,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
return ctx.ok({ uris: rows.map((r) => r.uri), cursor })
|
|
17
|
+
},
|
|
18
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineLabel } from '$hatk'
|
|
2
|
+
|
|
3
|
+
export default defineLabel({
|
|
4
|
+
definition: {
|
|
5
|
+
identifier: '{{name}}',
|
|
6
|
+
severity: 'inform',
|
|
7
|
+
blurs: 'none',
|
|
8
|
+
defaultSetting: 'warn',
|
|
9
|
+
locales: [{ lang: 'en', name: '{{Name}}', description: 'Description here' }],
|
|
10
|
+
},
|
|
11
|
+
async evaluate(ctx) {
|
|
12
|
+
// Return array of label identifiers to apply, or empty array
|
|
13
|
+
return []
|
|
14
|
+
},
|
|
15
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { OpengraphContext, OpengraphResult } from '@hatk/hatk/opengraph'
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
path: '/og/{{name}}/:id',
|
|
5
|
+
async generate(ctx: OpengraphContext): Promise<OpengraphResult> {
|
|
6
|
+
const { db, params } = ctx
|
|
7
|
+
return {
|
|
8
|
+
element: {
|
|
9
|
+
type: 'div',
|
|
10
|
+
props: {
|
|
11
|
+
style: { display: 'flex', width: '100%', height: '100%', background: '#080b12', color: 'white', alignItems: 'center', justifyContent: 'center' },
|
|
12
|
+
children: params.id,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { seed } from '$hatk'
|
|
2
|
+
|
|
3
|
+
const { createAccount, createRecord } = seed()
|
|
4
|
+
|
|
5
|
+
const alice = await createAccount('alice.test')
|
|
6
|
+
|
|
7
|
+
// await createRecord(alice, 'your.collection.here', {
|
|
8
|
+
// field: 'value',
|
|
9
|
+
// }, { rkey: 'my-record' })
|
|
10
|
+
|
|
11
|
+
console.log('\n[seed] Done!')
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from 'vitest'
|
|
2
|
+
import { createTestContext } from '@hatk/hatk/test'
|
|
3
|
+
|
|
4
|
+
let ctx: Awaited<ReturnType<typeof createTestContext>>
|
|
5
|
+
|
|
6
|
+
beforeAll(async () => {
|
|
7
|
+
ctx = await createTestContext()
|
|
8
|
+
await ctx.loadFixtures()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
afterAll(async () => ctx?.close())
|
|
12
|
+
|
|
13
|
+
describe('{{name}} feed', () => {
|
|
14
|
+
test('returns results', async () => {
|
|
15
|
+
const feed = ctx.loadFeed('{{name}}')
|
|
16
|
+
const result = await feed.generate(ctx.feedContext({ limit: 10 }))
|
|
17
|
+
expect(result).toBeDefined()
|
|
18
|
+
})
|
|
19
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from 'vitest'
|
|
2
|
+
import { createTestContext } from '@hatk/hatk/test'
|
|
3
|
+
|
|
4
|
+
let ctx: Awaited<ReturnType<typeof createTestContext>>
|
|
5
|
+
|
|
6
|
+
beforeAll(async () => {
|
|
7
|
+
ctx = await createTestContext()
|
|
8
|
+
await ctx.loadFixtures()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
afterAll(async () => ctx?.close())
|
|
12
|
+
|
|
13
|
+
describe('{{name}}', () => {
|
|
14
|
+
test('returns response', async () => {
|
|
15
|
+
const handler = ctx.loadXrpc('{{name}}')
|
|
16
|
+
const result = await handler.handler({ params: {} })
|
|
17
|
+
expect(result).toBeDefined()
|
|
18
|
+
})
|
|
19
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { defineQuery } from '$hatk'
|
|
2
|
+
|
|
3
|
+
export default defineQuery('{{name}}', async (ctx) => {
|
|
4
|
+
const { ok, db, params, packCursor, unpackCursor, spaceFilter } = ctx
|
|
5
|
+
const limit = params.limit ?? 30
|
|
6
|
+
const cursor = params.cursor
|
|
7
|
+
|
|
8
|
+
const conditions: string[] = []
|
|
9
|
+
const sqlParams: (string | number)[] = []
|
|
10
|
+
let paramIdx = 1
|
|
11
|
+
|
|
12
|
+
// Which permissioned spaces this viewer may be shown. Typed helpers like
|
|
13
|
+
// ctx.lookup and ctx.getRecords apply this already; hand-written SQL cannot,
|
|
14
|
+
// because nothing can inject a predicate into a string you wrote. Drop it on
|
|
15
|
+
// a collection a space writes into and hatk refuses the query by name.
|
|
16
|
+
//
|
|
17
|
+
// Outside a viewer's scope it is `s.space IS NULL` and binds nothing, so on an
|
|
18
|
+
// instance that indexes no space it costs one test on an indexed column.
|
|
19
|
+
const spaces = spaceFilter('s', paramIdx)
|
|
20
|
+
conditions.push(spaces.sql)
|
|
21
|
+
sqlParams.push(...spaces.params)
|
|
22
|
+
paramIdx = spaces.nextIdx
|
|
23
|
+
|
|
24
|
+
if (cursor) {
|
|
25
|
+
const parsed = unpackCursor(cursor)
|
|
26
|
+
if (parsed) {
|
|
27
|
+
conditions.push(`(s.indexed_at < $${paramIdx} OR (s.indexed_at = $${paramIdx + 1} AND s.cid < $${paramIdx + 2}))`)
|
|
28
|
+
sqlParams.push(parsed.primary, parsed.primary, parsed.cid)
|
|
29
|
+
paramIdx += 3
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const where = conditions.length ? 'WHERE ' + conditions.join(' AND ') : ''
|
|
34
|
+
|
|
35
|
+
const rows = (await db.query(
|
|
36
|
+
`SELECT s.* FROM "your.collection.here" s ${where} ORDER BY s.indexed_at DESC, s.cid DESC LIMIT $${paramIdx}`,
|
|
37
|
+
sqlParams.concat([limit + 1]),
|
|
38
|
+
)) as {
|
|
39
|
+
uri: string
|
|
40
|
+
cid: string
|
|
41
|
+
did: string
|
|
42
|
+
indexed_at: string
|
|
43
|
+
}[]
|
|
44
|
+
|
|
45
|
+
const hasMore = rows.length > limit
|
|
46
|
+
if (hasMore) rows.pop()
|
|
47
|
+
const lastRow = rows[rows.length - 1]
|
|
48
|
+
|
|
49
|
+
return ok({
|
|
50
|
+
items: rows,
|
|
51
|
+
cursor: hasMore && lastRow ? packCursor(lastRow.indexed_at, lastRow.cid) : undefined,
|
|
52
|
+
})
|
|
53
|
+
})
|
package/dist/test.d.ts
CHANGED
|
@@ -4,7 +4,9 @@ import type { FeedContext } from './feeds.ts';
|
|
|
4
4
|
export interface TestContext {
|
|
5
5
|
db: {
|
|
6
6
|
query: (sql: string, params?: any[]) => Promise<any[]>;
|
|
7
|
-
run: (sql: string,
|
|
7
|
+
run: (sql: string, params?: any[]) => Promise<void>;
|
|
8
|
+
/** Raw SQL with the permissioned-space guard off, as the runtime contexts have it. */
|
|
9
|
+
unfiltered: (sql: string, params?: any[]) => Promise<any[]>;
|
|
8
10
|
};
|
|
9
11
|
loadFixtures: (dir?: string) => Promise<void>;
|
|
10
12
|
loadFeed: (name: string) => {
|
package/dist/test.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../src/test.ts"],"names":[],"mappings":"AAIA,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../src/test.ts"],"names":[],"mappings":"AAIA,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAmBzD,OAAO,EAAE,IAAI,IAAI,iBAAiB,EAAE,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAA;AAIpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE;QACF,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;QACtD,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;QACnD,sFAAsF;QACtF,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;KAC5D,CAAA;IACD,YAAY,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7C,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;KAAE,CAAA;IAC5E,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;KAAE,CAAA;IACnE,WAAW,EAAE,CAAC,IAAI,CAAC,EAAE;QACnB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,MAAM,CAAC,EAAE;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAA;QAC/B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAChC,KAAK,WAAW,CAAA;IACjB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1B,gBAAgB,CAAC,OAAO,EAAE,UAAU,CAAA;IACpC,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CACxC;AAED,MAAM,WAAW,UAAW,SAAQ,WAAW;IAC7C,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC9D,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC7E,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,QAAQ,KAAK,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAA;IAC/D,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAClE;AAYD;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,WAAW,CAAC,CAyL9D;AA8BD,wBAAsB,eAAe,IAAI,OAAO,CAAC,UAAU,CAAC,CAiD3D"}
|
package/dist/test.js
CHANGED
|
@@ -1,27 +1,32 @@
|
|
|
1
1
|
import { resolve, dirname } from 'node:path';
|
|
2
2
|
import { readdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { registerHatkResolveHook } from "./resolve-hatk.js";
|
|
3
4
|
import YAML from 'yaml';
|
|
4
5
|
import { loadConfig } from "./config.js";
|
|
5
|
-
import { loadLexicons, storeLexicons, discoverCollections, generateTableSchema, generateCreateTableSQL, } from "./schema.js";
|
|
6
|
-
import { initDatabase, querySQL, runSQL, insertRecord, closeDatabase } from "./db.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
6
|
+
import { validatableLexicons, loadLexicons, storeLexicons, discoverCollections, generateTableSchema, generateCreateTableSQL, } from "./database/schema.js";
|
|
7
|
+
import { initDatabase, querySQL, runSQL, insertRecord, closeDatabase } from "./database/db.js";
|
|
8
|
+
import { createAdapter } from "./database/adapter-factory.js";
|
|
9
|
+
import { SQLITE_DIALECT } from "./database/dialect.js";
|
|
10
|
+
import { rebuildAllIndexes, setSearchPort } from "./database/fts.js";
|
|
11
|
+
import { executeFeed, listFeeds, createPaginate } from "./feeds.js";
|
|
12
|
+
import { executeXrpc, listXrpc, configureRelay, configureCdn } from "./xrpc.js";
|
|
13
|
+
import { initServer } from "./server-init.js";
|
|
11
14
|
import { discoverViews } from "./views.js";
|
|
12
|
-
import { loadOnLoginHook } from "./oauth/hooks.js";
|
|
13
15
|
import { validateLexicons } from '@bigmoves/lexicon';
|
|
14
|
-
import { packCursor, unpackCursor, isTakendownDid, filterTakendownDids } from "./db.js";
|
|
16
|
+
import { packCursor, unpackCursor, isTakendownDid, filterTakendownDids } from "./database/db.js";
|
|
15
17
|
import { seed as createSeedHelpers } from "./seed.js";
|
|
18
|
+
import { setPrivateCollections } from "./private-collections.js";
|
|
19
|
+
import { spaceFilterSql } from "./spaces/visibility.js";
|
|
20
|
+
import { guardedQuerySQL, unfilteredQuerySQL } from "./spaces/guard.js";
|
|
16
21
|
/**
|
|
17
|
-
* Find the project's config.
|
|
18
|
-
* Returns the resolved config path, or falls back to 'config.
|
|
22
|
+
* Find the project's hatk.config.ts by walking up from cwd.
|
|
23
|
+
* Returns the resolved config path, or falls back to 'hatk.config.ts'.
|
|
19
24
|
*/
|
|
20
25
|
function findConfigPath() {
|
|
21
26
|
const explicit = process.env.APPVIEW_CONFIG;
|
|
22
27
|
if (explicit)
|
|
23
28
|
return resolve(explicit);
|
|
24
|
-
return resolve('config.
|
|
29
|
+
return resolve('hatk.config.ts');
|
|
25
30
|
}
|
|
26
31
|
/**
|
|
27
32
|
* Boot an in-memory hatk context for unit tests.
|
|
@@ -33,13 +38,15 @@ function findConfigPath() {
|
|
|
33
38
|
* but it will NOT work with --pool=threads (multiple tests sharing a process).
|
|
34
39
|
*/
|
|
35
40
|
export async function createTestContext() {
|
|
41
|
+
registerHatkResolveHook();
|
|
36
42
|
const configPath = findConfigPath();
|
|
37
|
-
const config = loadConfig(configPath);
|
|
43
|
+
const config = await loadConfig(configPath);
|
|
38
44
|
const configDir = dirname(resolve(configPath));
|
|
39
45
|
configureRelay(config.relay);
|
|
46
|
+
configureCdn(config.cdn);
|
|
40
47
|
// Load and validate lexicons
|
|
41
48
|
const lexicons = loadLexicons(resolve(configDir, 'lexicons'));
|
|
42
|
-
const lexiconErrors = validateLexicons(
|
|
49
|
+
const lexiconErrors = validateLexicons(validatableLexicons(lexicons));
|
|
43
50
|
if (lexiconErrors) {
|
|
44
51
|
const messages = Object.entries(lexiconErrors).flatMap(([nsid, errs]) => errs.map((e) => `${nsid}: ${e}`));
|
|
45
52
|
throw new Error(`Invalid lexicons:\n${messages.join('\n')}`);
|
|
@@ -56,25 +63,30 @@ export async function createTestContext() {
|
|
|
56
63
|
continue;
|
|
57
64
|
const schema = generateTableSchema(nsid, lexicon, lexicons);
|
|
58
65
|
schemas.push(schema);
|
|
59
|
-
ddlStatements.push(generateCreateTableSQL(schema));
|
|
66
|
+
ddlStatements.push(generateCreateTableSQL(schema, SQLITE_DIALECT));
|
|
60
67
|
}
|
|
61
|
-
// In-memory
|
|
62
|
-
await
|
|
63
|
-
|
|
68
|
+
// In-memory SQLite — faster startup, no native module issues in Vite's module runner
|
|
69
|
+
const { adapter, searchPort } = await createAdapter('sqlite');
|
|
70
|
+
setSearchPort(searchPort);
|
|
71
|
+
await initDatabase(adapter, ':memory:', schemas, ddlStatements);
|
|
72
|
+
// Create the FTS shadow tables up front. main.ts does this after backfill;
|
|
73
|
+
// a test has no backfill, so without it the tables never exist and every
|
|
74
|
+
// ctx.search() fails its BM25 phase with "no such table: _fts_...". SQLite
|
|
75
|
+
// has no fuzzy phase to fall back on — jaroWinklerSimilarity is null there —
|
|
76
|
+
// so the failure surfaces as an empty result rather than an error, which is
|
|
77
|
+
// why it went unnoticed.
|
|
78
|
+
//
|
|
79
|
+
// The indexes are empty at this point; what matters is that this also fills
|
|
80
|
+
// the search-column cache, which is what lets insertRecord() index each
|
|
81
|
+
// record as it is written. Records inserted with raw SQL bypass that path and
|
|
82
|
+
// remain unsearchable, as they do in production.
|
|
83
|
+
await rebuildAllIndexes(collections);
|
|
84
|
+
// Discover views
|
|
64
85
|
discoverViews();
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
catch { }
|
|
69
|
-
// Skip setup hooks in test context — they're for server boot-time
|
|
70
|
-
// initialization (e.g. importing large datasets) and not appropriate for tests
|
|
71
|
-
// Discover feeds, xrpc, labels
|
|
72
|
-
await initFeeds(resolve(configDir, 'feeds'));
|
|
73
|
-
await initXrpc(resolve(configDir, 'xrpc'));
|
|
74
|
-
await initOpengraph(resolve(configDir, 'og'));
|
|
75
|
-
await initLabels(resolve(configDir, 'labels'));
|
|
86
|
+
// Discover feeds, xrpc, labels, hooks, og from server/ directory (skip setup scripts in tests)
|
|
87
|
+
await initServer(resolve(configDir, 'server'), { skipSetup: true });
|
|
76
88
|
return {
|
|
77
|
-
db: { query:
|
|
89
|
+
db: { query: guardedQuerySQL, run: runSQL, unfiltered: unfilteredQuerySQL },
|
|
78
90
|
_config: config,
|
|
79
91
|
_collections: collections,
|
|
80
92
|
loadFixtures: async (dir) => {
|
|
@@ -94,7 +106,12 @@ export async function createTestContext() {
|
|
|
94
106
|
if (Array.isArray(records)) {
|
|
95
107
|
for (const rec of records) {
|
|
96
108
|
const row = interpolateHelpers(rec);
|
|
97
|
-
await runSQL(`INSERT OR IGNORE INTO _repos (did, status, handle, backfilled_at) VALUES ($1, $2, $3, $4)`,
|
|
109
|
+
await runSQL(`INSERT OR IGNORE INTO _repos (did, status, handle, backfilled_at) VALUES ($1, $2, $3, $4)`, [
|
|
110
|
+
row.did,
|
|
111
|
+
row.status || 'active',
|
|
112
|
+
row.handle || row.did.split(':').pop() + '.test',
|
|
113
|
+
new Date().toISOString(),
|
|
114
|
+
]);
|
|
98
115
|
}
|
|
99
116
|
}
|
|
100
117
|
}
|
|
@@ -119,7 +136,7 @@ export async function createTestContext() {
|
|
|
119
136
|
const row = interpolateHelpers(rec);
|
|
120
137
|
const vals = keys.map((k) => row[k]);
|
|
121
138
|
const placeholders = keys.map((_, i) => `$${i + 1}`).join(', ');
|
|
122
|
-
await runSQL(`INSERT INTO "${tableName}" (${keys.map((k) => `"${k}"`).join(', ')}) VALUES (${placeholders})`,
|
|
139
|
+
await runSQL(`INSERT INTO "${tableName}" (${keys.map((k) => `"${k}"`).join(', ')}) VALUES (${placeholders})`, vals);
|
|
123
140
|
}
|
|
124
141
|
continue;
|
|
125
142
|
}
|
|
@@ -133,7 +150,12 @@ export async function createTestContext() {
|
|
|
133
150
|
// Auto-register DID in _repos if not already present
|
|
134
151
|
if (!seenDids.has(did)) {
|
|
135
152
|
seenDids.add(did);
|
|
136
|
-
await runSQL(`INSERT OR IGNORE INTO _repos (did, status, handle, backfilled_at) VALUES ($1, $2, $3, $4)`,
|
|
153
|
+
await runSQL(`INSERT OR IGNORE INTO _repos (did, status, handle, backfilled_at) VALUES ($1, $2, $3, $4)`, [
|
|
154
|
+
did,
|
|
155
|
+
'active',
|
|
156
|
+
did.split(':').pop() + '.test',
|
|
157
|
+
new Date().toISOString(),
|
|
158
|
+
]);
|
|
137
159
|
}
|
|
138
160
|
await insertRecord(tableName, uri, cid, did, fields);
|
|
139
161
|
}
|
|
@@ -164,14 +186,14 @@ export async function createTestContext() {
|
|
|
164
186
|
},
|
|
165
187
|
feedContext: (opts) => {
|
|
166
188
|
const paginateDeps = {
|
|
167
|
-
db: { query:
|
|
189
|
+
db: { query: guardedQuerySQL, unfiltered: unfilteredQuerySQL },
|
|
168
190
|
cursor: opts?.cursor,
|
|
169
191
|
limit: opts?.limit || 30,
|
|
170
192
|
packCursor,
|
|
171
193
|
unpackCursor,
|
|
172
194
|
};
|
|
173
195
|
return {
|
|
174
|
-
db: { query:
|
|
196
|
+
db: { query: guardedQuerySQL, unfiltered: unfilteredQuerySQL },
|
|
175
197
|
params: opts?.params || {},
|
|
176
198
|
cursor: opts?.cursor,
|
|
177
199
|
limit: opts?.limit || 30,
|
|
@@ -181,6 +203,7 @@ export async function createTestContext() {
|
|
|
181
203
|
isTakendown: isTakendownDid,
|
|
182
204
|
filterTakendownDids,
|
|
183
205
|
paginate: createPaginate(paginateDeps),
|
|
206
|
+
spaceFilter: spaceFilterSql,
|
|
184
207
|
};
|
|
185
208
|
},
|
|
186
209
|
close: async () => {
|
|
@@ -220,10 +243,11 @@ export async function startTestServer() {
|
|
|
220
243
|
// Import startServer — it creates the HTTP server and returns it
|
|
221
244
|
const { startServer } = await import("./server.js");
|
|
222
245
|
// Start server on port 0 (random available port)
|
|
223
|
-
const resolveViewer = (
|
|
224
|
-
const did =
|
|
246
|
+
const resolveViewer = (request) => {
|
|
247
|
+
const did = request.headers.get('x-test-viewer');
|
|
225
248
|
return typeof did === 'string' ? { did } : null;
|
|
226
249
|
};
|
|
250
|
+
setPrivateCollections(ctx._config.privateCollections);
|
|
227
251
|
const httpServer = startServer(0, ctx._collections, ctx._config.publicDir, ctx._config.oauth, ctx._config.admins, resolveViewer);
|
|
228
252
|
await new Promise((resolve) => httpServer.on('listening', resolve));
|
|
229
253
|
const port = httpServer.address().port;
|
package/dist/views.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// 1. Inline views: defined in the record lexicon with ref: "#main" (e.g., playView)
|
|
4
4
|
// 2. Defs views: defined in a defs lexicon, associated by naming convention (e.g., profileView)
|
|
5
5
|
import { log } from "./logger.js";
|
|
6
|
-
import { getAllLexicons, getLexicon } from "./schema.js";
|
|
6
|
+
import { getAllLexicons, getLexicon } from "./database/schema.js";
|
|
7
7
|
// --- Registry ---
|
|
8
8
|
/** All views keyed by full NSID (e.g., "fm.teal.alpha.feed.play#playView") */
|
|
9
9
|
const views = new Map();
|
package/dist/vite-plugin.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vite-plugin.d.ts","sourceRoot":"","sources":["../src/vite-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"vite-plugin.d.ts","sourceRoot":"","sources":["../src/vite-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgC,KAAK,MAAM,EAA6C,MAAM,MAAM,CAAA;AA8D3G,wBAAgB,IAAI,CAAC,IAAI,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAoOrD"}
|