From 2631c1238dc2cdd86ef921aed40f33d7af7fd941 Mon Sep 17 00:00:00 2001 From: stasik1404 Date: Thu, 20 Nov 2025 16:51:02 +0300 Subject: [PATCH 1/6] feat: add ommiting private fields feature --- docs/packages/node-mongo/overview.mdx | 15 +- packages/node-mongo/README.md | 6 +- packages/node-mongo/src/service.ts | 91 +++++++---- packages/node-mongo/src/tests/service.spec.ts | 146 ++++++++++++++++++ packages/node-mongo/src/types/index.ts | 24 ++- packages/node-mongo/src/utils/helpers.ts | 32 +++- 6 files changed, 262 insertions(+), 52 deletions(-) diff --git a/docs/packages/node-mongo/overview.mdx b/docs/packages/node-mongo/overview.mdx index 933a817f2..801135576 100644 --- a/docs/packages/node-mongo/overview.mdx +++ b/docs/packages/node-mongo/overview.mdx @@ -214,6 +214,7 @@ interface ServiceOptions { |`escapeRegExp`|Escape `$regex` values to prevent special characters from being interpreted as patterns. |`false`| |`collectionOptions`|MongoDB [CollectionOptions](https://mongodb.github.io/node-mongodb-native/4.10/interfaces/CollectionOptions.html)|`{}`| |`collectionCreateOptions`|MongoDB [CreateCollectionOptions](https://mongodb.github.io/node-mongodb-native/4.10/interfaces/CreateCollectionOptions.html)|`{}`| +|`privateFields`|Fields that should be omitted from the public response.|`[]`| ### `CreateConfig` Overrides `ServiceOptions` parameters for create operations. @@ -222,6 +223,7 @@ Overrides `ServiceOptions` parameters for create operations. type CreateConfig = { validateSchema?: boolean, publishEvents?: boolean, + mode?: 'public' | 'private', }; ``` @@ -231,6 +233,8 @@ Overrides `ServiceOptions` parameters for read operations. ```typescript type ReadConfig = { skipDeletedOnDocs?: boolean, + populate?: PopulateOptions | PopulateOptions[], + mode?: 'public' | 'private', }; ``` @@ -242,6 +246,7 @@ type UpdateConfig = { skipDeletedOnDocs?: boolean, validateSchema?: boolean, publishEvents?: boolean, + mode?: 'public' | 'private', }; ``` @@ -281,19 +286,11 @@ Extending API for a single service. ```typescript const service = db.createService('users', { schemaValidator: (obj) => schema.parseAsync(obj), + privateFields: ['passwordHash', 'signupToken', 'resetPasswordToken'], }); -const privateFields = [ - 'passwordHash', - 'signupToken', - 'resetPasswordToken', -]; - -const getPublic = (user: User | null) => _.omit(user, privateFields); - export default Object.assign(service, { updateLastRequest, - getPublic, }); ``` diff --git a/packages/node-mongo/README.md b/packages/node-mongo/README.md index a38004c44..8f815eca0 100644 --- a/packages/node-mongo/README.md +++ b/packages/node-mongo/README.md @@ -1046,15 +1046,11 @@ Extending API for a single service. ```typescript const service = db.createService("users", { schemaValidator: (obj) => schema.parseAsync(obj), + privateFields: ['passwordHash', 'signupToken', 'resetPasswordToken'], }); -const privateFields = ["passwordHash", "signupToken", "resetPasswordToken"]; - -const getPublic = (user: User | null) => _.omit(user, privateFields); - export default Object.assign(service, { updateLastRequest, - getPublic, }); ``` diff --git a/packages/node-mongo/src/service.ts b/packages/node-mongo/src/service.ts index f4bb06ad6..4a2916472 100755 --- a/packages/node-mongo/src/service.ts +++ b/packages/node-mongo/src/service.ts @@ -33,7 +33,7 @@ import { } from './types'; import logger from './utils/logger'; -import { addUpdatedOnField, generateId } from './utils/helpers'; +import { addUpdatedOnField, generateId, getPrivateFindOptions, omitPrivateFields } from './utils/helpers'; import PopulateUtil from './utils/populate'; import { inMemoryPublisher } from './events/in-memory'; @@ -45,6 +45,7 @@ const defaultOptions: ServiceOptions = { addCreatedOnField: true, addUpdatedOnField: true, escapeRegExp: false, + privateFields: [], }; const isDev = process.env.NODE_ENV === 'development'; @@ -239,17 +240,21 @@ class Service { readConfig: ReadConfig = {}, findOptions: FindOptions = {}, ): Promise<(U & PopulateTypes) | U | null> { + const { mode = 'private', populate } = readConfig; + const collection = await this.getCollection(); filter = this.handleReadOperations(filter, readConfig); - if (readConfig.populate) { - const docs = await this.populateAggregate(collection, filter, readConfig, findOptions); + const privateFindOptions = getPrivateFindOptions({ mode, findOptions, privateFields: this.options.privateFields }); + + if (populate) { + const docs = await this.populateAggregate(collection, filter, readConfig, privateFindOptions); return docs[0] || null; } - return collection.findOne(filter, findOptions); + return collection.findOne(filter, privateFindOptions); } // Method overloading for find @@ -270,16 +275,19 @@ class Service { readConfig: ReadConfig & { page?: number; perPage?: number } = {}, findOptions: FindOptions = {}, ): Promise | FindResult> { + const { mode = 'private', populate, page, perPage } = readConfig; + const collection = await this.getCollection(); - const { page, perPage } = readConfig; const hasPaging = !!page && !!perPage; filter = this.handleReadOperations(filter, readConfig); + const privateFindOptions = getPrivateFindOptions({ mode, findOptions, privateFields: this.options.privateFields }); + if (!hasPaging) { - const results = readConfig.populate - ? await this.populateAggregate(collection, filter, readConfig, findOptions) - : await collection.find(filter, findOptions).toArray(); + const results = populate + ? await this.populateAggregate(collection, filter, readConfig, privateFindOptions) + : await collection.find(filter, privateFindOptions).toArray(); return { pagesCount: 1, @@ -288,13 +296,13 @@ class Service { }; } - findOptions.skip = (page - 1) * perPage; - findOptions.limit = perPage; + privateFindOptions.skip = (page - 1) * perPage; + privateFindOptions.limit = perPage; const [results, count] = await Promise.all([ - readConfig.populate - ? this.populateAggregate(collection, filter, readConfig, findOptions) - : collection.find(filter, findOptions).toArray(), + populate + ? this.populateAggregate(collection, filter, readConfig, privateFindOptions) + : collection.find(filter, privateFindOptions).toArray(), collection.countDocuments(filter), ]); @@ -347,12 +355,13 @@ class Service { createConfig: CreateConfig = {}, insertOneOptions: InsertOneOptions = {}, ): Promise => { + const { mode = 'private', publishEvents } = createConfig; const collection = await this.getCollection(); const validEntity = await this.validateCreateOperation(object, createConfig); - const shouldPublishEvents = typeof createConfig.publishEvents === 'boolean' - ? createConfig.publishEvents + const shouldPublishEvents = typeof publishEvents === 'boolean' + ? publishEvents : this.options.publishEvents; if (shouldPublishEvents) { @@ -376,22 +385,28 @@ class Service { await collection.insertOne(validEntity as OptionalUnlessRequiredId, insertOneOptions); } - return validEntity; + if (mode === 'public') { + return validEntity; + } + + return omitPrivateFields(validEntity, this.options.privateFields); }; insertMany = async ( objects: Partial[], createConfig: CreateConfig = {}, bulkWriteOptions: BulkWriteOptions = {}, - ): Promise => { + ): Promise> => { + const { mode = 'private', publishEvents } = createConfig; + const collection = await this.getCollection(); const validEntities = await Promise.all(objects.map( (o) => this.validateCreateOperation(o, createConfig), )); - const shouldPublishEvents = typeof createConfig.publishEvents === 'boolean' - ? createConfig.publishEvents + const shouldPublishEvents = typeof publishEvents === 'boolean' + ? publishEvents : this.options.publishEvents; if (shouldPublishEvents) { @@ -415,7 +430,11 @@ class Service { await collection.insertMany(validEntities as OptionalUnlessRequiredId[], bulkWriteOptions); } - return validEntities; + if (mode === 'public') { + return validEntities; + } + + return validEntities.map((doc) => omitPrivateFields(doc, this.options.privateFields)); }; replaceOne = async ( @@ -455,6 +474,8 @@ class Service { updateConfig: UpdateConfig = {}, updateOptions: UpdateOptions = {}, ): Promise { + const { mode = 'private', validateSchema, publishEvents } = updateConfig; + const collection = await this.getCollection(); filter = this.handleReadOperations(filter, updateConfig); @@ -515,16 +536,16 @@ class Service { updateFilter = _.merge(updateFilter, { $set: { updatedOn: updatedOnDate } }); } - const shouldValidateSchema = typeof updateConfig.validateSchema === 'boolean' - ? updateConfig.validateSchema + const shouldValidateSchema = typeof validateSchema === 'boolean' + ? validateSchema : Boolean(this.options.schemaValidator); if (shouldValidateSchema) { await this.validateSchema(newDoc); } - const shouldPublishEvents = typeof updateConfig.publishEvents === 'boolean' - ? updateConfig.publishEvents + const shouldPublishEvents = typeof publishEvents === 'boolean' + ? publishEvents : this.options.publishEvents; if (shouldPublishEvents) { @@ -556,7 +577,11 @@ class Service { ); } - return newDoc; + if (mode === 'public') { + return newDoc; + } + + return omitPrivateFields(newDoc, this.options.privateFields); } updateMany( @@ -579,6 +604,8 @@ class Service { updateConfig: UpdateConfig = {}, updateOptions: UpdateOptions = {}, ): Promise { + const { mode = 'private', validateSchema, publishEvents } = updateConfig; + const collection = await this.getCollection(); filter = this.handleReadOperations(filter, updateConfig); @@ -654,8 +681,8 @@ class Service { }); } - const shouldValidateSchema = typeof updateConfig.validateSchema === 'boolean' - ? updateConfig.validateSchema + const shouldValidateSchema = typeof validateSchema === 'boolean' + ? validateSchema : Boolean(this.options.schemaValidator); if (shouldValidateSchema) { @@ -676,8 +703,8 @@ class Service { }, ); - const shouldPublishEvents = typeof updateConfig.publishEvents === 'boolean' - ? updateConfig.publishEvents + const shouldPublishEvents = typeof publishEvents === 'boolean' + ? publishEvents : this.options.publishEvents; if (shouldPublishEvents) { @@ -701,7 +728,11 @@ class Service { await collection.bulkWrite(bulkWriteQuery, updateOptions); } - return updated.map((u) => u?.doc) as U[]; + if (mode === 'public') { + return updated.map((u) => u?.doc) as U[]; + } + + return updated.map((u) => omitPrivateFields(u?.doc as U, this.options.privateFields)) as U[]; } deleteOne = async ( diff --git a/packages/node-mongo/src/tests/service.spec.ts b/packages/node-mongo/src/tests/service.spec.ts index b09d03280..8976fca66 100644 --- a/packages/node-mongo/src/tests/service.spec.ts +++ b/packages/node-mongo/src/tests/service.spec.ts @@ -43,6 +43,7 @@ const userSchema = z.object({ deletedOn: z.date().optional().nullable(), fullName: z.string(), age: z.number().optional(), + passwordHash: z.string().optional(), role: z.nativeEnum(UserRoles).default(UserRoles.MEMBER), permissions: z.array(z.nativeEnum(AdminPermissions)).optional(), birthDate: z.date().optional(), @@ -66,6 +67,12 @@ const companyService = database.createService('companies', { schemaValidator: (obj) => companySchema.parseAsync(obj), }); +const usersServiceWithPrivateFields = database.createService('usersWithPrivateFields', { + schemaValidator: (obj) => userSchema.parseAsync(obj), + privateFields: ['passwordHash'], +}); + + describe('service.ts', () => { before(async () => { await database.connect(); @@ -1491,4 +1498,143 @@ describe('service.ts', () => { updatedUser?.permissions?.[1]?.should.be.undefined; updatedUser?.permissions?.length?.should.be.equal(0); }); + + it('should omit private fields after insertOne method', async () => { + const newUserData = { + fullName: 'Test User with private fields', + age: 30, + role: UserRoles.ADMIN, + passwordHash: '123456', + }; + + const user = await usersServiceWithPrivateFields.insertOne(newUserData); + + (user?.passwordHash === undefined).should.be.equal(true); + user?.role?.should.be.equal(newUserData.role); + user?.fullName?.should.be.equal(newUserData.fullName); + user?.age?.should.be.equal(newUserData.age); + }); + + it('should omit private fields after insertMany method', async () => { + const newUsersData = [ + { + fullName: 'Test User with private fields', + age: 30, + role: UserRoles.ADMIN, + passwordHash: '123456', + }, + { + fullName: 'Test User with private fields 2', + age: 20, + role: UserRoles.MANAGER, + passwordHash: '789012', + }, + ]; + + const newUsers = await usersServiceWithPrivateFields.insertMany(newUsersData); + + (newUsers[0]?.passwordHash === undefined).should.be.equal(true); + newUsers[0]?.role?.should.be.equal(newUsersData[0].role); + newUsers[0]?.fullName?.should.be.equal(newUsersData[0].fullName); + newUsers[0]?.age?.should.be.equal(newUsersData[0].age); + + (newUsers[1]?.passwordHash === undefined).should.be.equal(true); + newUsers[1]?.role?.should.be.equal(newUsersData[1].role); + newUsers[1]?.fullName?.should.be.equal(newUsersData[1].fullName); + newUsers[1]?.age?.should.be.equal(newUsersData[1].age); + }); + + it('should omit private fields after updateOne method', async () => { + const newUsersData = { + fullName: 'Test User with private fields', + age: 30, + role: UserRoles.ADMIN, + passwordHash: '123456', + }; + + const updateUserData = { + fullName: 'Test User with private fields 2', + age: 20, + role: UserRoles.MANAGER, + passwordHash: '789012', + }; + + const newUser = await usersServiceWithPrivateFields.insertOne(newUsersData); + const updatedUser = await usersServiceWithPrivateFields.updateOne({ _id: newUser._id }, () => updateUserData); + + (updatedUser?.passwordHash === undefined).should.be.equal(true); + updatedUser?.role?.should.be.equal(updateUserData.role); + updatedUser?.fullName?.should.be.equal(updateUserData.fullName); + updatedUser?.age?.should.be.equal(updateUserData.age); + }); + + it('should omit private fields after updateMany method', async () => { + const newUsersData = [{ + fullName: 'Test User with private fields', + age: 30, + role: UserRoles.ADMIN, + passwordHash: '123456', + }, + { + fullName: 'Test User with private fields 2', + age: 20, + role: UserRoles.MANAGER, + passwordHash: '789012', + }]; + + const updateUserData = { + fullName: 'Test User with private fields 2', + age: 20, + role: UserRoles.MANAGER, + passwordHash: '789012', + }; + + const newUser = await usersServiceWithPrivateFields.insertMany(newUsersData); + const newUsersIds = newUser.map((u) => u?._id).filter(Boolean) as string[]; + const updatedUsers = await usersServiceWithPrivateFields.updateMany({ _id: { $in: newUsersIds } }, () => updateUserData); + + (updatedUsers[0]?.passwordHash === undefined).should.be.equal(true); + updatedUsers[0]?.role?.should.be.equal(updateUserData.role); + updatedUsers[0]?.fullName?.should.be.equal(updateUserData.fullName); + updatedUsers[0]?.age?.should.be.equal(updateUserData.age); + + (updatedUsers[1]?.passwordHash === undefined).should.be.equal(true); + updatedUsers[1]?.role?.should.be.equal(updateUserData.role); + updatedUsers[1]?.fullName?.should.be.equal(updateUserData.fullName); + updatedUsers[1]?.age?.should.be.equal(updateUserData.age); + }); + + it('should omit private fields after find method', async () => { + const newUserData = { + fullName: 'Test User with private fields', + age: 30, + role: UserRoles.ADMIN, + passwordHash: '123456', + }; + + const user = await usersServiceWithPrivateFields.insertOne(newUserData); + const { results: foundUsers } = await usersServiceWithPrivateFields.find({ _id: user._id }); + + (foundUsers[0]?.passwordHash === undefined).should.be.equal(true); + foundUsers[0]?.role?.should.be.equal(newUserData.role); + foundUsers[0]?.fullName?.should.be.equal(newUserData.fullName); + foundUsers[0]?.age?.should.be.equal(newUserData.age); + }); + + it('should omit private fields after findOne method', async () => { + const newUserData = { + fullName: 'Test User with private fields', + age: 30, + role: UserRoles.ADMIN, + passwordHash: '123456', + }; + + const user = await usersServiceWithPrivateFields.insertOne(newUserData); + const foundUser = await usersServiceWithPrivateFields.findOne({ _id: user._id }); + + (foundUser?.passwordHash === undefined).should.be.equal(true); + foundUser?.role?.should.be.equal(newUserData.role); + foundUser?.fullName?.should.be.equal(newUserData.fullName); + foundUser?.age?.should.be.equal(newUserData.age); + }); }); diff --git a/packages/node-mongo/src/types/index.ts b/packages/node-mongo/src/types/index.ts index b81b026cf..f04d5ea79 100644 --- a/packages/node-mongo/src/types/index.ts +++ b/packages/node-mongo/src/types/index.ts @@ -1,5 +1,5 @@ import { - ClientSession, Collection, CollectionOptions, CreateCollectionOptions, Document, MongoClient, + ClientSession, Collection, CollectionOptions, CreateCollectionOptions, Document, FindOptions, MongoClient, } from 'mongodb'; export type DbChangeType = 'create' | 'update' | 'delete'; @@ -61,8 +61,9 @@ export type FindResult = { }; export type CreateConfig = { - validateSchema?: boolean, - publishEvents?: boolean, + validateSchema?: boolean; + publishEvents?: boolean; + mode?: 'public' | 'private'; }; export type PopulateOptions = { @@ -79,11 +80,12 @@ export type PopulateOptions = { export type ReadConfig = { skipDeletedOnDocs?: boolean, populate?: PopulateOptions | PopulateOptions[]; + mode?: 'public' | 'private'; }; // Type-safe discriminated unions for populate operations export type ReadConfigWithPopulate = ReadConfig & { - populate: PopulateOptions | PopulateOptions[]; + populate: PopulateOptions | PopulateOptions[]; }; export type ReadConfigWithoutPopulate = ReadConfig & { @@ -91,9 +93,10 @@ export type ReadConfigWithoutPopulate = ReadConfig & { }; export type UpdateConfig = { - skipDeletedOnDocs?: boolean, - validateSchema?: boolean, - publishEvents?: boolean, + skipDeletedOnDocs?: boolean; + validateSchema?: boolean; + publishEvents?: boolean; + mode?: 'public' | 'private'; }; export type DeleteConfig = { @@ -128,6 +131,7 @@ interface ServiceOptions { collectionOptions?: CollectionOptions; collectionCreateOptions?: CreateCollectionOptions; escapeRegExp?: boolean; + privateFields?: string[]; } export type UpdateFilterFunction = (doc: U) => Partial; @@ -136,3 +140,9 @@ export { IDatabase, ServiceOptions, }; + +export interface GetPrivateProjectionParams { + privateFields?: string[]; + mode: 'public' | 'private'; + findOptions: FindOptions; +} diff --git a/packages/node-mongo/src/utils/helpers.ts b/packages/node-mongo/src/utils/helpers.ts index 3e9fc3b00..ea200a020 100644 --- a/packages/node-mongo/src/utils/helpers.ts +++ b/packages/node-mongo/src/utils/helpers.ts @@ -1,5 +1,6 @@ import * as _ from 'lodash'; import { ObjectId, UpdateFilter } from 'mongodb'; +import { GetPrivateProjectionParams } from 'src/types'; const deepCompare = ( data: unknown, @@ -40,4 +41,33 @@ const addUpdatedOnField = (update: UpdateFilter): UpdateFilter => { } as unknown as UpdateFilter; }; -export { deepCompare, generateId, addUpdatedOnField }; +const omitPrivateFields = ( + doc: T, + privateFields?: string[], +): T => { + if (!doc) return doc; + + return _.omit(doc, privateFields || []) as T; +}; + +const getPrivateFindOptions = (params: GetPrivateProjectionParams) => { + const { findOptions, privateFields, mode } = params; + + if (mode === 'private' && privateFields?.length) { + return { + ...findOptions, + projection: { + ...findOptions.projection, + ...privateFields.reduce((acc: Record, key: string) => { + acc[key] = 0; + + return acc; + }, {}), + }, + }; + } + + return findOptions; +}; + +export { deepCompare, generateId, addUpdatedOnField, omitPrivateFields, getPrivateFindOptions }; From 755b8ea47b33826a075120ce97df315f51ae8e12 Mon Sep 17 00:00:00 2001 From: stasik1404 Date: Mon, 24 Nov 2025 13:26:00 +0300 Subject: [PATCH 2/6] feat: add getPublic method for Service --- docs/packages/node-mongo/overview.mdx | 9 +- packages/node-mongo/src/database.ts | 2 +- packages/node-mongo/src/service.ts | 70 ++++----- .../src/tests/service-extending.spec.ts | 2 +- packages/node-mongo/src/tests/service.spec.ts | 139 +++--------------- packages/node-mongo/src/types/index.ts | 13 +- packages/node-mongo/src/utils/helpers.ts | 28 +--- 7 files changed, 59 insertions(+), 204 deletions(-) diff --git a/docs/packages/node-mongo/overview.mdx b/docs/packages/node-mongo/overview.mdx index 801135576..db0ae8205 100644 --- a/docs/packages/node-mongo/overview.mdx +++ b/docs/packages/node-mongo/overview.mdx @@ -222,8 +222,7 @@ Overrides `ServiceOptions` parameters for create operations. ```typescript type CreateConfig = { validateSchema?: boolean, - publishEvents?: boolean, - mode?: 'public' | 'private', + publishEvents?: boolean }; ``` @@ -233,8 +232,7 @@ Overrides `ServiceOptions` parameters for read operations. ```typescript type ReadConfig = { skipDeletedOnDocs?: boolean, - populate?: PopulateOptions | PopulateOptions[], - mode?: 'public' | 'private', + populate?: PopulateOptions | PopulateOptions[] }; ``` @@ -245,8 +243,7 @@ Overrides `ServiceOptions` parameters for update operations. type UpdateConfig = { skipDeletedOnDocs?: boolean, validateSchema?: boolean, - publishEvents?: boolean, - mode?: 'public' | 'private', + publishEvents?: boolean }; ``` diff --git a/packages/node-mongo/src/database.ts b/packages/node-mongo/src/database.ts index 2bf24ce2c..8bec41baa 100755 --- a/packages/node-mongo/src/database.ts +++ b/packages/node-mongo/src/database.ts @@ -83,7 +83,7 @@ class Database extends EventEmitter { createService( collectionName: string, - options?: ServiceOptions | undefined, + options?: ServiceOptions | undefined, ): Service { return new Service( collectionName, diff --git a/packages/node-mongo/src/service.ts b/packages/node-mongo/src/service.ts index 4a2916472..af87a4100 100755 --- a/packages/node-mongo/src/service.ts +++ b/packages/node-mongo/src/service.ts @@ -33,12 +33,12 @@ import { } from './types'; import logger from './utils/logger'; -import { addUpdatedOnField, generateId, getPrivateFindOptions, omitPrivateFields } from './utils/helpers'; +import { addUpdatedOnField, generateId, omitPrivateFields } from './utils/helpers'; import PopulateUtil from './utils/populate'; import { inMemoryPublisher } from './events/in-memory'; -const defaultOptions: ServiceOptions = { +const defaultOptions: ServiceOptions = { skipDeletedOnDocs: true, publishEvents: true, outbox: false, @@ -57,7 +57,7 @@ class Service { private _collectionName: string; - private options: ServiceOptions; + private options: ServiceOptions; private db; @@ -68,7 +68,7 @@ class Service { constructor( collectionName: string, db: IDatabase, - options: ServiceOptions = {}, + options: ServiceOptions = {}, ) { this._collectionName = collectionName; this.db = db; @@ -240,21 +240,19 @@ class Service { readConfig: ReadConfig = {}, findOptions: FindOptions = {}, ): Promise<(U & PopulateTypes) | U | null> { - const { mode = 'private', populate } = readConfig; + const { populate } = readConfig; const collection = await this.getCollection(); filter = this.handleReadOperations(filter, readConfig); - const privateFindOptions = getPrivateFindOptions({ mode, findOptions, privateFields: this.options.privateFields }); - if (populate) { - const docs = await this.populateAggregate(collection, filter, readConfig, privateFindOptions); + const docs = await this.populateAggregate(collection, filter, readConfig, findOptions); return docs[0] || null; } - return collection.findOne(filter, privateFindOptions); + return collection.findOne(filter, findOptions); } // Method overloading for find @@ -275,19 +273,17 @@ class Service { readConfig: ReadConfig & { page?: number; perPage?: number } = {}, findOptions: FindOptions = {}, ): Promise | FindResult> { - const { mode = 'private', populate, page, perPage } = readConfig; + const { populate, page, perPage } = readConfig; const collection = await this.getCollection(); const hasPaging = !!page && !!perPage; filter = this.handleReadOperations(filter, readConfig); - const privateFindOptions = getPrivateFindOptions({ mode, findOptions, privateFields: this.options.privateFields }); - if (!hasPaging) { const results = populate - ? await this.populateAggregate(collection, filter, readConfig, privateFindOptions) - : await collection.find(filter, privateFindOptions).toArray(); + ? await this.populateAggregate(collection, filter, readConfig, findOptions) + : await collection.find(filter, findOptions).toArray(); return { pagesCount: 1, @@ -296,13 +292,13 @@ class Service { }; } - privateFindOptions.skip = (page - 1) * perPage; - privateFindOptions.limit = perPage; + findOptions.skip = (page - 1) * perPage; + findOptions.limit = perPage; const [results, count] = await Promise.all([ populate - ? this.populateAggregate(collection, filter, readConfig, privateFindOptions) - : collection.find(filter, privateFindOptions).toArray(), + ? this.populateAggregate(collection, filter, readConfig, findOptions) + : collection.find(filter, findOptions).toArray(), collection.countDocuments(filter), ]); @@ -355,7 +351,7 @@ class Service { createConfig: CreateConfig = {}, insertOneOptions: InsertOneOptions = {}, ): Promise => { - const { mode = 'private', publishEvents } = createConfig; + const { publishEvents } = createConfig; const collection = await this.getCollection(); const validEntity = await this.validateCreateOperation(object, createConfig); @@ -385,19 +381,15 @@ class Service { await collection.insertOne(validEntity as OptionalUnlessRequiredId, insertOneOptions); } - if (mode === 'public') { - return validEntity; - } - - return omitPrivateFields(validEntity, this.options.privateFields); + return validEntity; }; insertMany = async ( objects: Partial[], createConfig: CreateConfig = {}, bulkWriteOptions: BulkWriteOptions = {}, - ): Promise> => { - const { mode = 'private', publishEvents } = createConfig; + ): Promise => { + const { publishEvents } = createConfig; const collection = await this.getCollection(); @@ -430,11 +422,7 @@ class Service { await collection.insertMany(validEntities as OptionalUnlessRequiredId[], bulkWriteOptions); } - if (mode === 'public') { - return validEntities; - } - - return validEntities.map((doc) => omitPrivateFields(doc, this.options.privateFields)); + return validEntities; }; replaceOne = async ( @@ -474,7 +462,7 @@ class Service { updateConfig: UpdateConfig = {}, updateOptions: UpdateOptions = {}, ): Promise { - const { mode = 'private', validateSchema, publishEvents } = updateConfig; + const { validateSchema, publishEvents } = updateConfig; const collection = await this.getCollection(); @@ -577,11 +565,7 @@ class Service { ); } - if (mode === 'public') { - return newDoc; - } - - return omitPrivateFields(newDoc, this.options.privateFields); + return newDoc; } updateMany( @@ -604,7 +588,7 @@ class Service { updateConfig: UpdateConfig = {}, updateOptions: UpdateOptions = {}, ): Promise { - const { mode = 'private', validateSchema, publishEvents } = updateConfig; + const { validateSchema, publishEvents } = updateConfig; const collection = await this.getCollection(); @@ -728,11 +712,7 @@ class Service { await collection.bulkWrite(bulkWriteQuery, updateOptions); } - if (mode === 'public') { - return updated.map((u) => u?.doc) as U[]; - } - - return updated.map((u) => omitPrivateFields(u?.doc as U, this.options.privateFields)) as U[]; + return updated.map((u) => u?.doc).filter(Boolean) as U[]; } deleteOne = async ( @@ -1008,6 +988,10 @@ class Service { this.collection = null; } }; + + getPublic = (doc: U | null): Partial | null => { + return omitPrivateFields(doc, this.options.privateFields || []); + }; } export default Service; diff --git a/packages/node-mongo/src/tests/service-extending.spec.ts b/packages/node-mongo/src/tests/service-extending.spec.ts index 04bf17e9d..6b85bf9a5 100644 --- a/packages/node-mongo/src/tests/service-extending.spec.ts +++ b/packages/node-mongo/src/tests/service-extending.spec.ts @@ -28,7 +28,7 @@ class CustomService extends Service { }; } -function createService(collectionName: string, options: ServiceOptions = {}) { +function createService(collectionName: string, options: ServiceOptions = {}) { return new CustomService(collectionName, database, options); } diff --git a/packages/node-mongo/src/tests/service.spec.ts b/packages/node-mongo/src/tests/service.spec.ts index 8976fca66..9dfa50b5a 100644 --- a/packages/node-mongo/src/tests/service.spec.ts +++ b/packages/node-mongo/src/tests/service.spec.ts @@ -1499,142 +1499,45 @@ describe('service.ts', () => { updatedUser?.permissions?.length?.should.be.equal(0); }); - it('should omit private fields after insertOne method', async () => { - const newUserData = { - fullName: 'Test User with private fields', + it('should omit private fields using array configuration', async () => { + const userToInsertPayload = { + fullName: 'John Doe', age: 30, role: UserRoles.ADMIN, passwordHash: '123456', }; - - const user = await usersServiceWithPrivateFields.insertOne(newUserData); - - (user?.passwordHash === undefined).should.be.equal(true); - user?.role?.should.be.equal(newUserData.role); - user?.fullName?.should.be.equal(newUserData.fullName); - user?.age?.should.be.equal(newUserData.age); - }); - it('should omit private fields after insertMany method', async () => { - const newUsersData = [ - { - fullName: 'Test User with private fields', - age: 30, - role: UserRoles.ADMIN, - passwordHash: '123456', - }, - { - fullName: 'Test User with private fields 2', - age: 20, - role: UserRoles.MANAGER, - passwordHash: '789012', - }, - ]; - - const newUsers = await usersServiceWithPrivateFields.insertMany(newUsersData); + const user = await usersServiceWithPrivateFields.insertOne(userToInsertPayload); - (newUsers[0]?.passwordHash === undefined).should.be.equal(true); - newUsers[0]?.role?.should.be.equal(newUsersData[0].role); - newUsers[0]?.fullName?.should.be.equal(newUsersData[0].fullName); - newUsers[0]?.age?.should.be.equal(newUsersData[0].age); + const publicUser = usersServiceWithPrivateFields.getPublic(user); - (newUsers[1]?.passwordHash === undefined).should.be.equal(true); - newUsers[1]?.role?.should.be.equal(newUsersData[1].role); - newUsers[1]?.fullName?.should.be.equal(newUsersData[1].fullName); - newUsers[1]?.age?.should.be.equal(newUsersData[1].age); + (publicUser?.passwordHash === undefined).should.be.equal(true); + publicUser?.fullName?.should.be.equal(userToInsertPayload.fullName); + publicUser?.age?.should.be.equal(userToInsertPayload.age); + publicUser?.role?.should.be.equal(userToInsertPayload.role); }); - it('should omit private fields after updateOne method', async () => { - const newUsersData = { - fullName: 'Test User with private fields', + it('should return original document when no privateFields configured', async () => { + const userToInsertPayload = { + fullName: 'John Doe', age: 30, role: UserRoles.ADMIN, passwordHash: '123456', }; - - const updateUserData = { - fullName: 'Test User with private fields 2', - age: 20, - role: UserRoles.MANAGER, - passwordHash: '789012', - }; - - const newUser = await usersServiceWithPrivateFields.insertOne(newUsersData); - const updatedUser = await usersServiceWithPrivateFields.updateOne({ _id: newUser._id }, () => updateUserData); - - (updatedUser?.passwordHash === undefined).should.be.equal(true); - updatedUser?.role?.should.be.equal(updateUserData.role); - updatedUser?.fullName?.should.be.equal(updateUserData.fullName); - updatedUser?.age?.should.be.equal(updateUserData.age); - }); - it('should omit private fields after updateMany method', async () => { - const newUsersData = [{ - fullName: 'Test User with private fields', - age: 30, - role: UserRoles.ADMIN, - passwordHash: '123456', - }, - { - fullName: 'Test User with private fields 2', - age: 20, - role: UserRoles.MANAGER, - passwordHash: '789012', - }]; - - const updateUserData = { - fullName: 'Test User with private fields 2', - age: 20, - role: UserRoles.MANAGER, - passwordHash: '789012', - }; - - const newUser = await usersServiceWithPrivateFields.insertMany(newUsersData); - const newUsersIds = newUser.map((u) => u?._id).filter(Boolean) as string[]; - const updatedUsers = await usersServiceWithPrivateFields.updateMany({ _id: { $in: newUsersIds } }, () => updateUserData); - - (updatedUsers[0]?.passwordHash === undefined).should.be.equal(true); - updatedUsers[0]?.role?.should.be.equal(updateUserData.role); - updatedUsers[0]?.fullName?.should.be.equal(updateUserData.fullName); - updatedUsers[0]?.age?.should.be.equal(updateUserData.age); - - (updatedUsers[1]?.passwordHash === undefined).should.be.equal(true); - updatedUsers[1]?.role?.should.be.equal(updateUserData.role); - updatedUsers[1]?.fullName?.should.be.equal(updateUserData.fullName); - updatedUsers[1]?.age?.should.be.equal(updateUserData.age); - }); + const user = await usersService.insertOne(userToInsertPayload); - it('should omit private fields after find method', async () => { - const newUserData = { - fullName: 'Test User with private fields', - age: 30, - role: UserRoles.ADMIN, - passwordHash: '123456', - }; - - const user = await usersServiceWithPrivateFields.insertOne(newUserData); - const { results: foundUsers } = await usersServiceWithPrivateFields.find({ _id: user._id }); + const publicUser = usersService.getPublic(user); - (foundUsers[0]?.passwordHash === undefined).should.be.equal(true); - foundUsers[0]?.role?.should.be.equal(newUserData.role); - foundUsers[0]?.fullName?.should.be.equal(newUserData.fullName); - foundUsers[0]?.age?.should.be.equal(newUserData.age); + publicUser?.passwordHash?.should.be.equal(userToInsertPayload.passwordHash); + publicUser?.fullName?.should.be.equal(userToInsertPayload.fullName); + publicUser?.age?.should.be.equal(userToInsertPayload.age); + publicUser?.role?.should.be.equal(userToInsertPayload.role); }); - it('should omit private fields after findOne method', async () => { - const newUserData = { - fullName: 'Test User with private fields', - age: 30, - role: UserRoles.ADMIN, - passwordHash: '123456', - }; + it('should handle null documents in getPublic', async () => { + const publicUser = usersServiceWithPrivateFields.getPublic(null); - const user = await usersServiceWithPrivateFields.insertOne(newUserData); - const foundUser = await usersServiceWithPrivateFields.findOne({ _id: user._id }); - - (foundUser?.passwordHash === undefined).should.be.equal(true); - foundUser?.role?.should.be.equal(newUserData.role); - foundUser?.fullName?.should.be.equal(newUserData.fullName); - foundUser?.age?.should.be.equal(newUserData.age); + (publicUser === null).should.be.equal(true); }); }); diff --git a/packages/node-mongo/src/types/index.ts b/packages/node-mongo/src/types/index.ts index f04d5ea79..057844aed 100644 --- a/packages/node-mongo/src/types/index.ts +++ b/packages/node-mongo/src/types/index.ts @@ -63,7 +63,6 @@ export type FindResult = { export type CreateConfig = { validateSchema?: boolean; publishEvents?: boolean; - mode?: 'public' | 'private'; }; export type PopulateOptions = { @@ -80,7 +79,6 @@ export type PopulateOptions = { export type ReadConfig = { skipDeletedOnDocs?: boolean, populate?: PopulateOptions | PopulateOptions[]; - mode?: 'public' | 'private'; }; // Type-safe discriminated unions for populate operations @@ -96,7 +94,6 @@ export type UpdateConfig = { skipDeletedOnDocs?: boolean; validateSchema?: boolean; publishEvents?: boolean; - mode?: 'public' | 'private'; }; export type DeleteConfig = { @@ -121,7 +118,7 @@ interface IDatabase { ) => Promise, } -interface ServiceOptions { +interface ServiceOptions { skipDeletedOnDocs?: boolean, schemaValidator?: (obj: any) => Promise, publishEvents?: boolean, @@ -131,7 +128,7 @@ interface ServiceOptions { collectionOptions?: CollectionOptions; collectionCreateOptions?: CreateCollectionOptions; escapeRegExp?: boolean; - privateFields?: string[]; + privateFields?: Array; } export type UpdateFilterFunction = (doc: U) => Partial; @@ -140,9 +137,3 @@ export { IDatabase, ServiceOptions, }; - -export interface GetPrivateProjectionParams { - privateFields?: string[]; - mode: 'public' | 'private'; - findOptions: FindOptions; -} diff --git a/packages/node-mongo/src/utils/helpers.ts b/packages/node-mongo/src/utils/helpers.ts index ea200a020..fa18bcc39 100644 --- a/packages/node-mongo/src/utils/helpers.ts +++ b/packages/node-mongo/src/utils/helpers.ts @@ -1,6 +1,5 @@ import * as _ from 'lodash'; import { ObjectId, UpdateFilter } from 'mongodb'; -import { GetPrivateProjectionParams } from 'src/types'; const deepCompare = ( data: unknown, @@ -42,32 +41,13 @@ const addUpdatedOnField = (update: UpdateFilter): UpdateFilter => { }; const omitPrivateFields = ( - doc: T, - privateFields?: string[], -): T => { + doc: T | null, + privateFields: Array, +): Partial | null => { if (!doc) return doc; return _.omit(doc, privateFields || []) as T; }; -const getPrivateFindOptions = (params: GetPrivateProjectionParams) => { - const { findOptions, privateFields, mode } = params; - if (mode === 'private' && privateFields?.length) { - return { - ...findOptions, - projection: { - ...findOptions.projection, - ...privateFields.reduce((acc: Record, key: string) => { - acc[key] = 0; - - return acc; - }, {}), - }, - }; - } - - return findOptions; -}; - -export { deepCompare, generateId, addUpdatedOnField, omitPrivateFields, getPrivateFindOptions }; +export { deepCompare, generateId, addUpdatedOnField, omitPrivateFields }; From 68d52d0ac17dd8bb1985cc28d79d5effa83fbb69 Mon Sep 17 00:00:00 2001 From: stasik1404 Date: Mon, 24 Nov 2025 13:45:07 +0300 Subject: [PATCH 3/6] feat: Add doc for new getPublic method --- docs/packages/node-mongo/overview.mdx | 7 +++--- packages/node-mongo/README.md | 32 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/docs/packages/node-mongo/overview.mdx b/docs/packages/node-mongo/overview.mdx index db0ae8205..0ce74f0e3 100644 --- a/docs/packages/node-mongo/overview.mdx +++ b/docs/packages/node-mongo/overview.mdx @@ -222,7 +222,7 @@ Overrides `ServiceOptions` parameters for create operations. ```typescript type CreateConfig = { validateSchema?: boolean, - publishEvents?: boolean + publishEvents?: boolean, }; ``` @@ -232,7 +232,7 @@ Overrides `ServiceOptions` parameters for read operations. ```typescript type ReadConfig = { skipDeletedOnDocs?: boolean, - populate?: PopulateOptions | PopulateOptions[] + populate?: PopulateOptions | PopulateOptions[], }; ``` @@ -243,7 +243,7 @@ Overrides `ServiceOptions` parameters for update operations. type UpdateConfig = { skipDeletedOnDocs?: boolean, validateSchema?: boolean, - publishEvents?: boolean + publishEvents?: boolean, }; ``` @@ -315,6 +315,7 @@ function createService(collectionName: string, options: Ser const userService = createService('users', { schemaValidator: (obj) => schema.parseAsync(obj), + privateFields: ['passwordHash', 'signupToken', 'resetPasswordToken'], }); await userService.createOrUpdate( diff --git a/packages/node-mongo/README.md b/packages/node-mongo/README.md index 8f815eca0..805dfb55b 100644 --- a/packages/node-mongo/README.md +++ b/packages/node-mongo/README.md @@ -253,6 +253,38 @@ Fetches the first document that matches the filter. Returns `null` if document w **Returns** `Promise`. +### `getPublic` + +```typescript +getPublic(doc: U | null): Partial | null +``` + +```typescript +const user = await userService.findOne({ _id: u._id }); + +const publicUser = userService.getPublic(user); +``` + +Removes private fields from a document and returns a sanitized version. Private fields are defined in the `privateFields` option when creating the service. Returns `null` if the input document is `null`. If no `privateFields` are configured, returns the original document unchanged. + +**Parameters** + +- doc: `U | null` - The document to sanitize. Can be `null`. + +**Returns** `Partial | null` - A document with private fields omitted, or `null` if the input was `null`. + +**Example** + +```typescript +const service = db.createService("users", { + privateFields: ['passwordHash', 'signupToken', 'resetPasswordToken'], +}); + +const user = await service.findOne({ _id: userId }); + +const publicUser = service.getPublic(user); +``` + ### `updateOne` ```typescript From 3b8eaaec4942d0448ae7e33bb9bfab629af63955 Mon Sep 17 00:00:00 2001 From: stasik1404 Date: Mon, 24 Nov 2025 13:49:16 +0300 Subject: [PATCH 4/6] chore: code refactor --- packages/node-mongo/src/service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/node-mongo/src/service.ts b/packages/node-mongo/src/service.ts index af87a4100..a0b5279da 100755 --- a/packages/node-mongo/src/service.ts +++ b/packages/node-mongo/src/service.ts @@ -273,7 +273,7 @@ class Service { readConfig: ReadConfig & { page?: number; perPage?: number } = {}, findOptions: FindOptions = {}, ): Promise | FindResult> { - const { populate, page, perPage } = readConfig; + const { populate, page, perPage } = readConfig; const collection = await this.getCollection(); const hasPaging = !!page && !!perPage; @@ -352,6 +352,7 @@ class Service { insertOneOptions: InsertOneOptions = {}, ): Promise => { const { publishEvents } = createConfig; + const collection = await this.getCollection(); const validEntity = await this.validateCreateOperation(object, createConfig); From 255cbfa83cb90c83864e91358ba737a36470aaff Mon Sep 17 00:00:00 2001 From: stasik1404 Date: Mon, 24 Nov 2025 14:37:43 +0300 Subject: [PATCH 5/6] fix: rolback node-mongo docs --- docs/packages/node-mongo/overview.mdx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/packages/node-mongo/overview.mdx b/docs/packages/node-mongo/overview.mdx index 0ce74f0e3..933a817f2 100644 --- a/docs/packages/node-mongo/overview.mdx +++ b/docs/packages/node-mongo/overview.mdx @@ -214,7 +214,6 @@ interface ServiceOptions { |`escapeRegExp`|Escape `$regex` values to prevent special characters from being interpreted as patterns. |`false`| |`collectionOptions`|MongoDB [CollectionOptions](https://mongodb.github.io/node-mongodb-native/4.10/interfaces/CollectionOptions.html)|`{}`| |`collectionCreateOptions`|MongoDB [CreateCollectionOptions](https://mongodb.github.io/node-mongodb-native/4.10/interfaces/CreateCollectionOptions.html)|`{}`| -|`privateFields`|Fields that should be omitted from the public response.|`[]`| ### `CreateConfig` Overrides `ServiceOptions` parameters for create operations. @@ -232,7 +231,6 @@ Overrides `ServiceOptions` parameters for read operations. ```typescript type ReadConfig = { skipDeletedOnDocs?: boolean, - populate?: PopulateOptions | PopulateOptions[], }; ``` @@ -283,11 +281,19 @@ Extending API for a single service. ```typescript const service = db.createService('users', { schemaValidator: (obj) => schema.parseAsync(obj), - privateFields: ['passwordHash', 'signupToken', 'resetPasswordToken'], }); +const privateFields = [ + 'passwordHash', + 'signupToken', + 'resetPasswordToken', +]; + +const getPublic = (user: User | null) => _.omit(user, privateFields); + export default Object.assign(service, { updateLastRequest, + getPublic, }); ``` @@ -315,7 +321,6 @@ function createService(collectionName: string, options: Ser const userService = createService('users', { schemaValidator: (obj) => schema.parseAsync(obj), - privateFields: ['passwordHash', 'signupToken', 'resetPasswordToken'], }); await userService.createOrUpdate( From bf6db6d93c84a09f92bcde61b41f3ed5e42f406c Mon Sep 17 00:00:00 2001 From: stasik1404 Date: Tue, 25 Nov 2025 10:16:44 +0300 Subject: [PATCH 6/6] feat: add types for result value from getPublic method --- packages/node-mongo/README.md | 6 ++++-- packages/node-mongo/src/database.ts | 8 ++++---- packages/node-mongo/src/service.ts | 20 +++++++++++-------- packages/node-mongo/src/tests/service.spec.ts | 11 ++++++---- packages/node-mongo/src/types/index.ts | 4 ++-- packages/node-mongo/src/utils/helpers.ts | 11 +++++----- 6 files changed, 34 insertions(+), 26 deletions(-) diff --git a/packages/node-mongo/README.md b/packages/node-mongo/README.md index 805dfb55b..1d228fc10 100644 --- a/packages/node-mongo/README.md +++ b/packages/node-mongo/README.md @@ -276,8 +276,10 @@ Removes private fields from a document and returns a sanitized version. Private **Example** ```typescript -const service = db.createService("users", { - privateFields: ['passwordHash', 'signupToken', 'resetPasswordToken'], +const USER_PRIVATE_FIELDS = ['passwordHash', 'signupToken', 'resetPasswordToken'] as const + +const service = db.createService("users", { + privateFields: USER_PRIVATE_FIELDS, }); const user = await service.findOne({ _id: userId }); diff --git a/packages/node-mongo/src/database.ts b/packages/node-mongo/src/database.ts index 8bec41baa..bd1fe06e2 100755 --- a/packages/node-mongo/src/database.ts +++ b/packages/node-mongo/src/database.ts @@ -81,11 +81,11 @@ class Database extends EventEmitter { await this.client.close(); }; - createService( + createService = []>( collectionName: string, - options?: ServiceOptions | undefined, - ): Service { - return new Service( + options?: ServiceOptions | undefined, + ): Service { + return new Service( collectionName, this as IDatabase, options, diff --git a/packages/node-mongo/src/service.ts b/packages/node-mongo/src/service.ts index a0b5279da..03877e82c 100755 --- a/packages/node-mongo/src/service.ts +++ b/packages/node-mongo/src/service.ts @@ -38,7 +38,7 @@ import PopulateUtil from './utils/populate'; import { inMemoryPublisher } from './events/in-memory'; -const defaultOptions: ServiceOptions = { +const defaultOptions: ServiceOptions> = { skipDeletedOnDocs: true, publishEvents: true, outbox: false, @@ -50,14 +50,14 @@ const defaultOptions: ServiceOptions = { const isDev = process.env.NODE_ENV === 'development'; -class Service { +class Service = []> { private client?: MongoClient; private collection: Collection | null; private _collectionName: string; - private options: ServiceOptions; + private options: ServiceOptions; private db; @@ -68,14 +68,14 @@ class Service { constructor( collectionName: string, db: IDatabase, - options: ServiceOptions = {}, + options: ServiceOptions = {}, ) { this._collectionName = collectionName; this.db = db; this.options = { ...defaultOptions, ...options, - }; + } as ServiceOptions; this.waitForConnection = db.waitForConnection; if (this.options.outbox) { @@ -990,9 +990,13 @@ class Service { } }; - getPublic = (doc: U | null): Partial | null => { - return omitPrivateFields(doc, this.options.privateFields || []); - }; + getPublic(doc: null): null; + + getPublic(doc: T): Omit; + + getPublic(doc: T | null): Omit | null { + return omitPrivateFields(doc, this.options.privateFields || []); + } } export default Service; diff --git a/packages/node-mongo/src/tests/service.spec.ts b/packages/node-mongo/src/tests/service.spec.ts index 9dfa50b5a..2a58b6626 100644 --- a/packages/node-mongo/src/tests/service.spec.ts +++ b/packages/node-mongo/src/tests/service.spec.ts @@ -36,6 +36,8 @@ enum AdminPermissions { EDIT = 'edit', } +const USER_PRIVATE_FIELDS = ['passwordHash'] as const; + const userSchema = z.object({ _id: z.string(), createdOn: z.date().optional(), @@ -67,12 +69,11 @@ const companyService = database.createService('companies', { schemaValidator: (obj) => companySchema.parseAsync(obj), }); -const usersServiceWithPrivateFields = database.createService('usersWithPrivateFields', { +const usersServiceWithPrivateFields = database.createService('usersWithPrivateFields', { schemaValidator: (obj) => userSchema.parseAsync(obj), - privateFields: ['passwordHash'], + privateFields: USER_PRIVATE_FIELDS, }); - describe('service.ts', () => { before(async () => { await database.connect(); @@ -1511,7 +1512,9 @@ describe('service.ts', () => { const publicUser = usersServiceWithPrivateFields.getPublic(user); - (publicUser?.passwordHash === undefined).should.be.equal(true); + // @ts-expect-error Property 'passwordHash' does not exist + publicUser?.passwordHash; + publicUser?.fullName?.should.be.equal(userToInsertPayload.fullName); publicUser?.age?.should.be.equal(userToInsertPayload.age); publicUser?.role?.should.be.equal(userToInsertPayload.role); diff --git a/packages/node-mongo/src/types/index.ts b/packages/node-mongo/src/types/index.ts index 057844aed..a10d328fb 100644 --- a/packages/node-mongo/src/types/index.ts +++ b/packages/node-mongo/src/types/index.ts @@ -118,7 +118,7 @@ interface IDatabase { ) => Promise, } -interface ServiceOptions { +interface ServiceOptions = []> { skipDeletedOnDocs?: boolean, schemaValidator?: (obj: any) => Promise, publishEvents?: boolean, @@ -128,7 +128,7 @@ interface ServiceOptions { collectionOptions?: CollectionOptions; collectionCreateOptions?: CreateCollectionOptions; escapeRegExp?: boolean; - privateFields?: Array; + privateFields?: PrivateFields; } export type UpdateFilterFunction = (doc: U) => Partial; diff --git a/packages/node-mongo/src/utils/helpers.ts b/packages/node-mongo/src/utils/helpers.ts index fa18bcc39..f53222ee5 100644 --- a/packages/node-mongo/src/utils/helpers.ts +++ b/packages/node-mongo/src/utils/helpers.ts @@ -40,14 +40,13 @@ const addUpdatedOnField = (update: UpdateFilter): UpdateFilter => { } as unknown as UpdateFilter; }; -const omitPrivateFields = ( +const omitPrivateFields = ( doc: T | null, - privateFields: Array, -): Partial | null => { - if (!doc) return doc; + privateFields: ReadonlyArray, +): Omit | null => { + if (!doc) return null; - return _.omit(doc, privateFields || []) as T; + return _.omit(doc, privateFields); }; - export { deepCompare, generateId, addUpdatedOnField, omitPrivateFields };