0.0.102 • Published 7 months ago

typegeese v0.0.102

Weekly downloads
-
License
-
Repository
-
Last release
7 months ago

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.

0.0.102

7 months ago

0.0.101

7 months ago

0.0.100

7 months ago

0.0.95

7 months ago

0.0.96

7 months ago

0.0.97

7 months ago

0.0.98

7 months ago

0.0.99

7 months ago

0.0.94

7 months ago

0.0.93

7 months ago

0.0.92

7 months ago

0.0.91

7 months ago

0.0.90

7 months ago

0.0.89

7 months ago

0.0.88

7 months ago

0.0.87

7 months ago

0.0.86

7 months ago

0.0.85

7 months ago

0.0.84

8 months ago

0.0.83

8 months ago

0.0.82

8 months ago

0.0.81

8 months ago

0.0.80

8 months ago

0.0.79

8 months ago

0.0.78

8 months ago

0.0.77

8 months ago

0.0.76

8 months ago

0.0.75

8 months ago

0.0.74

8 months ago

0.0.73

8 months ago

0.0.72

8 months ago

0.0.71

8 months ago

0.0.70

8 months ago

0.0.69

8 months ago

0.0.68

8 months ago

0.0.67

8 months ago

0.0.66

8 months ago

0.0.65

8 months ago

0.0.64

8 months ago

0.0.63

8 months ago

0.0.62

8 months ago

0.0.61

8 months ago

0.0.60

8 months ago

0.0.59

8 months ago

0.0.58

8 months ago

0.0.57

8 months ago

0.0.56

8 months ago

0.0.55

8 months ago

0.0.54

8 months ago

0.0.53

8 months ago

0.0.52

8 months ago

0.0.51

8 months ago

0.0.50

8 months ago

0.0.49

8 months ago

0.0.48

8 months ago

0.0.47

8 months ago

0.0.46

8 months ago

0.0.45

8 months ago

0.0.44

8 months ago

0.0.43

8 months ago

0.0.42

8 months ago

0.0.41

8 months ago

0.0.40

8 months ago

0.0.39

8 months ago

0.0.38

8 months ago

0.0.37

8 months ago

0.0.36

8 months ago

0.0.35

8 months ago

0.0.34

8 months ago

0.0.33

8 months ago

0.0.32

8 months ago

0.0.31

8 months ago

0.0.30

8 months ago

0.0.29

8 months ago

0.0.28

8 months ago

0.0.27

8 months ago

0.0.26

8 months ago

0.0.24

8 months ago

0.0.23

8 months ago

0.0.22

8 months ago

0.0.21

8 months ago

0.0.20

8 months ago

0.0.19

8 months ago

0.0.18

8 months ago

0.0.17

8 months ago

0.0.16

8 months ago

0.0.15

8 months ago

0.0.14

8 months ago

0.0.13

8 months ago

0.0.12

8 months ago

0.0.11

8 months ago

0.0.10

8 months ago

0.0.9

8 months ago

0.0.8

8 months ago

0.0.7

8 months ago

0.0.6

8 months ago

0.0.5

8 months ago

0.0.4

8 months ago

0.0.3

8 months ago

0.0.2

8 months ago

0.0.1

8 months ago

0.0.0

8 months ago