Skip to content

Commit

Permalink
feat: test support for mongoose v8
Browse files Browse the repository at this point in the history
  • Loading branch information
Dogan AY committed Jan 14, 2025
1 parent 681d026 commit e707cf6
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 49 deletions.
4 changes: 2 additions & 2 deletions packages/datasource-mongoose/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
"luxon": "^3.2.1"
},
"devDependencies": {
"mongoose": "^7.0.1"
"mongoose": "8.9.2"
},
"peerDependencies": {
"mongoose": "6.x || 7.x"
"mongoose": "6.x || 7.x || 8.x"
},
"scripts": {
"build": "tsc",
Expand Down
15 changes: 11 additions & 4 deletions packages/datasource-mongoose/src/mongoose/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Model, Schema, SchemaType } from 'mongoose';

import { Stack } from '../types';
import { recursiveDelete, recursiveSet } from '../utils/helpers';
import VersionManager from '../utils/version-manager';

export type SchemaBranch = { [key: string]: SchemaNode };
export type SchemaNode = SchemaType | SchemaBranch;
Expand Down Expand Up @@ -154,7 +155,9 @@ export default class MongooseSchema {
}

// We ended up on a field => box it.
if (child instanceof SchemaType) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (child instanceof SchemaType && child.name !== 'EmbeddedDocument') {
child = { content: child };
isLeaf = true;
}
Expand All @@ -173,7 +176,7 @@ export default class MongooseSchema {
// Exclude mixedFieldPattern $* and privateFieldPattern __
if (!name.startsWith('$*') && !name.includes('__') && (name !== '_id' || level === 0)) {
// Flatten nested schemas and arrays
if (field.constructor.name === 'SubdocumentPath') {
if (VersionManager.isSubDocument(field)) {
const subPaths = this.buildFields(field.schema as Schema, level + 1);

for (const [subName, subField] of Object.entries(subPaths)) {
Expand All @@ -185,8 +188,12 @@ export default class MongooseSchema {
for (const [subName, subField] of Object.entries(subPaths)) {
recursiveSet(paths, `${name}.[].${subName}`, subField);
}
} else if (field.constructor.name === 'SchemaArray') {
recursiveSet(paths, `${name}.[]`, (field as any).caster);
} else if (VersionManager.isSubDocumentArray(field)) {
recursiveSet(
paths,
`${name}.[]`,
VersionManager.getRawSchemaFromCaster((field as any).caster),
);
} else {
recursiveSet(paths, name, field);
}
Expand Down
30 changes: 1 addition & 29 deletions packages/datasource-mongoose/src/utils/schema/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export default class FieldsGenerator {

/** Compute column type from CleanSchema */
private static getColumnType(field: SchemaNode): ColumnType {
const columnType = this.getColumnTypeRec(field);
const columnType = VersionManager.getColumnTypeRec(field as any);

// Enum fields are promoted to enum instead of string _only_ if they are at the root of the
// record.
Expand All @@ -114,34 +114,6 @@ export default class FieldsGenerator {
return columnType;
}

/** Build ColumnType from CleanSchema recursively */
private static getColumnTypeRec(field: SchemaNode): ColumnType {
if (field instanceof SchemaType) {
if (field.instance === 'Buffer') {
return 'Binary';
}

if (['String', 'Number', 'Date', 'Boolean'].includes(field.instance)) {
return field.instance as PrimitiveTypes;
}

if ([VersionManager.ObjectIdTypeName, 'Decimal128'].includes(field.instance)) {
return 'String';
}

return 'Json';
}

if (field['[]']) {
return [this.getColumnTypeRec(field['[]'])];
}

return Object.entries(field).reduce(
(memo, [name, subSchema]) => ({ ...memo, [name]: this.getColumnTypeRec(subSchema) }),
{},
);
}

/** Get enum validator from field definition */
private static getEnumValues(field: SchemaType): string[] {
return field.options?.enum instanceof Array ? field.options.enum : field.options?.enum?.values;
Expand Down
151 changes: 147 additions & 4 deletions packages/datasource-mongoose/src/utils/version-manager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,150 @@
import mongoose from 'mongoose';
/* eslint-disable @typescript-eslint/ban-ts-comment */
import type { ColumnType, PrimitiveTypes } from '@forestadmin/datasource-toolkit';

import mongoose, { Schema, SchemaType, VirtualType } from 'mongoose';

const dynamicSchema = new mongoose.Schema({ objectId: mongoose.Schema.Types.ObjectId });

function getNativeType(type) {
switch (type) {
case String:
return 'String';
case Boolean:
return 'Boolean';
case Number:
return 'Number';
case Date:
return 'Date';
default:
return null;
}
}

export default class VersionManager {
static readonly ObjectIdTypeName: 'ObjectId' | 'ObjectID' = mongoose.version.startsWith('6')
? 'ObjectID'
: 'ObjectId';
static readonly ObjectIdTypeName = dynamicSchema.paths.objectId.instance;

public static isSubDocument(field): field is Schema.Types.Subdocument {
return (
field?.name === 'EmbeddedDocument' ||
field?.constructor?.name === 'SubdocumentPath' ||
(field?.instance === 'Embedded' && field instanceof SchemaType)
);
}

public static isSubDocumentArray(field): field is Schema.Types.DocumentArray {
return (
(field?.instance === 'Array' || field?.instance === 'DocumentArray') &&
field instanceof SchemaType
);
}

public static getRawSchemaFromCaster(definition) {
return definition.options?.type ? definition.options.type[0] : definition;
}

private static getBasicType(type: string) {
if (type === 'Buffer') {
return 'Binary';
}

if (['String', 'Number', 'Date', 'Boolean'].includes(type)) {
return type as PrimitiveTypes;
}

if ([VersionManager.ObjectIdTypeName, 'Decimal128'].includes(type)) {
return 'String';
}

return 'Json';
}

private static getColumnTypeV8(field): ColumnType {
// Virtual fields are not supported, we cannot type them
if (!field || field instanceof VirtualType || (typeof field.auto === 'boolean' && field.auto)) {
return;
}

// https://mongoosejs.com/docs/subdocs.html#subdocuments-versus-nested-paths
if (typeof field === 'function' && field.name !== 'EmbeddedDocument') {
if (field.options?.type[0]) {
return [this.getColumnTypeV8(field.options.type[0])];
}

return getNativeType(field);
}

if (field.type) {
return this.getColumnTypeV8(field.type);
}

if (field.tree) {
// This a Subtype, handle it recursively
return Object.entries(field.tree).reduce(
(acc, [name, subSchema]) => ({
...acc,
[name]: this.getColumnTypeV8(subSchema),
}),
{},
);
}

if (this.isSubDocument(field)) {
// @ts-ignore
return Object.entries(field.schema.tree).reduce(
(acc, [name, subSchema]) => ({
...acc,
[name]: VersionManager.getColumnTypeV8(subSchema),
}),
{},
);
}

if (this.isSubDocumentArray(field)) {
return [this.getColumnTypeV8(field.caster)];
}

if (field['[]']) {
return [this.getColumnTypeV8(field['[]'])];
}

if (Array.isArray(field) && field[0]) {
return [this.getColumnTypeV8(field[0])];
}

if (field instanceof SchemaType) {
return this.getBasicType(field.instance);
}

// Json defined object
return Object.entries(field).reduce(
(memo, [name, subSchema]) => ({
...memo,
[name]: this.getColumnTypeV8(subSchema as SchemaType),
}),
{},
);
}

/** Build ColumnType from CleanSchema recursively */
public static getColumnTypeRec<T = unknown>(field: SchemaType<T>): ColumnType {
if (mongoose.version.startsWith('8')) {
return this.getColumnTypeV8(field);
}

if (field instanceof SchemaType) {
return this.getBasicType(field.instance);
}

if (field['[]']) {
return [VersionManager.getColumnTypeRec(field['[]'])];
}

return Object.entries(field).reduce(
(memo, [name, subSchema]) => ({
...memo,
[name]: VersionManager.getColumnTypeRec(subSchema as SchemaType),
}),
{},
);
}
}
10 changes: 1 addition & 9 deletions packages/datasource-mongoose/test/utils/version-manager.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
describe('VersionManager', () => {
beforeEach(() => jest.resetModules());

it('should return "ObjectID" for mongoose 6', () => {
jest.mock('mongoose', () => ({ version: '6.0.0' }));
// eslint-disable-next-line @typescript-eslint/no-var-requires,global-require
const VersionManager = require('../../src/utils/version-manager').default;
expect(VersionManager.ObjectIdTypeName).toEqual('ObjectID');
});

it('should return "ObjectId" for mongoose 7', () => {
jest.mock('mongoose', () => ({ version: '7.0.0' }));
it('should dynamically return type name for current mongoose version', () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires,global-require
const VersionManager = require('../../src/utils/version-manager').default;

Expand Down
Loading

0 comments on commit e707cf6

Please sign in to comment.