···88import { Firehose } from '@atproto/sync';
99import { IdResolver } from '@atproto/identity';
1010import { PrismaService } from '../prisma/prisma.service';
1111-1212-const COLLECTION = 'app.opnshelf.movie';
1313-1414-// Expected record shape for app.opnshelf.movie
1515-interface MovieRecord {
1616- $type: string;
1717- movieId: string;
1818- source: string;
1919- watchedAt: string;
2020- createdAt: string;
2121-}
2222-2323-function isMovieRecord(record: unknown): record is MovieRecord {
2424- if (!record || typeof record !== 'object') return false;
2525- const r = record as Record<string, unknown>;
2626- return (
2727- (r.$type === COLLECTION || r.$type === `${COLLECTION}#main`) &&
2828- typeof r.movieId === 'string' &&
2929- typeof r.source === 'string' &&
3030- typeof r.watchedAt === 'string' &&
3131- typeof r.createdAt === 'string'
3232- );
3333-}
1111+import {
1212+ main as movieSchema,
1313+ $nsid as COLLECTION,
1414+} from '../lexicons/app/opnshelf/movie';
1515+import type { Main as MovieRecord } from '../lexicons/app/opnshelf/movie.defs';
34163517interface FirehoseEvent {
3618 event: string;
···11698 if (evt.collection !== COLLECTION) return;
11799118100 const record = evt.record;
119119- if (!isMovieRecord(record)) {
101101+102102+ // Validate the record using the generated schema
103103+ let movieRecord: MovieRecord;
104104+ try {
105105+ movieRecord = movieSchema.parse(record);
106106+ } catch {
120107 this.logger.debug('Received invalid movie record, skipping');
121108 return;
122109 }
···153140 rkey,
154141 cid,
155142 userDid: authorDid,
156156- movieId: record.movieId,
157157- watchedDate: new Date(record.watchedAt),
143143+ movieId: movieRecord.movieId,
144144+ watchedDate: new Date(movieRecord.watchedAt),
158145 status: 'watched',
159146 },
160147 update: {
161148 cid,
162162- watchedDate: new Date(record.watchedAt),
149149+ watchedDate: new Date(movieRecord.watchedAt),
163150 status: 'watched',
164151 },
165152 });
166153167154 this.logger.debug(
168168- `Indexed movie ${record.movieId} for user ${authorDid}`,
155155+ `Indexed movie ${movieRecord.movieId} for user ${authorDid}`,
169156 );
170157 }
171158
+5
backend/src/lexicons/app.ts
···11+/*
22+ * THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
33+ */
44+55+export * as opnshelf from './app/opnshelf.js';
+5
backend/src/lexicons/app/opnshelf.ts
···11+/*
22+ * THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
33+ */
44+55+export * as movie from './opnshelf/movie.js';
+63
backend/src/lexicons/app/opnshelf/movie.defs.ts
···11+/*
22+ * THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
33+ */
44+55+import { l } from '@atproto/lex';
66+77+const $nsid = 'app.opnshelf.movie';
88+99+export { $nsid };
1010+1111+/** A tracked movie record for OpnShelf */
1212+type Main = {
1313+ $type: 'app.opnshelf.movie';
1414+1515+ /**
1616+ * TMDB movie ID
1717+ */
1818+ movieId: string;
1919+2020+ /**
2121+ * Source of the movie data (e.g., tmdb)
2222+ */
2323+ source: string;
2424+2525+ /**
2626+ * When the movie was watched
2727+ */
2828+ watchedAt: l.DatetimeString;
2929+3030+ /**
3131+ * Record creation timestamp
3232+ */
3333+ createdAt: l.DatetimeString;
3434+};
3535+3636+export type { Main };
3737+3838+/** A tracked movie record for OpnShelf */
3939+const main = l.record<'tid', Main>(
4040+ 'tid',
4141+ $nsid,
4242+ l.object({
4343+ movieId: l.string(),
4444+ source: l.string(),
4545+ watchedAt: l.string({ format: 'datetime' }),
4646+ createdAt: l.string({ format: 'datetime' }),
4747+ }),
4848+);
4949+5050+export { main };
5151+5252+export const $isTypeOf = /*#__PURE__*/ main.isTypeOf.bind(main),
5353+ $build = /*#__PURE__*/ main.build.bind(main),
5454+ $type = /*#__PURE__*/ main.$type;
5555+export const $assert = /*#__PURE__*/ main.assert.bind(main),
5656+ $check = /*#__PURE__*/ main.check.bind(main),
5757+ $cast = /*#__PURE__*/ main.cast.bind(main),
5858+ $ifMatches = /*#__PURE__*/ main.ifMatches.bind(main),
5959+ $matches = /*#__PURE__*/ main.matches.bind(main),
6060+ $parse = /*#__PURE__*/ main.parse.bind(main),
6161+ $safeParse = /*#__PURE__*/ main.safeParse.bind(main),
6262+ $validate = /*#__PURE__*/ main.validate.bind(main),
6363+ $safeValidate = /*#__PURE__*/ main.safeValidate.bind(main);
+6
backend/src/lexicons/app/opnshelf/movie.ts
···11+/*
22+ * THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
33+ */
44+55+export * from './movie.defs.js';
66+export * as $defs from './movie.defs.js';
+9-6
backend/src/movies/movies.service.ts
···22import { PrismaService } from '../prisma/prisma.service';
33import { ConfigService } from '@nestjs/config';
44import { Agent } from '@atproto/api';
55-66-const COLLECTION = 'app.opnshelf.movie';
55+import {
66+ main as movieSchema,
77+ $nsid as COLLECTION,
88+} from '../lexicons/app/opnshelf/movie';
99+import type { Main as MovieRecord } from '../lexicons/app/opnshelf/movie.defs';
710811export interface TMDBMovie {
912 id: number;
···118121 const rkey = `movie-${movieId}`;
119122 const now = new Date().toISOString();
120123121121- // Build the AT Protocol record
122122- const record = {
123123- $type: COLLECTION,
124124+ // Build the AT Protocol record using the generated schema builder
125125+ // This ensures type safety and validation
126126+ const record: MovieRecord = movieSchema.build({
124127 movieId,
125128 source: 'tmdb',
126129 watchedAt: now,
127130 createdAt: now,
128128- };
131131+ });
129132130133 // Create agent from session and write record to user's PDS
131134 const agent = new Agent(
+8-6
lexicons/app/opnshelf/movie.json
···88 "key": "tid",
99 "record": {
1010 "type": "object",
1111- "required": ["movieId", "status"],
1111+ "required": ["movieId", "source", "watchedAt", "createdAt"],
1212 "properties": {
1313 "movieId": {
1414 "type": "string",
1515 "description": "TMDB movie ID"
1616 },
1717- "status": {
1717+ "source": {
1818 "type": "string",
1919- "description": "Watch status of the movie (e.g., watched, wantToWatch, watching)"
1919+ "description": "Source of the movie data (e.g., tmdb)"
2020 },
2121- "watchedDate": {
2222- "type": "datetime",
2121+ "watchedAt": {
2222+ "type": "string",
2323+ "format": "datetime",
2324 "description": "When the movie was watched"
2425 },
2526 "createdAt": {
2626- "type": "datetime",
2727+ "type": "string",
2828+ "format": "datetime",
2729 "description": "Record creation timestamp"
2830 }
2931 }