Scratch space for learning atproto app development

Update tutorial to reflect changes to example app

Changed files
+32 -30
lexicons
+31 -29
TUTORIAL.md
···
- `bsky.app` is feeling 🦋 according to `https://bsky.app/status.json`
- `reddit.com` is feeling 🤓 according to `https://reddit.com/status.json`
-
The Atmosphere works the same way, except we're going to check `at://` instead of `https://`. Each user has a data repo under an `at://` URL. We'll crawl all the `at://`s in the Atmosphere for all the `/status.json` records and aggregate them into our SQLite database.
## Step 1. Starting with our ExpressJS app
···
await agent.getRecord({
repo: agent.accountDid, // The user
collection: 'app.bsky.actor.profile', // The collection
-
rkey: 'self', // The record name
})
```
We write records using a similar API. Since our goal is to write "status" records, let's look at how that will happen:
```typescript
await agent.putRecord({
repo: agent.accountDid, // The user
collection: 'com.example.status', // The collection
-
rkey: 'self', // The record name
record: { // The record value
status: "👍",
-
updatedAt: new Date().toISOString()
}
})
```
···
const record = {
$type: 'com.example.status',
status: req.body?.status,
-
updatedAt: new Date().toISOString(),
}
try {
···
await agent.putRecord({
repo: agent.accountDid,
collection: 'com.example.status',
-
rkey: 'self',
record,
})
} catch (err) {
···
> ### Why create a schema?
>
-
> Schemas help other applications understand the data your app is creating. By publishing your schemas, you enable compatibility and reduce the chances of bad data affecting your app.
Let's create our schema in the `/lexicons` folder of our codebase. You can [read more about how to define schemas here](#todo).
···
"defs": {
"main": {
"type": "record",
-
"key": "literal:self",
"record": {
"type": "object",
-
"required": ["status", "updatedAt"],
"properties": {
"status": {
"type": "string",
···
"maxGraphemes": 1,
"maxLength": 32
},
-
"updatedAt": {
"type": "string",
"format": "datetime"
}
···
const record = {
$type: 'com.example.status',
status: req.body?.status,
-
updatedAt: new Date().toISOString(),
}
if (!Status.validateRecord(record).success) {
return res.status(400).json({ error: 'Invalid status' })
···
│ REPO │ Event stream
├──────┘
│ ┌───────────────────────────────────────────┐
-
├───┼ 1 PUT /com.example.status/self │
│ └───────────────────────────────────────────┘
│ ┌───────────────────────────────────────────┐
├───┼ 2 DEL /app.bsky.feed.post/3l244rmrxjx2v │
│ └───────────────────────────────────────────┘
│ ┌───────────────────────────────────────────┐
-
├───┼ 3 PUT /app.bsky.actor/self │
▼ └───────────────────────────────────────────┘
```
···
// Create our statuses table
await db.schema
.createTable('status')
-
.addColumn('authorDid', 'varchar', (col) => col.primaryKey())
.addColumn('status', 'varchar', (col) => col.notNull())
-
.addColumn('updatedAt', 'varchar', (col) => col.notNull())
.addColumn('indexedAt', 'varchar', (col) => col.notNull())
.execute()
```
···
await db
.insertInto('status')
.values({
authorDid: evt.author,
status: record.status,
-
updatedAt: record.updatedAt,
indexedAt: new Date().toISOString(),
})
.onConflict((oc) =>
-
oc.column('authorDid').doUpdateSet({
status: record.status,
-
updatedAt: record.updatedAt,
indexedAt: new Date().toISOString(),
})
)
···
<!-- src/pages/home.ts -->
${statuses.map((status, i) => {
const handle = didHandleMap[status.authorDid] || status.authorDid
-
const date = ts(status)
return html`
<div class="status-line">
<div>
···
handler(async (req, res) => {
// ...
try {
// Write the status record to the user's repository
-
await agent.putRecord({
repo: agent.accountDid,
collection: 'com.example.status',
-
rkey: 'self',
record,
})
} catch (err) {
ctx.logger.warn({ err }, 'failed to write record')
return res.status(500).json({ error: 'Failed to write record' })
···
await ctx.db
.insertInto('status')
.values({
authorDid: agent.accountDid,
status: record.status,
-
updatedAt: record.updatedAt,
indexedAt: new Date().toISOString(),
})
-
.onConflict((oc) =>
-
oc.column('authorDid').doUpdateSet({
-
status: record.status,
-
updatedAt: record.updatedAt,
-
indexedAt: new Date().toISOString(),
-
})
-
)
.execute()
} catch (err) {
ctx.logger.warn(
···
- `bsky.app` is feeling 🦋 according to `https://bsky.app/status.json`
- `reddit.com` is feeling 🤓 according to `https://reddit.com/status.json`
+
The Atmosphere works the same way, except we're going to check `at://` instead of `https://`. Each user has a data repo under an `at://` URL. We'll crawl all the `at://`s in the Atmosphere for all the "status.json" records and aggregate them into our SQLite database.
+
+
> `at://` is the URL scheme of the AT Protocol.
## Step 1. Starting with our ExpressJS app
···
await agent.getRecord({
repo: agent.accountDid, // The user
collection: 'app.bsky.actor.profile', // The collection
+
rkey: 'self', // The record key
})
```
We write records using a similar API. Since our goal is to write "status" records, let's look at how that will happen:
```typescript
+
// Generate a time-based key for our record
+
const rkey = TID.nextStr()
+
+
// Write the
await agent.putRecord({
repo: agent.accountDid, // The user
collection: 'com.example.status', // The collection
+
rkey, // The record key
record: { // The record value
status: "👍",
+
createdAt: new Date().toISOString()
}
})
```
···
const record = {
$type: 'com.example.status',
status: req.body?.status,
+
createdAt: new Date().toISOString(),
}
try {
···
await agent.putRecord({
repo: agent.accountDid,
collection: 'com.example.status',
+
rkey: TID.nextStr(),
record,
})
} catch (err) {
···
> ### Why create a schema?
>
+
> Schemas help other applications understand the data your app is creating. By publishing your schemas, you enable compatibility with other apps and reduce the chances of bad data affecting your app.
Let's create our schema in the `/lexicons` folder of our codebase. You can [read more about how to define schemas here](#todo).
···
"defs": {
"main": {
"type": "record",
+
"key": "tid",
"record": {
"type": "object",
+
"required": ["status", "createdAt"],
"properties": {
"status": {
"type": "string",
···
"maxGraphemes": 1,
"maxLength": 32
},
+
"createdAt": {
"type": "string",
"format": "datetime"
}
···
const record = {
$type: 'com.example.status',
status: req.body?.status,
+
createdAt: new Date().toISOString(),
}
if (!Status.validateRecord(record).success) {
return res.status(400).json({ error: 'Invalid status' })
···
│ REPO │ Event stream
├──────┘
│ ┌───────────────────────────────────────────┐
+
├───┼ 1 PUT /app.bsky.feed.post/3l244rmrxjx2v │
│ └───────────────────────────────────────────┘
│ ┌───────────────────────────────────────────┐
├───┼ 2 DEL /app.bsky.feed.post/3l244rmrxjx2v │
│ └───────────────────────────────────────────┘
│ ┌───────────────────────────────────────────┐
+
├───┼ 3 PUT /app.bsky.actor.profile/self │
▼ └───────────────────────────────────────────┘
```
···
// Create our statuses table
await db.schema
.createTable('status')
+
.addColumn('uri', 'varchar', (col) => col.primaryKey())
+
.addColumn('authorDid', 'varchar', (col) => col.notNull())
.addColumn('status', 'varchar', (col) => col.notNull())
+
.addColumn('createdAt', 'varchar', (col) => col.notNull())
.addColumn('indexedAt', 'varchar', (col) => col.notNull())
.execute()
```
···
await db
.insertInto('status')
.values({
+
uri: evt.uri.toString(),
authorDid: evt.author,
status: record.status,
+
createdAt: record.createdAt,
indexedAt: new Date().toISOString(),
})
.onConflict((oc) =>
+
oc.column('uri').doUpdateSet({
status: record.status,
indexedAt: new Date().toISOString(),
})
)
···
<!-- src/pages/home.ts -->
${statuses.map((status, i) => {
const handle = didHandleMap[status.authorDid] || status.authorDid
return html`
<div class="status-line">
<div>
···
handler(async (req, res) => {
// ...
+
let uri
try {
// Write the status record to the user's repository
+
const res = await agent.putRecord({
repo: agent.accountDid,
collection: 'com.example.status',
+
rkey: TID.nextStr(),
record,
})
+
uri = res.uri
} catch (err) {
ctx.logger.warn({ err }, 'failed to write record')
return res.status(500).json({ error: 'Failed to write record' })
···
await ctx.db
.insertInto('status')
.values({
+
uri,
authorDid: agent.accountDid,
status: record.status,
+
createdAt: record.createdAt,
indexedAt: new Date().toISOString(),
})
.execute()
} catch (err) {
ctx.logger.warn(
+1 -1
lexicons/status.json
···
"defs": {
"main": {
"type": "record",
-
"key": "literal:self",
"record": {
"type": "object",
"required": ["status", "createdAt"],
···
"defs": {
"main": {
"type": "record",
+
"key": "tid",
"record": {
"type": "object",
"required": ["status", "createdAt"],