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
and beforeUpdate
functions:
1const customPersistable = persistable({ 2 beforeInsert: ({ doc }) => { 3 // Modify the document before insertion 4 return { ...doc, customField: 'value' }; 5 }, 6 beforeUpdate: ({ doc }) => { 7 // Modify the document before update 8 return { ...doc, lastModified: new Date() }; 9 }, 10}); 11 12export const CustomUsersCollection = createCollection({ 13 name: 'customUsers', 14 composers: [customPersistable], 15});
You can also customize the persistable
composer by providing the afterInsert
and afterUpdate
functions, which apply an action after the document has been inserted or updated.
1const customPersistable = persistable({ 2 afterInsert: ({ doc }) => { 3 // Any action after the document has been inserted 4 }, 5 afterUpdate: ({ doc }) => { 6 // Any action after the document has been updated 7 }, 8}); 9 10export const CustomUsersCollection = createCollection({ 11 name: 'customUsers', 12 composers: [customPersistable], 13});
Also, you can use the shouldFetchFullDoc
flag to decide whether the old document should be fetched from the database and used in the afterUpdate
function.
1const savedUser = await UsersCollection.save( 2 { _id: user._id, name: 'Bob' }, 3 { shouldFetchFullDoc: true } 4);
The persistable
composer provides a convenient way to handle document persistence with automatic timestamp management and customizable pre-save hooks.
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}); 7 8// Example of soft removal 9const user = await UsersCollection.insertAsync({ name: 'John Doe' }); 10await UsersCollection.removeAsync(user._id); 11 12// The user is not actually removed, but marked as removed (using option includeSoftRemoved) 13const removedUser = await UsersCollection.findOneAsync( 14 { _id: user._id }, 15 { includeSoftRemoved: true } 16); 17console.log(removedUser); // { _id: ..., name: 'John Doe', isRemoved: true } 18 19// The user seems to be removed 20const removedUser2 = await UsersCollection.findOneAsync({ _id: user._id }); 21console.log(removedUser2); // null
You can also customize the softRemoval
composer by providing the afterRemove
function. This function will be called after the documents has been removed, and you can provide the documents to this function by adding the flag shouldFetchFullDoc
.
1const customSoftRemoval = softRemoval({ 2 afterRemove: ({ docs }) => { 3 // Any action after the documents have been removed 4 }, 5});
1await UsersCollection.removeAsync(user._id, { shouldFetchFullDoc: 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