Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow declaring methods and publications under models #1820

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions imports/client/components/FirehosePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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(() => {
Expand Down
6 changes: 2 additions & 4 deletions imports/client/components/PuzzlePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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:
//
Expand Down
2 changes: 1 addition & 1 deletion imports/lib/models/BlobMappings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof BlobMappings>;

export default BlobMappings;
15 changes: 14 additions & 1 deletion imports/lib/models/ChatMessages.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<typeof ChatMessages>;
Expand Down
27 changes: 23 additions & 4 deletions imports/lib/models/Model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -345,9 +347,14 @@ export function normalizeIndexOptions(options: CreateIndexesOptions): Normalized
.sort();
}

export const AllModels = new Set<Model<any, any>>();
export const AllModels = new Set<Model<any, any, any, any>>();

class Model<Schema extends MongoRecordZodType, IdSchema extends z.ZodTypeAny = typeof stringId> {
class Model<
Schema extends MongoRecordZodType,
Methods extends Readonly<Record<string, TypedMethod<any, any>>> = Record<string, never>,
Publications extends Readonly<Record<string, TypedPublication<any>>> = Record<string, never>,
IdSchema extends z.ZodTypeAny = typeof stringId,
> {
name: string;

schema: Schema extends z.ZodObject<
Expand All @@ -362,14 +369,26 @@ class Model<Schema extends MongoRecordZodType, IdSchema extends z.ZodTypeAny = t

indexes: ModelIndexSpecification[] = [];

constructor(name: string, schema: Schema, idSchema?: IdSchema) {
methods: Methods;

publications: Publications;

constructor(
name: string,
schema: Schema,
methods?: Methods,
publications?: Publications,
idSchema?: IdSchema,
) {
this.schema = schema instanceof z.ZodObject ?
schema.extend({ _id: idSchema ?? stringId }) :
schema.and(z.object({ _id: idSchema ?? stringId })) as any;
validateSchema(this.schema);
this.name = name;
this.relaxedSchema = relaxSchema(this.schema);
this.collection = new Mongo.Collection(name);
this.methods = methods ?? {} as Methods;
this.publications = publications ?? {} as Publications;
AllModels.add(this);
}

Expand Down Expand Up @@ -526,6 +545,6 @@ class Model<Schema extends MongoRecordZodType, IdSchema extends z.ZodTypeAny = t
}
}

export type ModelType<M extends Model<any, any>> = z.output<M['schema']>;
export type ModelType<M extends Model<any, any, any, any>> = z.output<M['schema']>;

export default Model;
16 changes: 15 additions & 1 deletion imports/lib/models/SoftDeletedModel.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -57,6 +59,8 @@ const injectOptions = <Opts extends Mongo.Options<any>>(

class SoftDeletedModel<
Schema extends MongoRecordZodType,
Methods extends Readonly<Record<string, TypedMethod<any, any>>> = Record<string, never>,
Publications extends Readonly<Record<string, TypedPublication<any>>> = Record<string, never>,
IdSchema extends z.ZodTypeAny = typeof stringId
> extends Model<
Schema extends z.ZodObject<
Expand All @@ -68,14 +72,24 @@ class SoftDeletedModel<
z.objectUtil.extendShape<Shape, { deleted: typeof deleted }>, UnknownKeys, Catchall
> :
z.ZodIntersection<Schema, z.ZodObject<{ deleted: typeof deleted }>>,
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,
);
}
Expand Down
5 changes: 0 additions & 5 deletions imports/lib/publications/chatMessagesForFirehose.ts

This file was deleted.

5 changes: 0 additions & 5 deletions imports/lib/publications/chatMessagesForPuzzle.ts

This file was deleted.

8 changes: 0 additions & 8 deletions imports/methods/sendChatMessage.ts

This file was deleted.

4 changes: 2 additions & 2 deletions imports/server/methods/sendChatMessage.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion imports/server/models/Blobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof Blobs>;

export default Blobs;
2 changes: 1 addition & 1 deletion imports/server/models/DriveActivityLatests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof DriveActivityLatests>;

export default DriveActivityLatests;
2 changes: 1 addition & 1 deletion imports/server/models/LatestDeploymentTimestamps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof LatestDeploymentTimestamps>;
export default LatestDeploymentTimestamps;
3 changes: 1 addition & 2 deletions imports/server/publications/chatMessagesForFirehose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 1 addition & 2 deletions imports/server/publications/chatMessagesForPuzzle.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion imports/server/publishJoinedQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type Projection<T> = Partial<Record<keyof T, 0 | 1>>;
// effectively building the tree in the generic parameters. I'm not entirely
// sure how to actually do that.
export type PublishSpec<T extends { _id: string }> = {
model: Mongo.Collection<T> | Model<z.ZodType<T, any, any> & MongoRecordZodType>,
model: Mongo.Collection<T> | Model<z.ZodType<T, any, any> & MongoRecordZodType, any, any>,
allowDeleted?: boolean,
projection?: Projection<T>,
foreignKeys?: {
Expand Down
3 changes: 1 addition & 2 deletions tests/acceptance/chatHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand Down