From 7c3e9c0d598960b8fa43a3e56449d27e01c4b875 Mon Sep 17 00:00:00 2001 From: Aleksandra Mazur Date: Mon, 15 Sep 2025 00:07:23 +0300 Subject: [PATCH] feat: auto omitting of document private fields --- docs/packages/node-mongo/overview.mdx | 13 ++----- .../api/src/resources/user/user.service.ts | 9 +---- .../api/src/resources/user/user.service.ts | 5 +-- packages/node-mongo/README.md | 5 +-- packages/node-mongo/src/service.ts | 6 ++- packages/node-mongo/src/tests/service.spec.ts | 38 +++++++++++++++++++ packages/node-mongo/src/types/index.ts | 1 + packages/node-mongo/src/utils/helpers.ts | 13 ++++++- .../api/src/resources/user/user.service.ts | 5 +-- 9 files changed, 68 insertions(+), 27 deletions(-) diff --git a/docs/packages/node-mongo/overview.mdx b/docs/packages/node-mongo/overview.mdx index 933a817f..3ded55e5 100644 --- a/docs/packages/node-mongo/overview.mdx +++ b/docs/packages/node-mongo/overview.mdx @@ -200,6 +200,7 @@ interface ServiceOptions { outbox?: boolean, collectionOptions?: CollectionOptions; collectionCreateOptions?: CreateCollectionOptions; + privateFields?: string[]; } ``` @@ -214,6 +215,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. @@ -281,19 +283,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, }); ``` @@ -321,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/examples/public-docs/apps/api/src/resources/user/user.service.ts b/examples/public-docs/apps/api/src/resources/user/user.service.ts index 559fbccf..e84e8ef2 100644 --- a/examples/public-docs/apps/api/src/resources/user/user.service.ts +++ b/examples/public-docs/apps/api/src/resources/user/user.service.ts @@ -8,6 +8,7 @@ import { User } from './user.types'; const service = db.createService(DATABASE_DOCUMENTS.USERS, { schemaValidator: (obj) => schema.parseAsync(obj), + privateFields: ['passwordHash', 'signupToken', 'resetPasswordToken'], }); const updateLastRequest = (_id: string) => { @@ -21,13 +22,7 @@ const updateLastRequest = (_id: string) => { ); }; -export const privateFields = [ - 'passwordHash', - 'signupToken', - 'resetPasswordToken', -]; - -const getPublic = (user: User | null) => _.omit(user, privateFields); +const getPublic = (user: User | null) => service.getPublic(user); export default Object.assign(service, { updateLastRequest, diff --git a/examples/stripe-subscriptions/apps/api/src/resources/user/user.service.ts b/examples/stripe-subscriptions/apps/api/src/resources/user/user.service.ts index 6768f412..dd92a239 100644 --- a/examples/stripe-subscriptions/apps/api/src/resources/user/user.service.ts +++ b/examples/stripe-subscriptions/apps/api/src/resources/user/user.service.ts @@ -8,6 +8,7 @@ import { User } from 'types'; const service = db.createService(DATABASE_DOCUMENTS.USERS, { schemaValidator: (obj) => userSchema.parseAsync(obj), + privateFields: ['passwordHash', 'signupToken', 'resetPasswordToken'], }); const updateLastRequest = (_id: string) => @@ -20,9 +21,7 @@ const updateLastRequest = (_id: string) => }, ); -const privateFields = ['passwordHash', 'signupToken', 'resetPasswordToken']; - -const getPublic = (user: User | null) => _.omit(user, privateFields); +const getPublic = (user: User | null) => service.getPublic(user); export default Object.assign(service, { updateLastRequest, diff --git a/packages/node-mongo/README.md b/packages/node-mongo/README.md index a38004c4..c1ddece3 100644 --- a/packages/node-mongo/README.md +++ b/packages/node-mongo/README.md @@ -1046,11 +1046,10 @@ 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); +const getPublic = (user: User | null) => service.getPublic(user); export default Object.assign(service, { updateLastRequest, diff --git a/packages/node-mongo/src/service.ts b/packages/node-mongo/src/service.ts index f4bb06ad..fed658a6 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, omitPrivateFields } from './utils/helpers'; import PopulateUtil from './utils/populate'; import { inMemoryPublisher } from './events/in-memory'; @@ -977,6 +977,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.spec.ts b/packages/node-mongo/src/tests/service.spec.ts index b09d0328..19b15329 100644 --- a/packages/node-mongo/src/tests/service.spec.ts +++ b/packages/node-mongo/src/tests/service.spec.ts @@ -66,6 +66,11 @@ const companyService = database.createService('companies', { schemaValidator: (obj) => companySchema.parseAsync(obj), }); +const usersServicePrivateFields = database.createService('usersPrivateFields', { + schemaValidator: (obj) => userSchema.parseAsync(obj), + privateFields: ['fullName', 'age'], +}); + describe('service.ts', () => { before(async () => { await database.connect(); @@ -1491,4 +1496,37 @@ describe('service.ts', () => { updatedUser?.permissions?.[1]?.should.be.undefined; updatedUser?.permissions?.length?.should.be.equal(0); }); + + it('should omit private fields using array configuration', async () => { + const user = await usersServicePrivateFields.insertOne({ + fullName: 'John Doe', + age: 30, + role: UserRoles.ADMIN, + }); + + const publicUser = usersServicePrivateFields.getPublic(user); + + (publicUser?.fullName === undefined).should.be.equal(true); + (publicUser?.age === undefined).should.be.equal(true); + publicUser?.role?.should.be.equal(UserRoles.ADMIN); + }); + + it('should return original document when no privateFields configured', async () => { + const user = await usersService.insertOne({ + fullName: 'Test User', + age: 30, + role: UserRoles.ADMIN, + }); + + const publicUser = usersService.getPublic(user); + + publicUser?.fullName?.should.be.equal('Test User'); + publicUser?.age?.should.be.equal(30); + publicUser?.role?.should.be.equal(UserRoles.ADMIN); + }); + + it('should handle null documents in getPublic', async () => { + const publicUser = usersServicePrivateFields.getPublic(null); + (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 b81b026c..65856be4 100644 --- a/packages/node-mongo/src/types/index.ts +++ b/packages/node-mongo/src/types/index.ts @@ -128,6 +128,7 @@ interface ServiceOptions { collectionOptions?: CollectionOptions; collectionCreateOptions?: CreateCollectionOptions; escapeRegExp?: boolean; + privateFields?: string[]; } 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 3e9fc3b0..06f1b485 100644 --- a/packages/node-mongo/src/utils/helpers.ts +++ b/packages/node-mongo/src/utils/helpers.ts @@ -40,4 +40,15 @@ const addUpdatedOnField = (update: UpdateFilter): UpdateFilter => { } as unknown as UpdateFilter; }; -export { deepCompare, generateId, addUpdatedOnField }; +const omitPrivateFields = >( + doc: T | null | undefined, + privateFields?: string[], +): Partial | null => { + if (!doc) return null; + + if (!privateFields) return doc; + + return _.omit(doc, privateFields) as Partial; +}; + +export { deepCompare, generateId, addUpdatedOnField, omitPrivateFields }; diff --git a/template/apps/api/src/resources/user/user.service.ts b/template/apps/api/src/resources/user/user.service.ts index f38346f7..4a0f2244 100644 --- a/template/apps/api/src/resources/user/user.service.ts +++ b/template/apps/api/src/resources/user/user.service.ts @@ -9,6 +9,7 @@ import { User } from 'types'; const service = db.createService(DATABASE_DOCUMENTS.USERS, { schemaValidator: (obj) => userSchema.parseAsync(obj), escapeRegExp: true, + privateFields: ['passwordHash'], }); service.createIndex({ email: 1 }, { unique: true }); @@ -23,9 +24,7 @@ const updateLastRequest = (_id: string) => }, ); -const privateFields = ['passwordHash']; - -const getPublic = (user: User | null) => _.omit(user, privateFields); + const getPublic = (user: User | null) => service.getPublic(user); export default Object.assign(service, { updateLastRequest,