npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

typegeese

v0.0.102

Published

<p align="center"> <img src="./assets/mascot.png" width="200px" align="center" alt="typegeese mascot" /> <h1 align="center">Typegeese</h1> <p align="center"> Painless database migrations via <strong>migration-defined schemas</strong> </p> </p>

Downloads

44

Readme

Typegeese is a type-safe ORM for MongoDB which introduces the concept of migration-defined schemas.

With Typegeese, your schema migrations become the source of truth for the structure of your data.

This makes it possible for Typegeese to automatically apply schema migrations on-demand without the need for migration generation scripts or complex data migration processes.

Note: Typegeese is currently experimental; expect bugs, breaking changes, and incomplete documentation 😅

Migration-defined schemas

Typegeese schemas are defined in terms of migrations, each of which creates a new versioned schema.

These migrations are defined using TypeScript classes powered by the amazing typegoose library (which is where the name typegeese is inspired from).

The first version (v0) of a schema extends from Schema('Name'):

// ./user/v0.ts
import { Schema, prop } from "typegeese";

export default class User extends Schema('User')<typeof User> {
  static _v = 0;

  @prop({ type: String, required: true })
  email!: string;

  @prop({ type: String, required: false })
  name!: string | null;
}
// ./user/$schema.ts
export { default as User } from './v0.js';
const user: User = await select(UserModel.findOne(...), { ... });
//          ^ The generic type argument in the schema class definition
//            allows typegeese to infer the correct `User` schema type
//            from this call

Typegeese also uses this generic type argument to verify that the mandatory "_v" property is present on the class.

When you want to add a new property, you extend the previous version of your schema by passing it to typegeese's Schema function:

// ./user/v1-add-profile-image.ts
import { type Migrate, Schema, prop } from "typegeese";

import UserV0 from './v0.js';

export default class User extends Schema(UserV0)<typeof User> {
  static _v = 'v1-profile-image';

  @prop({ type: String, required: false })
  profileImageUrl!: string | null;

  static _migration: Migrate = (migrate: Migrate<UserV0, User>) =>
    migrate({ profileImageUrl: null });
}
// ./user/$schema.ts
export * from './v1-add-profile-image.js';

The static _migration property can handle arbitrarily complex migrations:

// ./user/v2-add-username.ts
import {
  getModelForSchema,
  type Migrate,
  prop,
  Schema,
  select
} from 'typegeese';

import UserV1 from './v1-add-profile-image.js';

export default class User extends Schema(UserV1)<typeof User> {
  static _v = 'v2-add-username';

  @prop({ type: String, required: true })
  username!: string;

  static _migration: Migrate = async (migrate: Migrate<UserV1, User>) => {
    const { _id, mongoose } = migrate;
    const UserV1Model = getModelForSchema(UserV1, { mongoose });
    const user = await select(
      UserV1Model.findById(_id),
      { email: true }
    );

    if (user === null) return null;

    return migrate({
      username: user.email.split('@')[0]
    })
  }
}
// ./user/$schema.ts
export { default as User } from './v2-add-username.js';

If you want to be able to view all your schema's properties in one place, you can install and use @typegeese/shape, which comes with a t helper that leverages TypeScript inference to define a type containing your schema's properties:

// ./user/$schema.ts
export { default as User } from './v2-add-username.js';

import type { t } from '@typegeese/shape';
import type * as $ from '../$schemas.js';

// This type is type-checked by TypeScript to ensure
// that it always stays up to date with every new migration
export type $User = t.Shape<
  $.User,
  {
    _id: string;
    name: string | null;
    email: string;
    profileImageUrl: string | null;
    username: string;
  }
>;

The t helper can also be used to define the shape of your schema at runtime:

// ./user/$shape.ts
import { t } from '@typegeese/shape'

import type { $User } from './$schema.js';

// Typegeese's `t` helper also lets you declare the shape of
// your schema at runtime which can be imported from the
// client side (it's recommended to use a separate file for
// the runtime definition so your bundler doesn't end up
// importing server-side code)
export const User = t.Shape<$User>({
  _id: t,
  name: t,
  email: t,
  profileImageUrl: t,
  username: t
});

Examples

The examples use the following UserV0 schema:

// ./user/v0.ts
import { Schema, prop } from 'typegeese';

export default class User extends Schema('User')<typeof User> {
  static _v = 0;

  @prop({ type: String, required: true })
  email!: string;

  @prop({ type: String, required: false })
  name!: string | null;
}

Adding a new field

// ./user/v1-add-username.ts
import {
  type Migrate,
  Schema,
  prop,
  getModelForSchema,
  select,
  type Migrate
} from 'typegeese';

import UserV0 from './v0.js';

export default class User extends Schema(UserV0)<typeof User> {
  static _v = 'v1-add-username';

  @prop({ type: String, required: true })
  username!: string;

  static _migration: Migrate = (migrate: Migrate<UserV0, User>) => {
    const { _id, mongoose } = migrate;
    const UserV0Model = getModelForSchema(UserV0, { mongoose });
    const user = await select(
      UserV0Model.findById(_id),
      { email: true }
    );

    if (user === null) return null;

    return migrate({
      username: user.email.split('@')[0]
    })
  }
}

Removing a field

// ./user/v1-remove-name.ts
import { type Migrate, Schema, prop } from 'typegeese';

import UserV0 from './v0.js';

export default class User extends Schema(
  UserV0
  { omit: { name: true } }
)<typeof User> {
  static _v = 'v1-remove-name';

  static _migration: Migrate = (migrate: Migrate<UserV0, User>) => migrate({})
}

Renaming a field

// ./user/v1-rename-name-to-full-name.ts
import {
  type Migrate,
  Schema,
  prop,
  getModelForSchema,
  select
} from 'typegeese';

import UserV0 from './v0.js';

export default class User extends Schema(
  UserV0,
  { omit: { name: true } }
)<typeof User> {
  static _v = 'v1-rename-name-to-full-name';

  @prop({ type: String, required: false })
  fullName!: string | null;

  static _migration: Migrate = (migrate: Migrate<User, UserV0>) => {
    const { _id, mongoose } = migrate;
    const UserV0Model = getModelForSchema(UserV0, { mongoose });
    const user = await select(
      UserV0Model.findById(_id),
      { name: true }
    );

    if (user === null) return null;

    return migrate({
      fullName: user.name
    })
  }
}

Renaming a schema

In order to preserve compatibility with a blue/green deployment strategy, typegeese handles schema renames by running queries on both the old collection and the new renamed collection, and then lazily copying over documents into the new collection as they are queried from the renamed model.

// ./_user/v1-rename-to-account.ts
// ^ We rename the folder to use an underscore prefix to indicate that it was renamed

import {
  type Migrate,
  Schema,
  prop,
  getModelForSchema,
  select
} from 'typegeese';
import UserV0 from './v0.js';

export default class User extends Schema(UserV0)<typeof User> {
  static _v = 'v1-rename-to-account';

  static _migration: Migrate = (migrate: Migrate<UserV0, User>) => migrate({})
}
// ./account/v0.ts

import { User } from '../_user/$schema.js';

export class Account extends Schema('Account', { from: User })<typeof Account> {
  static _v = 0;
}

Implementation

The Schema(...) function is used purely for type inference and returns the Object constructor at runtime:

class User extends Schema('User')<typeof User> { /* ... */ }
class Post extends Schema(PostV0)<typeof Post> { /* ... */ }

// Equivalent at runtime to:
class User extends Object {}
class Post extends Object {}

In practice, extends Object is equivalent to omitting the extends clause.

By returning the Object constructor in the extends clause, we avoid using inheritance for migrations. This reduces the chance of conflicts with typegoose's intended uses of inheritance (e.g. for discriminator types).

Instead, typegeese dynamically constructs schemas at runtime when the functions getModelForSchema or loadModelSchemas are called.

Limitations

Currently, typegeese expects that there exists only one reference to its internal functions (since it uses Reflect#getMetadata and Reflect#defineMetadata). This means that you must mark typegeese as external when using a bundler like Webpack.