@smartsoft001/crud-shell-nestjs 2.76.0 → 2.80.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.
@@ -1,32 +0,0 @@
1
- import * as queryToMongo from './query-to-mongo';
2
-
3
- describe('crud-nestjs: query-to-mongo', () => {
4
- describe('q2m', () => {
5
- it('should convert query string to mongo object', () => {
6
- const result = queryToMongo.q2m('a=1&b=2');
7
- expect(result).toHaveProperty('criteria');
8
- });
9
- it('should convert query object to mongo object', () => {
10
- const result = queryToMongo.q2m({ a: '1', b: '2' });
11
- expect(result).toHaveProperty('criteria');
12
- });
13
- it('should provide links function', () => {
14
- const result = queryToMongo.q2m({ a: '1', limit: '2', offset: '0' });
15
- expect(typeof result.links).toBe('function');
16
- });
17
- it('links should return null if no limit', () => {
18
- const result = queryToMongo.q2m({ a: '1' });
19
- expect(result.links('url', 10)).toBeNull();
20
- });
21
- it('links should return prev and first if offset > 0', () => {
22
- const result = queryToMongo.q2m({ a: '1', limit: '2', offset: '2' });
23
- const links = result.links('url', 10);
24
- expect(links).toHaveProperty('prev');
25
- });
26
- it('links should return next and last if more pages', () => {
27
- const result = queryToMongo.q2m({ a: '1', limit: '2', offset: '0' });
28
- const links = result.links('url', 10);
29
- expect(links).toHaveProperty('next');
30
- });
31
- });
32
- });
@@ -1,295 +0,0 @@
1
- import * as querystring from 'querystring';
2
- const iso8601 =
3
- /^\d{4}(-(0[1-9]|1[0-2])(-(0[1-9]|[12][0-9]|3[01]))?)?(T([01][0-9]|2[0-3]):[0-5]\d(:[0-5]\d(\.\d+)?)?(Z|[+-]\d{2}:\d{2}))?$/;
4
-
5
- // Convert comma separated list to a mongo projection.
6
- // for example f('field1,field2,field3') -> {field1:true,field2:true,field3:true}
7
- function fieldsToMongo(fields) {
8
- if (!fields) return null;
9
- const hash = {};
10
- fields.split(',').forEach(function (field) {
11
- hash[field.trim()] = 1;
12
- });
13
- return hash;
14
- }
15
-
16
- function convertRegex(val: string): string {
17
- return val.toString().replace(/\*/g, '[*]');
18
- }
19
-
20
- // Convert comma separated list to a mongo projection which specifies fields to omit.
21
- // for example f('field2') -> {field2:false}
22
- function omitFieldsToMongo(omitFields) {
23
- if (!omitFields) return null;
24
- const hash = {};
25
- omitFields.split(',').forEach(function (omitField) {
26
- hash[omitField.trim()] = 0;
27
- });
28
- return hash;
29
- }
30
-
31
- // Convert comma separated list to mongo sort options.
32
- // for example f('field1,+field2,-field3') -> {field1:1,field2:1,field3:-1}
33
- function sortToMongo(sort) {
34
- if (!sort) return null;
35
- const hash = {};
36
- let c;
37
- sort.split(',').forEach(function (field) {
38
- c = field.charAt(0);
39
- if (c === '-') field = field.substr(1);
40
- hash[field.trim()] = c === '-' ? -1 : 1;
41
- });
42
- return hash;
43
- }
44
-
45
- // Convert String to Number, Date, or Boolean if possible. Also strips ! prefix
46
- function typedValue(value) {
47
- if (value[0] === '!') value = value.substr(1);
48
- const regex = value.match(/^\/(.*)\/(i?)$/);
49
- const quotedString = value.match(/(["'])(?:\\\1|.)*?\1/);
50
-
51
- if (regex) {
52
- return new RegExp(regex[1], regex[2]);
53
- } else if (quotedString) {
54
- return quotedString[0].substr(1, quotedString[0].length - 2);
55
- } else if (value === 'true') {
56
- return true;
57
- } else if (value === 'false') {
58
- return false;
59
- } else if (iso8601.test(value) && value.length !== 4 && value.length !== 10) {
60
- return new Date(value);
61
- } else if (!isNaN(Number(value))) {
62
- return Number(value);
63
- }
64
-
65
- return value;
66
- }
67
-
68
- // Convert a comma separated string value to an array of values. Commas
69
- // in a quoted strings and regexes are ignored. Also strips ! prefix from values.
70
- function typedValues(svalue) {
71
- const commaSplit = /("[^"]*")|('[^']*')|(\/[^/]*\/i?)|([^,]+)/g;
72
- const values = [];
73
- svalue.match(commaSplit).forEach(function (value) {
74
- values.push(typedValue(value));
75
- });
76
- return values;
77
- }
78
-
79
- // Convert a key/value pair split at an equals sign into a mongo comparison.
80
- // Converts value Strings to Numbers or Booleans when possible.
81
- // for example:
82
- // + f('key','value') => {key:'key',value:'value'}
83
- // + f('key>','value') => {key:'key',value:{$gte:'value'}}
84
- // + f('key') => {key:'key',value:{$exists: true}}
85
- // + f('!key') => {key:'key',value:{$exists: false}}
86
- // + f('key:op','value') => {key: 'key', value:{ $op: value}}
87
- // + f('key','op:value') => {key: 'key', value:{ $op: value}}
88
- function comparisonToMongo(key, value) {
89
- const join = value === '' ? key : key.concat('=', value);
90
- const parts = join.match(/^(!?[^><~!=:]+)(?:=?([><]=?|~?=|!?=|:.+=)(.+))?$/);
91
- let op;
92
- const hash = {} as any;
93
- if (!parts) return null;
94
-
95
- key = parts[1];
96
- op = parts[2];
97
-
98
- if (!op) {
99
- if (key[0] !== '!') value = { $exists: true };
100
- else {
101
- key = key.substr(1);
102
- value = { $exists: false };
103
- }
104
- } else if (op === '=' && parts[3] === '!') {
105
- value = { $exists: false };
106
- } else if (op === '=' || op === '!=') {
107
- if (op === '=' && parts[3][0] === '!') op = '!=';
108
- // tslint:disable-next-line:no-shadowed-variable
109
- const array = typedValues(parts[3]);
110
- if (array.length > 1) {
111
- value = {};
112
- op = op === '=' ? '$in' : '$nin';
113
- value[op] = array;
114
- } else if (op === '!=') {
115
- value =
116
- array[0] instanceof RegExp ? { $not: array[0] } : { $ne: array[0] };
117
- } else if (array[0][0] === '!') {
118
- const sValue = array[0].substr(1);
119
- const regex = sValue.match(/^\/(.*)\/(i?)$/);
120
- value = regex
121
- ? { $not: new RegExp(regex[1], regex[2]) }
122
- : { $ne: sValue };
123
- } else {
124
- value = array[0];
125
- }
126
- } else if (op[0] === ':' && op[op.length - 1] === '=') {
127
- op = '$' + op.substr(1, op.length - 2);
128
- const array = [];
129
- // tslint:disable-next-line:no-shadowed-variable
130
- parts[3].split(',').forEach(function (value) {
131
- array.push(typedValue(value));
132
- });
133
- value = {};
134
- value[op] = array.length === 1 ? array[0] : array;
135
- } else {
136
- value = typedValue(parts[3]);
137
- if (op === '>') value = { $gt: value };
138
- else if (op === '>=') value = { $gte: value };
139
- else if (op === '<') value = { $lt: value };
140
- else if (op === '<=') value = { $lte: value };
141
- else if (op === '~=')
142
- value = {
143
- $regex: value ? convertRegex(value) : '',
144
- $options: 'i',
145
- };
146
- }
147
-
148
- hash.key = key;
149
- hash.value = value;
150
- return hash;
151
- }
152
-
153
- // Checks for keys that are ordinal positions, such as {'0':'one','1':'two','2':'three'}
154
- function hasOrdinalKeys(obj) {
155
- let c = 0;
156
- for (const key in obj) {
157
- if (Number(key) !== c++) return false;
158
- }
159
- return true;
160
- }
161
-
162
- // Convert query parameters to a mongo query criteria.
163
- // for example {field1:"red","field2>2":""} becomes {field1:"red",field2:{$gt:2}}
164
- function queryCriteriaToMongo(query, options = null) {
165
- const hash = {};
166
- let deep, p;
167
- options = options || {};
168
-
169
- for (const key in query) {
170
- if (
171
- Object.prototype.hasOwnProperty.call(query, key) &&
172
- (!options.ignore || options.ignore.indexOf(key) === -1)
173
- ) {
174
- deep = typeof query[key] === 'object' && !hasOrdinalKeys(query[key]);
175
-
176
- if (deep) {
177
- p = {
178
- key: key,
179
- value: queryCriteriaToMongo(query[key]),
180
- };
181
- } else {
182
- p = comparisonToMongo(key, query[key]);
183
- }
184
-
185
- if (p) {
186
- if (!hash[p.key]) {
187
- hash[p.key] = p.value;
188
- } else if (typeof p.value === 'string') {
189
- hash[p.key] = Object.assign(hash[p.key], {
190
- $eq: p.value,
191
- });
192
- } else {
193
- hash[p.key] = Object.assign(hash[p.key], p.value);
194
- }
195
- }
196
- }
197
- }
198
- return hash;
199
- }
200
-
201
- // Convert query parameters to a mongo query options.
202
- // for example {fields:'a,b',offset:8,limit:16} becomes {fields:{a:true,b:true},skip:8,limit:16}
203
- function queryOptionsToMongo(query, options) {
204
- const hash = {} as any,
205
- fields = fieldsToMongo(query[options.keywords.fields]),
206
- omitFields = omitFieldsToMongo(query[options.keywords.omit]),
207
- sort = sortToMongo(query[options.keywords.sort]),
208
- maxLimit = options.maxLimit || 9007199254740992;
209
-
210
- let limit = options.maxLimit || 0;
211
-
212
- if (fields) hash.fields = fields;
213
- // omit intentionally overwrites fields if both have been specified in the query
214
- // mongo does not accept mixed true/fals field specifiers for projections
215
- if (omitFields) hash.fields = omitFields;
216
- if (sort) hash.sort = sort;
217
-
218
- if (query[options.keywords.offset])
219
- hash.skip = Number(query[options.keywords.offset]);
220
- if (query[options.keywords.limit])
221
- limit = Math.min(Number(query[options.keywords.limit]), maxLimit);
222
- if (limit) {
223
- hash.limit = limit;
224
- } else if (options.maxLimit) {
225
- hash.limit = maxLimit;
226
- }
227
-
228
- return hash;
229
- }
230
-
231
- export function q2m(query = null, options = null) {
232
- query = query || {};
233
- options = options || {};
234
- options.keywords = options.keywords || {};
235
-
236
- const defaultKeywords = {
237
- fields: 'fields',
238
- omit: 'omit',
239
- sort: 'sort',
240
- offset: 'offset',
241
- limit: 'limit',
242
- };
243
- options.keywords = Object.assign(defaultKeywords, options.keywords);
244
- const ignoreKeywords = [
245
- options.keywords.fields,
246
- options.keywords.omit,
247
- options.keywords.sort,
248
- options.keywords.offset,
249
- options.keywords.limit,
250
- ];
251
-
252
- if (!options.ignore) {
253
- options.ignore = [];
254
- } else {
255
- options.ignore =
256
- typeof options.ignore === 'string' ? [options.ignore] : options.ignore;
257
- }
258
- options.ignore = options.ignore.concat(ignoreKeywords);
259
- if (!options.parser) options.parser = querystring;
260
-
261
- if (typeof query === 'string') query = options.parser.parse(query);
262
-
263
- return {
264
- criteria: queryCriteriaToMongo(query, options),
265
- options: queryOptionsToMongo(query, options),
266
-
267
- links: function (url, totalCount) {
268
- const offset = this.options.skip || 0;
269
- const limit = Math.min(this.options.limit || 0, totalCount);
270
- const links = {};
271
- const last = {} as any;
272
-
273
- if (!limit) return null;
274
-
275
- options = options || {};
276
-
277
- if (offset > 0) {
278
- query[options.keywords.offset] = Math.max(offset - limit, 0);
279
- links['prev'] = url + '?' + options.parser.stringify(query);
280
- query[options.keywords.offset] = 0;
281
- links['first'] = url + '?' + options.parser.stringify(query);
282
- }
283
- if (offset + limit < totalCount) {
284
- last.pages = Math.ceil(totalCount / limit);
285
- last.offset = (last.pages - 1) * limit;
286
-
287
- query[options.keywords.offset] = Math.min(offset + limit, last.offset);
288
- links['next'] = url + '?' + options.parser.stringify(query);
289
- query[options.keywords.offset] = last.offset;
290
- links['last'] = url + '?' + options.parser.stringify(query);
291
- }
292
- return links;
293
- },
294
- };
295
- }
@@ -1,122 +0,0 @@
1
- import { of } from 'rxjs';
2
-
3
- import { CrudService } from '@smartsoft001/crud-shell-app-services';
4
-
5
- import { CrudGateway } from './crud.gateway';
6
-
7
- describe('crud-nestjs: CrudGateway', () => {
8
- let service: jest.Mocked<CrudService<any>>;
9
- let gateway: CrudGateway<any>;
10
- let client: any;
11
-
12
- beforeEach(() => {
13
- service = {
14
- changes: jest.fn(),
15
- } as any;
16
- gateway = new CrudGateway(service);
17
- gateway['afterInit']({});
18
- client = { id: 'client1' };
19
- });
20
-
21
- describe('afterInit', () => {
22
- it('should initialize _clientsSubscriptions', () => {
23
- const gw = new CrudGateway(service);
24
- gw['afterInit']({});
25
- expect(gw['_clientsSubscriptions']).toBeInstanceOf(Map);
26
- });
27
- });
28
-
29
- describe('handleConnection', () => {
30
- it('should log client connection', () => {
31
- client.id = 'abc';
32
- const logSpy = jest.spyOn(console, 'log').mockImplementation();
33
- gateway.handleConnection(client);
34
- expect(logSpy).toHaveBeenCalledWith('Client connected: abc');
35
- logSpy.mockRestore();
36
- });
37
- });
38
-
39
- describe('handleDisconnect', () => {
40
- it('should clear subscription and log disconnect', () => {
41
- const logSpy = jest.spyOn(console, 'log').mockImplementation();
42
- const unsub = jest.fn();
43
- gateway['_clientsSubscriptions'].set(client.id, {
44
- unsubscribe: unsub,
45
- } as any);
46
- gateway.handleDisconnect(client);
47
- expect(unsub).toHaveBeenCalled();
48
- logSpy.mockRestore();
49
- });
50
- });
51
-
52
- describe('clearSubscription', () => {
53
- it('should unsubscribe and delete if present', () => {
54
- const unsub = jest.fn();
55
- gateway['_clientsSubscriptions'].set(client.id, {
56
- unsubscribe: unsub,
57
- } as any);
58
- gateway['clearSubscription'](client);
59
- expect(gateway['_clientsSubscriptions'].has(client.id)).toBe(false);
60
- });
61
- it('should do nothing if not present', () => {
62
- expect(() => gateway['clearSubscription'](client)).not.toThrow();
63
- });
64
- });
65
-
66
- describe('handleFilter', () => {
67
- //TODO: testowanie observable
68
- // it('should subscribe to service.changes and emit events', done => {
69
- // const res: ItemChangedData = { type: 'update', id: '1', data: {
70
- // removedFields: [],
71
- // updatedFields: {}
72
- // } };
73
- // service.changes.mockReturnValue(of(res));
74
- // const observer = gateway.handleFilter({}, client).subscribe({
75
- // next: (msg) => {
76
- // expect(msg).toEqual({ event: 'changes', data: res });
77
- // observer.unsubscribe();
78
- // done();
79
- // }
80
- // });
81
- // });
82
- //
83
- // it('should handle errors from service.changes', done => {
84
- // service.changes.mockReturnValue(throwError(() => new Error('fail')));
85
- // const observer = gateway.handleFilter({}, client).subscribe({
86
- // error: (err) => {
87
- // expect(err).toBeInstanceOf(Error);
88
- // observer.unsubscribe();
89
- // done();
90
- // }
91
- // });
92
- // });
93
-
94
- it('should clear previous subscription before subscribing', () => {
95
- const unsub = jest.fn();
96
- gateway['_clientsSubscriptions'].set(client.id, {
97
- unsubscribe: unsub,
98
- } as any);
99
- service.changes.mockReturnValue(
100
- of({
101
- type: 'update',
102
- id: '1',
103
- data: { removedFields: [], updatedFields: {} },
104
- }),
105
- );
106
- gateway.handleFilter({}, client).subscribe().unsubscribe();
107
- expect(unsub).toHaveBeenCalled();
108
- });
109
-
110
- it('should store new subscription in _clientsSubscriptions', () => {
111
- service.changes.mockReturnValue(
112
- of({
113
- type: 'update',
114
- id: '1',
115
- data: { removedFields: [], updatedFields: {} },
116
- }),
117
- );
118
- gateway.handleFilter({}, client).subscribe().unsubscribe();
119
- expect(gateway['_clientsSubscriptions'].has(client.id)).toBe(true);
120
- });
121
- });
122
- });
@@ -1,73 +0,0 @@
1
- import {
2
- ConnectedSocket,
3
- MessageBody,
4
- OnGatewayConnection,
5
- OnGatewayDisconnect,
6
- OnGatewayInit,
7
- SubscribeMessage,
8
- WebSocketGateway,
9
- WsResponse,
10
- } from '@nestjs/websockets';
11
- import { Observable, Subscription } from 'rxjs';
12
- import { Socket } from 'socket.io';
13
-
14
- import { CrudService } from '@smartsoft001/crud-shell-app-services';
15
- import { ItemChangedData } from '@smartsoft001/crud-shell-dtos';
16
- import { IEntity } from '@smartsoft001/domain-core';
17
-
18
- @WebSocketGateway({
19
- transports: ['websocket'],
20
- path: '/' + process.env.URL_PREFIX + '/_socket',
21
- namespace: '/' + process.env.URL_PREFIX,
22
- })
23
- export class CrudGateway<T extends IEntity<string>>
24
- implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
25
- {
26
- private _clientsSubscriptions: Map<string, Subscription>;
27
-
28
- constructor(private service: CrudService<T>) {}
29
-
30
- @SubscribeMessage('changes')
31
- handleFilter(
32
- @MessageBody() data: { id?: string },
33
- @ConnectedSocket() client: Socket,
34
- ): Observable<WsResponse<ItemChangedData>> {
35
- const event = 'changes';
36
-
37
- return new Observable<WsResponse<ItemChangedData>>((observer) => {
38
- this.clearSubscription(client);
39
-
40
- this._clientsSubscriptions.set(
41
- client.id,
42
- this.service.changes(data).subscribe(
43
- (res) => {
44
- observer.next({ event, data: res });
45
- },
46
- (error) => observer.error(error),
47
- ),
48
- );
49
- });
50
- }
51
-
52
- afterInit(server: any) {
53
- this._clientsSubscriptions = new Map<string, Subscription>();
54
- console.log('CrudGateway Init');
55
- }
56
-
57
- handleDisconnect(client: any) {
58
- this.clearSubscription(client);
59
-
60
- console.log(`Client disconnected: ${client.id}`);
61
- }
62
-
63
- private clearSubscription(client: any) {
64
- if (this._clientsSubscriptions.has(client.id)) {
65
- this._clientsSubscriptions.get(client.id).unsubscribe();
66
- this._clientsSubscriptions.delete(client.id);
67
- }
68
- }
69
-
70
- handleConnection(client: any, ...args: any[]) {
71
- console.log(`Client connected: ${client.id}`);
72
- }
73
- }
@@ -1,3 +0,0 @@
1
- import { CrudGateway } from './crud/crud.gateway';
2
-
3
- export const GATEWAYS = [CrudGateway];
@@ -1,61 +0,0 @@
1
- import { UnauthorizedException, Logger } from '@nestjs/common';
2
-
3
- import { AuthJwtGuard, AuthOrAnonymousJwtGuard } from './auth.guard';
4
-
5
- describe('crud-nestjs: AuthJwtGuard', () => {
6
- let guard: AuthJwtGuard;
7
- let loggerWarnSpy: jest.SpyInstance;
8
-
9
- beforeEach(() => {
10
- guard = new AuthJwtGuard();
11
- loggerWarnSpy = jest.spyOn(Logger.prototype, 'warn').mockImplementation();
12
- });
13
-
14
- afterEach(() => {
15
- loggerWarnSpy.mockRestore();
16
- });
17
-
18
- it('should return user if no error and user exists', () => {
19
- const user = { id: 1 };
20
- expect(guard.handleRequest(null, user, null)).toBe(user);
21
- });
22
-
23
- it('should throw UnauthorizedException if no user and no error', () => {
24
- expect(() => guard.handleRequest(null, null, { msg: 'no user' })).toThrow(
25
- UnauthorizedException,
26
- );
27
- });
28
-
29
- it('should throw error if error is present', () => {
30
- const error = new Error('fail');
31
- expect(() => guard.handleRequest(error, null, { msg: 'err' })).toThrow(
32
- error,
33
- );
34
- });
35
-
36
- it('should log info if no user', () => {
37
- try {
38
- guard.handleRequest(null, null, { msg: 'info' });
39
- } catch {
40
- // Intentionally left empty: exception is expected and tested below
41
- }
42
- expect(loggerWarnSpy).toHaveBeenCalledWith(JSON.stringify({ msg: 'info' }));
43
- });
44
- });
45
-
46
- describe('crud-nestjs: AuthOrAnonymousJwtGuard', () => {
47
- let guard: AuthOrAnonymousJwtGuard;
48
-
49
- beforeEach(() => {
50
- guard = new AuthOrAnonymousJwtGuard();
51
- });
52
-
53
- it('should return user if user exists', () => {
54
- const user = { id: 2 };
55
- expect(guard.handleRequest(null, user, null)).toBe(user);
56
- });
57
-
58
- it('should return undefined if user is undefined', () => {
59
- expect(guard.handleRequest(null, undefined, null)).toBeUndefined();
60
- });
61
- });
@@ -1,22 +0,0 @@
1
- import { Injectable, UnauthorizedException, Logger } from '@nestjs/common';
2
- import { AuthGuard } from '@nestjs/passport';
3
-
4
- @Injectable()
5
- export class AuthJwtGuard extends AuthGuard('jwt') {
6
- private readonly logger = new Logger(AuthJwtGuard.name, { timestamp: true });
7
-
8
- handleRequest(err: any, user: any, info: any): any {
9
- if (err || !user) {
10
- this.logger.warn(JSON.stringify(info));
11
- throw err || new UnauthorizedException();
12
- }
13
- return user;
14
- }
15
- }
16
-
17
- @Injectable()
18
- export class AuthOrAnonymousJwtGuard extends AuthGuard('jwt') {
19
- handleRequest(err: any, user: any, info: any): any {
20
- return user;
21
- }
22
- }