From 0b70431fe94f3b11f15d374ef7c68c419505718e Mon Sep 17 00:00:00 2001 From: Evan Broder Date: Fri, 8 Sep 2023 10:35:17 -0700 Subject: [PATCH] Allow declaring methods and publications under models Almost all of our publications and models are associated (or at least primarily associated) with a model. This is reflected in the way we name our methods ("verbNoun"). However, we previously placed all methods in a single flat directory and did the same with publications. Instead, allow declaring methods and publications as part of creating a new model class. This has the convenient side-effect of reducing the imports you need to operate on a model to just the model class itself. It also makes the internal identifier for a method or publication match how we refer to it in code (e.g. `ChatMessages.methods.send`). For now, the implementations of the methods and publications are still in the same files as before; that's potentially something to reevaluate in the future. Additionally, it arguably makes it harder to enumerate all methods and publications, but that might be an acceptable tradeoff. --- imports/client/components/FirehosePage.tsx | 3 +-- imports/client/components/PuzzlePage.tsx | 6 ++--- imports/lib/models/BlobMappings.ts | 2 +- imports/lib/models/ChatMessages.ts | 15 ++++++++++- imports/lib/models/Model.ts | 27 ++++++++++++++++--- imports/lib/models/SoftDeletedModel.ts | 16 ++++++++++- .../publications/chatMessagesForFirehose.ts | 5 ---- .../lib/publications/chatMessagesForPuzzle.ts | 5 ---- imports/methods/sendChatMessage.ts | 8 ------ imports/server/methods/sendChatMessage.ts | 4 +-- imports/server/models/Blobs.ts | 2 +- imports/server/models/DriveActivityLatests.ts | 2 +- .../models/LatestDeploymentTimestamps.ts | 2 +- .../publications/chatMessagesForFirehose.ts | 3 +-- .../publications/chatMessagesForPuzzle.ts | 3 +-- imports/server/publishJoinedQuery.ts | 2 +- tests/acceptance/chatHooks.ts | 3 +-- 17 files changed, 65 insertions(+), 43 deletions(-) delete mode 100644 imports/lib/publications/chatMessagesForFirehose.ts delete mode 100644 imports/lib/publications/chatMessagesForPuzzle.ts delete mode 100644 imports/methods/sendChatMessage.ts diff --git a/imports/client/components/FirehosePage.tsx b/imports/client/components/FirehosePage.tsx index e25b3bbe1..aea2060fd 100644 --- a/imports/client/components/FirehosePage.tsx +++ b/imports/client/components/FirehosePage.tsx @@ -18,7 +18,6 @@ import ChatMessages from '../../lib/models/ChatMessages'; import Puzzles from '../../lib/models/Puzzles'; import type { PuzzleType } from '../../lib/models/Puzzles'; import nodeIsMention from '../../lib/nodeIsMention'; -import chatMessagesForFirehose from '../../lib/publications/chatMessagesForFirehose'; import { useBreadcrumb } from '../hooks/breadcrumb'; import useSubscribeDisplayNames from '../hooks/useSubscribeDisplayNames'; import useTypedSubscribe from '../hooks/useTypedSubscribe'; @@ -115,7 +114,7 @@ const FirehosePage = () => { useBreadcrumb({ title: 'Firehose', path: `/hunts/${huntId}/firehose` }); const profilesLoading = useSubscribeDisplayNames(huntId); - const chatMessagesLoading = useTypedSubscribe(chatMessagesForFirehose, { huntId }); + const chatMessagesLoading = useTypedSubscribe(ChatMessages.publications.forFirehose, { huntId }); const loading = profilesLoading() || chatMessagesLoading(); const displayNames = useTracker(() => { diff --git a/imports/client/components/PuzzlePage.tsx b/imports/client/components/PuzzlePage.tsx index e7a56c222..32e747aff 100644 --- a/imports/client/components/PuzzlePage.tsx +++ b/imports/client/components/PuzzlePage.tsx @@ -68,7 +68,6 @@ import type { TagType } from '../../lib/models/Tags'; import nodeIsMention from '../../lib/nodeIsMention'; import nodeIsText from '../../lib/nodeIsText'; import { userMayWritePuzzlesForHunt } from '../../lib/permission_stubs'; -import chatMessagesForPuzzle from '../../lib/publications/chatMessagesForPuzzle'; import puzzleForPuzzlePage from '../../lib/publications/puzzleForPuzzlePage'; import { computeSolvedness } from '../../lib/solvedness'; import addPuzzleAnswer from '../../methods/addPuzzleAnswer'; @@ -81,7 +80,6 @@ import type { Sheet } from '../../methods/listDocumentSheets'; import listDocumentSheets from '../../methods/listDocumentSheets'; import removePuzzleAnswer from '../../methods/removePuzzleAnswer'; import removePuzzleTag from '../../methods/removePuzzleTag'; -import sendChatMessage from '../../methods/sendChatMessage'; import undestroyPuzzle from '../../methods/undestroyPuzzle'; import updatePuzzle from '../../methods/updatePuzzle'; import GoogleScriptInfo from '../GoogleScriptInfo'; @@ -601,7 +599,7 @@ const ChatInput = React.memo(({ }; // Send chat message. - sendChatMessage.call({ puzzleId, content: JSON.stringify(cleanedMessage) }); + ChatMessages.methods.send.call({ puzzleId, content: JSON.stringify(cleanedMessage) }); setContent(initialValue); fancyEditorRef.current?.clearInput(); if (onMessageSent) { @@ -1802,7 +1800,7 @@ const PuzzlePage = React.memo(() => { const puzzleLoading = useTypedSubscribe(puzzleForPuzzlePage, { puzzleId, huntId }); - const chatMessagesLoading = useTypedSubscribe(chatMessagesForPuzzle, { puzzleId, huntId }); + const chatMessagesLoading = useTypedSubscribe(ChatMessages.publications.forPuzzle, { puzzleId, huntId }); // There are some model dependencies that we have to be careful about: // diff --git a/imports/lib/models/BlobMappings.ts b/imports/lib/models/BlobMappings.ts index d765406e7..53a06e9dc 100644 --- a/imports/lib/models/BlobMappings.ts +++ b/imports/lib/models/BlobMappings.ts @@ -9,7 +9,7 @@ const BlobMapping = z.object({ blob: z.string().regex(/^[a-fA-F0-9]{64}$/), }); -const BlobMappings = new Model('jr_blob_mappings', BlobMapping, nonEmptyString); +const BlobMappings = new Model('jr_blob_mappings', BlobMapping, {}, {}, nonEmptyString); export type BlobMappingType = ModelType; export default BlobMappings; diff --git a/imports/lib/models/ChatMessages.ts b/imports/lib/models/ChatMessages.ts index 91848b99f..c4fc29c96 100644 --- a/imports/lib/models/ChatMessages.ts +++ b/imports/lib/models/ChatMessages.ts @@ -1,4 +1,6 @@ import { z } from 'zod'; +import TypedMethod from '../../methods/TypedMethod'; +import TypedPublication from '../publications/TypedPublication'; import type { ModelType } from './Model'; import SoftDeletedModel from './SoftDeletedModel'; import { allowedEmptyString, foreignKey } from './customTypes'; @@ -44,7 +46,18 @@ const ChatMessage = withCommon(z.object({ // The date this message was sent. Used for ordering chats in the log. timestamp: z.date(), })); -const ChatMessages = new SoftDeletedModel('jr_chatmessages', ChatMessage); +const ChatMessages = new SoftDeletedModel('jr_chatmessages', ChatMessage, { + send: new TypedMethod<{ puzzleId: string, content: string }, void>( + 'ChatMessages.methods.send' + ), +}, { + forPuzzle: new TypedPublication<{ puzzleId: string, huntId: string }>( + 'ChatMessages.publications.forPuzzle' + ), + forFirehose: new TypedPublication<{ huntId: string }>( + 'ChatMessages.publications.forFirehose' + ), +}); ChatMessages.addIndex({ deleted: 1, puzzle: 1 }); ChatMessages.addIndex({ hunt: 1, createdAt: 1 }); export type ChatMessageType = ModelType; diff --git a/imports/lib/models/Model.ts b/imports/lib/models/Model.ts index f1b305b6a..8423ea851 100644 --- a/imports/lib/models/Model.ts +++ b/imports/lib/models/Model.ts @@ -3,6 +3,8 @@ import type { Document, IndexDirection, IndexSpecification, CreateIndexesOptions, } from 'mongodb'; import { z } from 'zod'; +import type TypedMethod from '../../methods/TypedMethod'; +import type TypedPublication from '../publications/TypedPublication'; import { IsInsert, IsUpdate, IsUpsert, stringId, } from './customTypes'; @@ -345,9 +347,14 @@ export function normalizeIndexOptions(options: CreateIndexesOptions): Normalized .sort(); } -export const AllModels = new Set>(); +export const AllModels = new Set>(); -class Model { +class Model< + Schema extends MongoRecordZodType, + Methods extends Readonly>> = Record, + Publications extends Readonly>> = Record, + IdSchema extends z.ZodTypeAny = typeof stringId, +> { name: string; schema: Schema extends z.ZodObject< @@ -362,7 +369,17 @@ class Model> = z.output; +export type ModelType> = z.output; export default Model; diff --git a/imports/lib/models/SoftDeletedModel.ts b/imports/lib/models/SoftDeletedModel.ts index 87bb51d40..b8259035a 100644 --- a/imports/lib/models/SoftDeletedModel.ts +++ b/imports/lib/models/SoftDeletedModel.ts @@ -1,5 +1,7 @@ import { Mongo } from 'meteor/mongo'; import { z } from 'zod'; +import type TypedMethod from '../../methods/TypedMethod'; +import type TypedPublication from '../publications/TypedPublication'; import type { ModelType, Selector, SelectorToResultType } from './Model'; import Model from './Model'; import type { stringId } from './customTypes'; @@ -57,6 +59,8 @@ const injectOptions = >( class SoftDeletedModel< Schema extends MongoRecordZodType, + Methods extends Readonly>> = Record, + Publications extends Readonly>> = Record, IdSchema extends z.ZodTypeAny = typeof stringId > extends Model< Schema extends z.ZodObject< @@ -68,14 +72,24 @@ class SoftDeletedModel< z.objectUtil.extendShape, UnknownKeys, Catchall > : z.ZodIntersection>, + Methods, + Publications, IdSchema > { - constructor(name: string, schema: Schema, idSchema?: IdSchema) { + constructor( + name: string, + schema: Schema, + methods?: Methods, + publications?: Publications, + idSchema?: IdSchema, + ) { super( name, schema instanceof z.ZodObject ? schema.extend({ deleted }) : schema.and(z.object({ deleted })) as any, + methods, + publications, idSchema, ); } diff --git a/imports/lib/publications/chatMessagesForFirehose.ts b/imports/lib/publications/chatMessagesForFirehose.ts deleted file mode 100644 index 82a0c2791..000000000 --- a/imports/lib/publications/chatMessagesForFirehose.ts +++ /dev/null @@ -1,5 +0,0 @@ -import TypedPublication from './TypedPublication'; - -export default new TypedPublication<{ huntId: string }>( - 'ChatMessages.publications.forFirehose' -); diff --git a/imports/lib/publications/chatMessagesForPuzzle.ts b/imports/lib/publications/chatMessagesForPuzzle.ts deleted file mode 100644 index 22f23b810..000000000 --- a/imports/lib/publications/chatMessagesForPuzzle.ts +++ /dev/null @@ -1,5 +0,0 @@ -import TypedPublication from './TypedPublication'; - -export default new TypedPublication<{ puzzleId: string, huntId: string }>( - 'ChatMessages.publications.forPuzzle' -); diff --git a/imports/methods/sendChatMessage.ts b/imports/methods/sendChatMessage.ts deleted file mode 100644 index 0395191d0..000000000 --- a/imports/methods/sendChatMessage.ts +++ /dev/null @@ -1,8 +0,0 @@ -import TypedMethod from './TypedMethod'; - -export default new TypedMethod<{ - puzzleId: string, - content: string, -}, void>( - 'ChatMessages.methods.send' -); diff --git a/imports/server/methods/sendChatMessage.ts b/imports/server/methods/sendChatMessage.ts index 4b96fae23..44b835db6 100644 --- a/imports/server/methods/sendChatMessage.ts +++ b/imports/server/methods/sendChatMessage.ts @@ -1,9 +1,9 @@ import { check, Match } from 'meteor/check'; -import sendChatMessage from '../../methods/sendChatMessage'; +import ChatMessages from '../../lib/models/ChatMessages'; import sendChatMessageInternal from '../sendChatMessageInternal'; import defineMethod from './defineMethod'; -defineMethod(sendChatMessage, { +defineMethod(ChatMessages.methods.send, { validate(arg) { check(arg, { puzzleId: String, diff --git a/imports/server/models/Blobs.ts b/imports/server/models/Blobs.ts index 56cabab5a..dcb187ce0 100644 --- a/imports/server/models/Blobs.ts +++ b/imports/server/models/Blobs.ts @@ -16,7 +16,7 @@ export const Blob = z.object({ size: z.number().int().nonnegative(), }); -const Blobs = new Model('jr_blobs', Blob, z.string().regex(/^[a-fA-F0-9]{64}$/)); +const Blobs = new Model('jr_blobs', Blob, {}, {}, z.string().regex(/^[a-fA-F0-9]{64}$/)); export type BlobType = ModelType; export default Blobs; diff --git a/imports/server/models/DriveActivityLatests.ts b/imports/server/models/DriveActivityLatests.ts index a4b68180e..8a75e665c 100644 --- a/imports/server/models/DriveActivityLatests.ts +++ b/imports/server/models/DriveActivityLatests.ts @@ -8,7 +8,7 @@ const DriveActivityLatest = z.object({ ts: z.date(), }); -const DriveActivityLatests = new Model('jr_drive_activity_latests', DriveActivityLatest, z.literal('default')); +const DriveActivityLatests = new Model('jr_drive_activity_latests', DriveActivityLatest, {}, {}, z.literal('default')); export type DriveActivityLatestType = ModelType; export default DriveActivityLatests; diff --git a/imports/server/models/LatestDeploymentTimestamps.ts b/imports/server/models/LatestDeploymentTimestamps.ts index 00b616607..1be53b922 100644 --- a/imports/server/models/LatestDeploymentTimestamps.ts +++ b/imports/server/models/LatestDeploymentTimestamps.ts @@ -12,6 +12,6 @@ const LatestDeploymentTimestamp = withTimestamps(z.object({ gitRevision: nonEmptyString, })); -const LatestDeploymentTimestamps = new Model('jr_latest_deployment_timestamps', LatestDeploymentTimestamp, z.literal('default')); +const LatestDeploymentTimestamps = new Model('jr_latest_deployment_timestamps', LatestDeploymentTimestamp, {}, {}, z.literal('default')); export type LatestDeploymentTimestampType = ModelType; export default LatestDeploymentTimestamps; diff --git a/imports/server/publications/chatMessagesForFirehose.ts b/imports/server/publications/chatMessagesForFirehose.ts index a7702b18e..5b6c3f6c0 100644 --- a/imports/server/publications/chatMessagesForFirehose.ts +++ b/imports/server/publications/chatMessagesForFirehose.ts @@ -2,11 +2,10 @@ import { check } from 'meteor/check'; import ChatMessages from '../../lib/models/ChatMessages'; import MeteorUsers from '../../lib/models/MeteorUsers'; import Puzzles from '../../lib/models/Puzzles'; -import chatMessagesForFirehose from '../../lib/publications/chatMessagesForFirehose'; import publishJoinedQuery from '../publishJoinedQuery'; import definePublication from './definePublication'; -definePublication(chatMessagesForFirehose, { +definePublication(ChatMessages.publications.forFirehose, { validate(arg) { check(arg, { huntId: String, diff --git a/imports/server/publications/chatMessagesForPuzzle.ts b/imports/server/publications/chatMessagesForPuzzle.ts index a0b05ee9d..2e23ca4d2 100644 --- a/imports/server/publications/chatMessagesForPuzzle.ts +++ b/imports/server/publications/chatMessagesForPuzzle.ts @@ -1,10 +1,9 @@ import { check } from 'meteor/check'; import ChatMessages from '../../lib/models/ChatMessages'; import MeteorUsers from '../../lib/models/MeteorUsers'; -import chatMessagesForPuzzle from '../../lib/publications/chatMessagesForPuzzle'; import definePublication from './definePublication'; -definePublication(chatMessagesForPuzzle, { +definePublication(ChatMessages.publications.forPuzzle, { validate(arg) { check(arg, { puzzleId: String, diff --git a/imports/server/publishJoinedQuery.ts b/imports/server/publishJoinedQuery.ts index 08a311a20..1b3b395e1 100644 --- a/imports/server/publishJoinedQuery.ts +++ b/imports/server/publishJoinedQuery.ts @@ -17,7 +17,7 @@ type Projection = Partial>; // effectively building the tree in the generic parameters. I'm not entirely // sure how to actually do that. export type PublishSpec = { - model: Mongo.Collection | Model & MongoRecordZodType>, + model: Mongo.Collection | Model & MongoRecordZodType, any, any>, allowDeleted?: boolean, projection?: Projection, foreignKeys?: { diff --git a/tests/acceptance/chatHooks.ts b/tests/acceptance/chatHooks.ts index 76fd6412f..c1ce0eca2 100644 --- a/tests/acceptance/chatHooks.ts +++ b/tests/acceptance/chatHooks.ts @@ -2,7 +2,6 @@ import { promisify } from 'util'; import { Meteor } from 'meteor/meteor'; import { assert } from 'chai'; import ChatMessages from '../../imports/lib/models/ChatMessages'; -import chatMessagesForFirehose from '../../imports/lib/publications/chatMessagesForFirehose'; import createFixtureHunt from '../../imports/methods/createFixtureHunt'; import provisionFirstUser from '../../imports/methods/provisionFirstUser'; import setGuessState from '../../imports/methods/setGuessState'; @@ -32,7 +31,7 @@ if (Meteor.isClient) { const before = new Date(); await setGuessState.callPromise({ guessId, state: 'correct' }); - await typedSubscribe.async(chatMessagesForFirehose, { huntId }); + await typedSubscribe.async(ChatMessages.publications.forFirehose, { huntId }); const newMessages = await ChatMessages.find({ createdAt: { $gt: before }, }).fetchAsync();