quave:collections

v3.1.1Published 2 weeks ago

quave:collections

quave:collections is a Meteor package that allows you to create your collections in a standard way.

Features

  • Schemas
  • Helpers
  • Composers

Compatible with Meteor 3.0.3+

Why

Every application that connects to databases usually need the following features:

  • A way to access object instances when they come from the database: helpers
  • Provide new methods to collections: collection
  • Valid the data before persisting: schemas
  • Centralize behaviors: composers

Meteor has packages for almost all these use cases but it's not easy to find the best in each case and also how to use them together, that is why we have created this package.

We offer here a standard way for you to create your collections by configuring all these features in a function call createCollection using a bunch of options in a declarative way and without using Javascript classes.

We also allow you to extend your Meteor.users collection in the same way as any other collection.

We believe we are not reinventing the wheel in this package but what we are doing is like putting together the wheels in the vehicle :).

Installation

meteor add quave:collections
meteor npm install simpl-schema

Usage

Methods

Example applying collection property:

1import { createCollection } from 'meteor/quave:collections';
2
3export const AddressesCollection = createCollection({
4  name: 'addresses',
5  collection: {
6    save(addressParam) {
7      const address = { ...addressParam };
8
9      if (address._id) {
10        this.update(address._id, { $set: { ...address } });
11        return address._id;
12      }
13      delete address._id;
14      return this.insert({ ...address });
15    },
16  },
17});

Schema

Example applying SimpleSchema:

1import { createCollection } from 'meteor/quave:collections';
2
3import SimpleSchema from 'simpl-schema';
4
5const PlayerSchema = new SimpleSchema({
6  name: {
7    type: String,
8  },
9  age: {
10    type: SimpleSchema.Integer,
11  },
12});
13
14export const PlayersCollection = createCollection({
15  name: 'players',
16  schema: PlayerSchema,
17});

Composers

Built-in composers

persistable

The persistable composer adds a save method to your collection, which handles both inserting new documents and updating existing ones. It also automatically manages createdAt and updatedAt fields.

To use the persistable composer:

1import { createCollection, persistable } from 'meteor/quave:collections';
2
3export const UsersCollection = createCollection({
4  name: 'users',
5  composers: [persistable()],
6});

The save method can be used as follows:

1// Insert a new document
2const newUser = await UsersCollection.save({
3  name: 'John Doe',
4  email: 'john@example.com',
5});
6
7// Update an existing document
8const updatedUser = await UsersCollection.save({
9  _id: newUser._id,
10  name: 'John Updated',
11});
12
13// Save with custom selector to find existing document
14const user = await UsersCollection.save(
15  { email: 'john@example.com', name: 'John Doe' },
16  { selectorToFindId: { email: 'john@example.com' } }
17);
18
19// Save without returning the document
20await UsersCollection.save({ name: 'Alice' }, { skipReturn: true });
21
22// Save and return only specific fields
23const savedUser = await UsersCollection.save(
24  { name: 'Bob' },
25  { projection: { name: 1 } }
26);

You can customize the persistable composer by providing beforeInsert, beforeUpdate, afterInsert, and afterUpdate functions, along with the shouldFetchFullDoc option:

1const customPersistable = persistable({
2  shouldFetchFullDoc: true, // When true, fetches the full document for updates
3  beforeInsert: ({ doc }) => {
4    // Modify the document before insertion
5    return { ...doc, customField: 'value' };
6  },
7  beforeUpdate: ({ doc }) => {
8    // Modify the document before update
9    return { ...doc, lastModified: new Date() };
10  },
11  afterInsert: ({ doc }) => {
12    // Any action after the document has been inserted
13  },
14  afterUpdate: ({ doc, oldDoc }) => {
15    // Any action after the document has been updated
16    // When shouldFetchFullDoc is true, oldDoc contains the full previous document
17  },
18});
19
20export const CustomUsersCollection = createCollection({
21  name: 'customUsers',
22  composers: [customPersistable],
23});
softRemoval

The softRemoval composer adds soft deletion functionality to your collection. Instead of permanently deleting documents, it marks them as removed and filters them out of normal queries.

To use the softRemoval composer:

1import { createCollection, softRemoval } from 'meteor/quave:collections';
2
3export const UsersCollection = createCollection({
4  name: 'users',
5  composers: [softRemoval({
6    shouldFetchFullDoc: false, // Optional, defaults to false
7  })],
8});

Basic usage example:

1// Example of soft removal
2const user = await UsersCollection.insertAsync({ name: 'John Doe' });
3await UsersCollection.removeAsync(user._id);
4
5// The user is not actually removed, but marked as removed (using option includeSoftRemoved)
6const removedUser = await UsersCollection.findOneAsync(
7  { _id: user._id },
8  { includeSoftRemoved: true }
9);
10console.log(removedUser); // { _id: ..., name: 'John Doe', isRemoved: true }
11
12// The user seems to be removed in normal queries
13const removedUser2 = await UsersCollection.findOneAsync({ _id: user._id });
14console.log(removedUser2); // null

You can customize the softRemoval composer by providing the afterRemove function and shouldFetchFullDoc option:

1const customSoftRemoval = softRemoval({
2  shouldFetchFullDoc: true, // When true, fetches full documents before removal
3  afterRemove: ({ docs, collection, isRemove, isHardRemove }) => {
4    // docs will contain the full documents when shouldFetchFullDoc is true
5    // isHardRemove indicates if it was a permanent removal
6    // Any action after the documents have been removed
7  },
8});
9
10export const CustomUsersCollection = createCollection({
11  name: 'customUsers',
12  composers: [customSoftRemoval],
13});
14
15// For hard removal:
16await CustomUsersCollection.removeAsync(user._id, { hardRemove: true });

Create your own composer

Example creating a way to paginate the fetch of data using composers

1import { createCollection } from 'meteor/quave:collections';
2
3const LIMIT = 7;
4export const paginable = (collection) =>
5  Object.assign({}, collection, {
6    getPaginated({ selector, options = {}, paginationAction }) {
7      const { skip, limit } = paginationAction || { skip: 0, limit: LIMIT };
8      const items = this.find(selector, {
9        ...options,
10        skip,
11        limit,
12      }).fetch();
13      const total = this.find(selector).count();
14      const nextSkip = skip + limit;
15      const previousSkip = skip - limit;
16
17      return {
18        items,
19        pagination: {
20          total,
21          totalPages: parseInt(total / limit, 10) + (total % limit > 0 ? 1 : 0),
22          currentPage:
23            parseInt(skip / limit, 10) + (skip % limit > 0 ? 1 : 0) + 1,
24          ...(nextSkip < total ? { next: { skip: nextSkip, limit } } : {}),
25          ...(previousSkip >= 0
26            ? { previous: { skip: previousSkip, limit } }
27            : {}),
28        },
29      };
30    },
31  });
32
33export const StoresCollection = createCollection({
34  name: 'stores',
35  composers: [paginable],
36});
37
38// This probably will come from the client, using Methods, REST, or GraphQL
39// const paginationAction = {skip: XXX, limit: YYY};
40
41const { items, pagination } = StoresCollection.getPaginated({
42  selector: {
43    ...(search ? { name: { $regex: search, $options: 'i' } } : {}),
44  },
45  options: { sort: { updatedAt: -1 } },
46  paginationAction,
47});

Options

Second argument for the default collections constructor. Example defining a transform function.

1const transform = (doc) => ({
2  ...doc,
3  get user() {
4    return Meteor.users.findOne(this.userId);
5  },
6});
7
8export const PlayersCollection = createCollection({
9  name: 'players',
10  schema,
11  options: {
12    transform,
13  },
14});

Meteor.users

Extending Meteor.users, also using collection, helpers, composers, apply.

You can use all these options also with name instead of instance.

1import { createCollection } from 'meteor/quave:collections';
2
3export const UsersCollection = createCollection({
4  instance: Meteor.users,
5  schema: UserTypeDef,
6  collection: {
7    isAdmin(userId) {
8      const user = userId && this.findOne(userId, { fields: { profiles: 1 } });
9      return (
10        user && user.profiles && user.profiles.includes(UserProfile.ADMIN.name)
11      );
12    },
13  },
14  helpers: {
15    toPaymentGatewayJson() {
16      return {
17        country: 'us',
18        external_id: this._id,
19        name: this.name,
20        type: 'individual',
21        email: this.email,
22      };
23    },
24  },
25  composers: [paginable],
26  apply(coll) {
27    coll.after.insert(userAfterInsert(coll), { fetchPrevious: false });
28    coll.after.update(userAfterUpdate);
29  },
30});

Publishing

Bump the version following semver in package.js and run meteor npm run generate-dts to generate the types.

Then publish the package:

meteor publish

License

MIT