@furystack/core 15.0.36 → 15.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/esm/filter-items.d.ts +11 -0
- package/esm/filter-items.d.ts.map +1 -0
- package/esm/filter-items.js +130 -0
- package/esm/filter-items.js.map +1 -0
- package/esm/filter-items.spec.d.ts +2 -0
- package/esm/filter-items.spec.d.ts.map +1 -0
- package/esm/filter-items.spec.js +174 -0
- package/esm/filter-items.spec.js.map +1 -0
- package/esm/in-memory-store.d.ts +0 -2
- package/esm/in-memory-store.d.ts.map +1 -1
- package/esm/in-memory-store.js +4 -117
- package/esm/in-memory-store.js.map +1 -1
- package/esm/index.d.ts +2 -0
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +2 -0
- package/esm/index.js.map +1 -1
- package/esm/models/physical-store.d.ts +8 -1
- package/esm/models/physical-store.d.ts.map +1 -1
- package/esm/system-identity-context.d.ts +68 -0
- package/esm/system-identity-context.d.ts.map +1 -0
- package/esm/system-identity-context.js +75 -0
- package/esm/system-identity-context.js.map +1 -0
- package/esm/system-identity-context.spec.d.ts +2 -0
- package/esm/system-identity-context.spec.d.ts.map +1 -0
- package/esm/system-identity-context.spec.js +90 -0
- package/esm/system-identity-context.spec.js.map +1 -0
- package/package.json +2 -2
- package/src/filter-items.spec.ts +216 -0
- package/src/filter-items.ts +148 -0
- package/src/in-memory-store.ts +4 -136
- package/src/index.ts +2 -0
- package/src/models/physical-store.ts +8 -1
- package/src/system-identity-context.spec.ts +101 -0
- package/src/system-identity-context.ts +83 -0
|
@@ -62,7 +62,14 @@ export interface FindOptions<T, TSelect extends Array<keyof T>> {
|
|
|
62
62
|
export type PartialResult<T, TFields extends Array<keyof T>> = Pick<T, TFields[number]>;
|
|
63
63
|
export declare const selectFields: <T extends object, TField extends Array<keyof T>>(entry: T, ...fields: TField) => PartialResult<T, TField>;
|
|
64
64
|
/**
|
|
65
|
-
* Interface that defines a physical store implementation
|
|
65
|
+
* Interface that defines a physical store implementation.
|
|
66
|
+
*
|
|
67
|
+
* **Important:** Writing directly to a physical store bypasses the Repository {@link DataSet} layer.
|
|
68
|
+
* This means authorization, modification hooks, and DataSet events (used by entity sync) will **not** be triggered.
|
|
69
|
+
* For any write operation that should be observable by other parts of the system (e.g. entity sync, audit logging),
|
|
70
|
+
* use the corresponding {@link DataSet} method instead via `getDataSetFor()`.
|
|
71
|
+
*
|
|
72
|
+
* @see {@link DataSet} for the authorized, event-emitting write gateway
|
|
66
73
|
*/
|
|
67
74
|
export interface PhysicalStore<T, TPrimaryKey extends keyof T, TWriteableData = WithOptionalId<T, TPrimaryKey>> extends EventHub<{
|
|
68
75
|
onEntityAdded: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"physical-store.d.ts","sourceRoot":"","sources":["../../src/models/physical-store.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"physical-store.d.ts","sourceRoot":"","sources":["../../src/models/physical-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Injector } from '@furystack/inject';
|
|
2
|
+
import { IdentityContext } from './identity-context.js';
|
|
3
|
+
import type { User } from './models/user.js';
|
|
4
|
+
/**
|
|
5
|
+
* An elevated {@link IdentityContext} that is always authenticated and authorized.
|
|
6
|
+
* Intended for trusted server-side operations such as background jobs, migrations, and seed scripts.
|
|
7
|
+
*
|
|
8
|
+
* **Warning:** This context bypasses **all** authorization checks. Never use it in user-facing
|
|
9
|
+
* request pipelines or any context where untrusted input could reach the DataSet.
|
|
10
|
+
* Prefer {@link useSystemIdentityContext} for scoped usage with automatic cleanup.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { useSystemIdentityContext } from '@furystack/core'
|
|
15
|
+
* import { getDataSetFor } from '@furystack/repository'
|
|
16
|
+
* import { usingAsync } from '@furystack/utils'
|
|
17
|
+
*
|
|
18
|
+
* await usingAsync(
|
|
19
|
+
* useSystemIdentityContext({ injector, username: 'migration-job' }),
|
|
20
|
+
* async (systemInjector) => {
|
|
21
|
+
* const dataSet = getDataSetFor(systemInjector, MyModel, 'id')
|
|
22
|
+
* await dataSet.add(systemInjector, newEntity)
|
|
23
|
+
* },
|
|
24
|
+
* )
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare class SystemIdentityContext extends IdentityContext {
|
|
28
|
+
private readonly username;
|
|
29
|
+
constructor(options?: {
|
|
30
|
+
username?: string;
|
|
31
|
+
});
|
|
32
|
+
isAuthenticated(): Promise<boolean>;
|
|
33
|
+
isAuthorized(..._roles: string[]): Promise<boolean>;
|
|
34
|
+
getCurrentUser<TUser extends User>(): Promise<TUser>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Creates a scoped child injector with an elevated {@link SystemIdentityContext}.
|
|
38
|
+
* The returned injector is {@link AsyncDisposable} and works with `usingAsync()` for automatic cleanup.
|
|
39
|
+
*
|
|
40
|
+
* **Warning:** The returned injector bypasses **all** authorization checks. Only use this in trusted
|
|
41
|
+
* server-side contexts (background jobs, migrations, seed scripts). Never pass the returned injector
|
|
42
|
+
* to user-facing request handlers.
|
|
43
|
+
*
|
|
44
|
+
* @param options.injector The parent injector to create a child from
|
|
45
|
+
* @param options.username The username for the system identity (defaults to `'system'`)
|
|
46
|
+
* @returns A child injector with the SystemIdentityContext set as the IdentityContext
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* import { useSystemIdentityContext } from '@furystack/core'
|
|
51
|
+
* import { getDataSetFor } from '@furystack/repository'
|
|
52
|
+
* import { usingAsync } from '@furystack/utils'
|
|
53
|
+
*
|
|
54
|
+
* await usingAsync(
|
|
55
|
+
* useSystemIdentityContext({ injector, username: 'seed-script' }),
|
|
56
|
+
* async (systemInjector) => {
|
|
57
|
+
* const dataSet = getDataSetFor(systemInjector, MyModel, 'id')
|
|
58
|
+
* await dataSet.add(systemInjector, { value: 'seeded' })
|
|
59
|
+
* },
|
|
60
|
+
* )
|
|
61
|
+
* // systemInjector is disposed here -- all scoped instances cleaned up
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare const useSystemIdentityContext: (options: {
|
|
65
|
+
injector: Injector;
|
|
66
|
+
username?: string;
|
|
67
|
+
}) => Injector;
|
|
68
|
+
//# sourceMappingURL=system-identity-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"system-identity-context.d.ts","sourceRoot":"","sources":["../src/system-identity-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAEjD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AACvD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAE5C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,qBAAsB,SAAQ,eAAe;IACxD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;gBAErB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE;IAK3B,eAAe;IAIf,YAAY,CAAC,GAAG,MAAM,EAAE,MAAM,EAAE;IAIhC,cAAc,CAAC,KAAK,SAAS,IAAI,KAAK,OAAO,CAAC,KAAK,CAAC;CAGrE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,wBAAwB,GAAI,SAAS;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,KAAG,QAK7F,CAAA"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { IdentityContext } from './identity-context.js';
|
|
2
|
+
/**
|
|
3
|
+
* An elevated {@link IdentityContext} that is always authenticated and authorized.
|
|
4
|
+
* Intended for trusted server-side operations such as background jobs, migrations, and seed scripts.
|
|
5
|
+
*
|
|
6
|
+
* **Warning:** This context bypasses **all** authorization checks. Never use it in user-facing
|
|
7
|
+
* request pipelines or any context where untrusted input could reach the DataSet.
|
|
8
|
+
* Prefer {@link useSystemIdentityContext} for scoped usage with automatic cleanup.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { useSystemIdentityContext } from '@furystack/core'
|
|
13
|
+
* import { getDataSetFor } from '@furystack/repository'
|
|
14
|
+
* import { usingAsync } from '@furystack/utils'
|
|
15
|
+
*
|
|
16
|
+
* await usingAsync(
|
|
17
|
+
* useSystemIdentityContext({ injector, username: 'migration-job' }),
|
|
18
|
+
* async (systemInjector) => {
|
|
19
|
+
* const dataSet = getDataSetFor(systemInjector, MyModel, 'id')
|
|
20
|
+
* await dataSet.add(systemInjector, newEntity)
|
|
21
|
+
* },
|
|
22
|
+
* )
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export class SystemIdentityContext extends IdentityContext {
|
|
26
|
+
username;
|
|
27
|
+
constructor(options) {
|
|
28
|
+
super();
|
|
29
|
+
this.username = options?.username ?? 'system';
|
|
30
|
+
}
|
|
31
|
+
isAuthenticated() {
|
|
32
|
+
return Promise.resolve(true);
|
|
33
|
+
}
|
|
34
|
+
isAuthorized(..._roles) {
|
|
35
|
+
return Promise.resolve(true);
|
|
36
|
+
}
|
|
37
|
+
getCurrentUser() {
|
|
38
|
+
return Promise.resolve({ username: this.username, roles: [] });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Creates a scoped child injector with an elevated {@link SystemIdentityContext}.
|
|
43
|
+
* The returned injector is {@link AsyncDisposable} and works with `usingAsync()` for automatic cleanup.
|
|
44
|
+
*
|
|
45
|
+
* **Warning:** The returned injector bypasses **all** authorization checks. Only use this in trusted
|
|
46
|
+
* server-side contexts (background jobs, migrations, seed scripts). Never pass the returned injector
|
|
47
|
+
* to user-facing request handlers.
|
|
48
|
+
*
|
|
49
|
+
* @param options.injector The parent injector to create a child from
|
|
50
|
+
* @param options.username The username for the system identity (defaults to `'system'`)
|
|
51
|
+
* @returns A child injector with the SystemIdentityContext set as the IdentityContext
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* import { useSystemIdentityContext } from '@furystack/core'
|
|
56
|
+
* import { getDataSetFor } from '@furystack/repository'
|
|
57
|
+
* import { usingAsync } from '@furystack/utils'
|
|
58
|
+
*
|
|
59
|
+
* await usingAsync(
|
|
60
|
+
* useSystemIdentityContext({ injector, username: 'seed-script' }),
|
|
61
|
+
* async (systemInjector) => {
|
|
62
|
+
* const dataSet = getDataSetFor(systemInjector, MyModel, 'id')
|
|
63
|
+
* await dataSet.add(systemInjector, { value: 'seeded' })
|
|
64
|
+
* },
|
|
65
|
+
* )
|
|
66
|
+
* // systemInjector is disposed here -- all scoped instances cleaned up
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export const useSystemIdentityContext = (options) => {
|
|
70
|
+
const ctx = new SystemIdentityContext({ username: options.username });
|
|
71
|
+
const childInjector = options.injector.createChild({ owner: 'SystemIdentityContext' });
|
|
72
|
+
childInjector.setExplicitInstance(ctx, IdentityContext);
|
|
73
|
+
return childInjector;
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=system-identity-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"system-identity-context.js","sourceRoot":"","sources":["../src/system-identity-context.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAGvD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,OAAO,qBAAsB,SAAQ,eAAe;IACvC,QAAQ,CAAQ;IAEjC,YAAY,OAA+B;QACzC,KAAK,EAAE,CAAA;QACP,IAAI,CAAC,QAAQ,GAAG,OAAO,EAAE,QAAQ,IAAI,QAAQ,CAAA;IAC/C,CAAC;IAEe,eAAe;QAC7B,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9B,CAAC;IAEe,YAAY,CAAC,GAAG,MAAgB;QAC9C,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9B,CAAC;IAEe,cAAc;QAC5B,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAsB,CAAC,CAAA;IACpF,CAAC;CACF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,OAAkD,EAAY,EAAE;IACvG,MAAM,GAAG,GAAG,IAAI,qBAAqB,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAA;IACrE,MAAM,aAAa,GAAG,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC,CAAA;IACtF,aAAa,CAAC,mBAAmB,CAAC,GAAG,EAAE,eAAe,CAAC,CAAA;IACvD,OAAO,aAAa,CAAA;AACtB,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"system-identity-context.spec.d.ts","sourceRoot":"","sources":["../src/system-identity-context.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Injector } from '@furystack/inject';
|
|
2
|
+
import { usingAsync } from '@furystack/utils';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { getCurrentUser, isAuthenticated, isAuthorized } from './helpers.js';
|
|
5
|
+
import { IdentityContext } from './identity-context.js';
|
|
6
|
+
import { SystemIdentityContext, useSystemIdentityContext } from './system-identity-context.js';
|
|
7
|
+
describe('SystemIdentityContext', () => {
|
|
8
|
+
it('isAuthenticated should return true', async () => {
|
|
9
|
+
const ctx = new SystemIdentityContext();
|
|
10
|
+
expect(await ctx.isAuthenticated()).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
it('isAuthorized should return true without roles', async () => {
|
|
13
|
+
const ctx = new SystemIdentityContext();
|
|
14
|
+
expect(await ctx.isAuthorized()).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
it('isAuthorized should return true with roles', async () => {
|
|
17
|
+
const ctx = new SystemIdentityContext();
|
|
18
|
+
expect(await ctx.isAuthorized('admin', 'superuser')).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
it('getCurrentUser should return default system user', async () => {
|
|
21
|
+
const ctx = new SystemIdentityContext();
|
|
22
|
+
const user = await ctx.getCurrentUser();
|
|
23
|
+
expect(user).toEqual({ username: 'system', roles: [] });
|
|
24
|
+
});
|
|
25
|
+
it('getCurrentUser should respect custom username', async () => {
|
|
26
|
+
const ctx = new SystemIdentityContext({ username: 'migration-job' });
|
|
27
|
+
const user = await ctx.getCurrentUser();
|
|
28
|
+
expect(user).toEqual({ username: 'migration-job', roles: [] });
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe('useSystemIdentityContext', () => {
|
|
32
|
+
it('should return a child injector, not the parent', async () => {
|
|
33
|
+
await usingAsync(new Injector(), async (parent) => {
|
|
34
|
+
const child = useSystemIdentityContext({ injector: parent });
|
|
35
|
+
expect(child).not.toBe(parent);
|
|
36
|
+
await child[Symbol.asyncDispose]();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
it('should resolve IdentityContext to a SystemIdentityContext', async () => {
|
|
40
|
+
await usingAsync(new Injector(), async (parent) => {
|
|
41
|
+
await usingAsync(useSystemIdentityContext({ injector: parent }), async (child) => {
|
|
42
|
+
const ctx = child.getInstance(IdentityContext);
|
|
43
|
+
expect(ctx).toBeInstanceOf(SystemIdentityContext);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
it('should be authenticated and authorized via helpers', async () => {
|
|
48
|
+
await usingAsync(new Injector(), async (parent) => {
|
|
49
|
+
await usingAsync(useSystemIdentityContext({ injector: parent }), async (child) => {
|
|
50
|
+
expect(await isAuthenticated(child)).toBe(true);
|
|
51
|
+
expect(await isAuthorized(child, 'admin')).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
it('should return the configured username via getCurrentUser', async () => {
|
|
56
|
+
await usingAsync(new Injector(), async (parent) => {
|
|
57
|
+
await usingAsync(useSystemIdentityContext({ injector: parent, username: 'seed-script' }), async (child) => {
|
|
58
|
+
const user = await getCurrentUser(child);
|
|
59
|
+
expect(user).toEqual({ username: 'seed-script', roles: [] });
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
it('should use default username when not specified', async () => {
|
|
64
|
+
await usingAsync(new Injector(), async (parent) => {
|
|
65
|
+
await usingAsync(useSystemIdentityContext({ injector: parent }), async (child) => {
|
|
66
|
+
const user = await getCurrentUser(child);
|
|
67
|
+
expect(user.username).toBe('system');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
it('should dispose the child injector after usingAsync completes', async () => {
|
|
72
|
+
await usingAsync(new Injector(), async (parent) => {
|
|
73
|
+
let childRef;
|
|
74
|
+
await usingAsync(useSystemIdentityContext({ injector: parent }), async (child) => {
|
|
75
|
+
childRef = child;
|
|
76
|
+
});
|
|
77
|
+
expect(childRef).toBeDefined();
|
|
78
|
+
expect(() => childRef.getInstance(IdentityContext)).toThrow('Injector already disposed');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
it('should not dispose the parent injector', async () => {
|
|
82
|
+
await usingAsync(new Injector(), async (parent) => {
|
|
83
|
+
await usingAsync(useSystemIdentityContext({ injector: parent }), async () => {
|
|
84
|
+
// no-op
|
|
85
|
+
});
|
|
86
|
+
expect(() => parent.getInstance(IdentityContext)).not.toThrow();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
//# sourceMappingURL=system-identity-context.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"system-identity-context.spec.js","sourceRoot":"","sources":["../src/system-identity-context.spec.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@furystack/core",
|
|
3
|
-
"version": "15.0
|
|
3
|
+
"version": "15.2.0",
|
|
4
4
|
"description": "Core FuryStack package",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"homepage": "https://github.com/furystack/furystack",
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@types/node": "^25.
|
|
49
|
+
"@types/node": "^25.3.0",
|
|
50
50
|
"typescript": "^5.9.3",
|
|
51
51
|
"vitest": "^4.0.18"
|
|
52
52
|
},
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { filterItems } from './filter-items.js'
|
|
3
|
+
import type { FilterType } from './models/physical-store.js'
|
|
4
|
+
|
|
5
|
+
type TestItem = {
|
|
6
|
+
id: number
|
|
7
|
+
name: string
|
|
8
|
+
age: number
|
|
9
|
+
active: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const items: TestItem[] = [
|
|
13
|
+
{ id: 1, name: 'Alice', age: 30, active: true },
|
|
14
|
+
{ id: 2, name: 'Bob', age: 25, active: false },
|
|
15
|
+
{ id: 3, name: 'Charlie', age: 35, active: true },
|
|
16
|
+
{ id: 4, name: 'Diana', age: 28, active: false },
|
|
17
|
+
{ id: 5, name: 'Eve', age: 22, active: true },
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
describe('filterItems', () => {
|
|
21
|
+
describe('no filter', () => {
|
|
22
|
+
it('should return all items when filter is undefined', () => {
|
|
23
|
+
expect(filterItems(items, undefined)).toEqual(items)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should return all items when filter is not provided', () => {
|
|
27
|
+
expect(filterItems(items)).toEqual(items)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should return an empty array when input is empty', () => {
|
|
31
|
+
expect(filterItems<TestItem>([], { name: { $eq: 'Alice' } })).toEqual([])
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should return all items when filter is an empty object', () => {
|
|
35
|
+
expect(filterItems(items, {} as FilterType<TestItem>)).toEqual(items)
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('$eq', () => {
|
|
40
|
+
it('should return items matching the exact value', () => {
|
|
41
|
+
const result = filterItems(items, { name: { $eq: 'Alice' } })
|
|
42
|
+
expect(result).toEqual([items[0]])
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should return empty array when no items match', () => {
|
|
46
|
+
const result = filterItems(items, { name: { $eq: 'Nobody' } })
|
|
47
|
+
expect(result).toEqual([])
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('$ne', () => {
|
|
52
|
+
it('should return items not matching the value', () => {
|
|
53
|
+
const result = filterItems(items, { name: { $ne: 'Alice' } })
|
|
54
|
+
expect(result).toEqual([items[1], items[2], items[3], items[4]])
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('$in', () => {
|
|
59
|
+
it('should return items whose value is in the array', () => {
|
|
60
|
+
const result = filterItems(items, { name: { $in: ['Alice', 'Bob'] } })
|
|
61
|
+
expect(result).toEqual([items[0], items[1]])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should return empty array when no values match', () => {
|
|
65
|
+
const result = filterItems(items, { name: { $in: ['Nobody'] } })
|
|
66
|
+
expect(result).toEqual([])
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('$nin', () => {
|
|
71
|
+
it('should return items whose value is not in the array', () => {
|
|
72
|
+
const result = filterItems(items, { name: { $nin: ['Alice', 'Bob'] } })
|
|
73
|
+
expect(result).toEqual([items[2], items[3], items[4]])
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('$gt', () => {
|
|
78
|
+
it('should return items with value greater than the threshold', () => {
|
|
79
|
+
const result = filterItems(items, { age: { $gt: 30 } })
|
|
80
|
+
expect(result).toEqual([items[2]])
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should exclude items equal to the threshold', () => {
|
|
84
|
+
const result = filterItems(items, { age: { $gt: 35 } })
|
|
85
|
+
expect(result).toEqual([])
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('$gte', () => {
|
|
90
|
+
it('should return items with value greater than or equal to the threshold', () => {
|
|
91
|
+
const result = filterItems(items, { age: { $gte: 30 } })
|
|
92
|
+
expect(result).toEqual([items[0], items[2]])
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('$lt', () => {
|
|
97
|
+
it('should return items with value less than the threshold', () => {
|
|
98
|
+
const result = filterItems(items, { age: { $lt: 25 } })
|
|
99
|
+
expect(result).toEqual([items[4]])
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('should exclude items equal to the threshold', () => {
|
|
103
|
+
const result = filterItems(items, { age: { $lt: 22 } })
|
|
104
|
+
expect(result).toEqual([])
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
describe('$lte', () => {
|
|
109
|
+
it('should return items with value less than or equal to the threshold', () => {
|
|
110
|
+
const result = filterItems(items, { age: { $lte: 25 } })
|
|
111
|
+
expect(result).toEqual([items[1], items[4]])
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('$startsWith', () => {
|
|
116
|
+
it('should return items whose string field starts with the value', () => {
|
|
117
|
+
const result = filterItems(items, { name: { $startsWith: 'Al' } })
|
|
118
|
+
expect(result).toEqual([items[0]])
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('should return empty array when no items match', () => {
|
|
122
|
+
const result = filterItems(items, { name: { $startsWith: 'Zz' } })
|
|
123
|
+
expect(result).toEqual([])
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('$endsWith', () => {
|
|
128
|
+
it('should return items whose string field ends with the value', () => {
|
|
129
|
+
const result = filterItems(items, { name: { $endsWith: 'ce' } })
|
|
130
|
+
expect(result).toEqual([items[0]])
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('$like', () => {
|
|
135
|
+
it('should match with % wildcards', () => {
|
|
136
|
+
const result = filterItems(items, { name: { $like: 'A%' } })
|
|
137
|
+
expect(result).toEqual([items[0]])
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('should match with % in the middle', () => {
|
|
141
|
+
const result = filterItems(items, { name: { $like: 'A%e' } })
|
|
142
|
+
expect(result).toEqual([items[0]])
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('should be case-insensitive', () => {
|
|
146
|
+
const result = filterItems(items, { name: { $like: 'alice' } })
|
|
147
|
+
expect(result).toEqual([items[0]])
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('should escape regex metacharacters in the pattern', () => {
|
|
151
|
+
type Item = { value: string }
|
|
152
|
+
const testItems: Item[] = [{ value: 'foo.bar' }, { value: 'fooXbar' }]
|
|
153
|
+
const result = filterItems(testItems, { value: { $like: 'foo.bar' } } as FilterType<Item>)
|
|
154
|
+
expect(result).toEqual([{ value: 'foo.bar' }])
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe('$regex', () => {
|
|
159
|
+
it('should return items matching the regex', () => {
|
|
160
|
+
const result = filterItems(items, { name: { $regex: '^[A-C]' } })
|
|
161
|
+
expect(result).toEqual([items[0], items[1], items[2]])
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('should return empty array when no items match', () => {
|
|
165
|
+
const result = filterItems(items, { name: { $regex: '^Z' } })
|
|
166
|
+
expect(result).toEqual([])
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('should throw on invalid regex syntax', () => {
|
|
170
|
+
expect(() => filterItems(items, { name: { $regex: '[invalid' } })).toThrow()
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
describe('$and', () => {
|
|
175
|
+
it('should return items matching all conditions', () => {
|
|
176
|
+
const result = filterItems(items, { $and: [{ age: { $gte: 25 } }, { active: { $eq: true } }] })
|
|
177
|
+
expect(result).toEqual([items[0], items[2]])
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('should return empty array when conditions are mutually exclusive', () => {
|
|
181
|
+
const result = filterItems(items, { $and: [{ name: { $eq: 'Alice' } }, { name: { $eq: 'Bob' } }] })
|
|
182
|
+
expect(result).toEqual([])
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
describe('$or', () => {
|
|
187
|
+
it('should return items matching any condition', () => {
|
|
188
|
+
const result = filterItems(items, { $or: [{ name: { $eq: 'Alice' } }, { name: { $eq: 'Bob' } }] })
|
|
189
|
+
expect(result).toEqual([items[0], items[1]])
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('should return empty array when no conditions match', () => {
|
|
193
|
+
const result = filterItems(items, { $or: [{ name: { $eq: 'Nobody' } }, { name: { $eq: 'Nope' } }] })
|
|
194
|
+
expect(result).toEqual([])
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
describe('combined field-level filters', () => {
|
|
199
|
+
it('should AND multiple field filters together', () => {
|
|
200
|
+
const result = filterItems(items, { age: { $gte: 25 }, active: { $eq: true } })
|
|
201
|
+
expect(result).toEqual([items[0], items[2]])
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
describe('error cases', () => {
|
|
206
|
+
it('should throw when a field filter is not an object', () => {
|
|
207
|
+
const badFilter = { name: 'Alice' } as unknown as FilterType<TestItem>
|
|
208
|
+
expect(() => filterItems(items, badFilter)).toThrow("The filter has to be an object, got string for field 'name'")
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('should throw for an unsupported filter operation', () => {
|
|
212
|
+
const badFilter = { name: { $unknown: 'x' } } as unknown as FilterType<TestItem>
|
|
213
|
+
expect(() => filterItems(items, badFilter)).toThrow("The filter key '$unknown' is not a valid operation")
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
})
|