@qelos/sdk 3.11.10 → 4.0.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/README.md +31 -0
- package/dist/__tests__/blueprints.types.test-d.d.ts +28 -0
- package/dist/__tests__/blueprints.types.test-d.js +11 -0
- package/dist/__tests__/blueprints.types.test-d.js.map +1 -0
- package/dist/administrator/events.d.ts +5 -0
- package/dist/administrator/events.js +8 -0
- package/dist/administrator/events.js.map +1 -1
- package/dist/administrator/workspaces.d.ts +8 -1
- package/dist/administrator/workspaces.js +7 -2
- package/dist/administrator/workspaces.js.map +1 -1
- package/dist/ai/agents.d.ts +56 -0
- package/dist/ai/agents.js +116 -0
- package/dist/ai/agents.js.map +1 -0
- package/dist/ai/index.d.ts +9 -3
- package/dist/ai/index.js +8 -1
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/types.d.ts +61 -0
- package/dist/authentication.d.ts +64 -1
- package/dist/authentication.js +137 -0
- package/dist/authentication.js.map +1 -1
- package/dist/base-sdk.d.ts +3 -0
- package/dist/base-sdk.js +16 -2
- package/dist/base-sdk.js.map +1 -1
- package/dist/blueprints-entities.d.ts +26 -0
- package/dist/blueprints-entities.js +91 -5
- package/dist/blueprints-entities.js.map +1 -1
- package/dist/blueprints.d.ts +3 -0
- package/dist/blueprints.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/workspaces.d.ts +16 -0
- package/dist/workspaces.js +40 -0
- package/dist/workspaces.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/blueprints.types.test-d.ts +49 -0
- package/src/administrator/events.ts +6 -1
- package/src/administrator/workspaces.ts +10 -2
- package/src/ai/agents.ts +171 -0
- package/src/ai/index.ts +22 -4
- package/src/ai/types.ts +66 -0
- package/src/authentication.ts +181 -1
- package/src/base-sdk.ts +16 -2
- package/src/blueprints-entities.ts +113 -5
- package/src/blueprints.ts +9 -2
- package/src/index.ts +26 -1
- package/src/workspaces.ts +63 -0
- package/test/authentication-social.test.ts +48 -0
- package/test/base-sdk.test.ts +26 -1
- package/test/index.test.ts +2 -1
- package/test/qelos-sdk-refresh.test.ts +2 -1
- package/test/workspaces.test.ts +170 -0
|
@@ -12,6 +12,68 @@ export interface IBaseBlueprintEntity {
|
|
|
12
12
|
metadata: any;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
// Blueprint entity responses default to the flat shape. Pass $flat: false (or 0)
|
|
16
|
+
// to opt back into the wrapped shape; this also keeps SDK behavior consistent
|
|
17
|
+
// against older servers that defaulted to wrapped.
|
|
18
|
+
function withFlatDefault<Q extends Record<string, any> | undefined>(query?: Q): Record<string, any> {
|
|
19
|
+
if (query && '$flat' in query) {
|
|
20
|
+
return query as Record<string, any>;
|
|
21
|
+
}
|
|
22
|
+
return { ...(query || {}), $flat: true };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type BlueprintEntityFilters = Record<string, any>;
|
|
26
|
+
|
|
27
|
+
export class QlBlueprintEntitiesQuery<T = any> {
|
|
28
|
+
#filters: BlueprintEntityFilters = {};
|
|
29
|
+
#options: Record<string, any> = {};
|
|
30
|
+
|
|
31
|
+
constructor(private entities: QlBlueprintEntities<T>) {}
|
|
32
|
+
|
|
33
|
+
where(filters: BlueprintEntityFilters): this {
|
|
34
|
+
Object.assign(this.#filters, filters);
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
limit(value: number): this {
|
|
39
|
+
this.#options.$limit = value;
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
skip(value: number): this {
|
|
44
|
+
this.#options.$skip = value;
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
sort(value: string): this {
|
|
49
|
+
this.#options.$sort = value;
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
select(fields: string | string[]): this {
|
|
54
|
+
this.#options.$select = Array.isArray(fields) ? fields.join(',') : fields;
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
toQuery(): ICommonQueryFilters & BlueprintEntityFilters {
|
|
59
|
+
return { ...this.#filters, ...this.#options };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
find(extra?: RequestExtra): Promise<(IBaseBlueprintEntity & T)[]> {
|
|
63
|
+
return this.entities.getList(this.toQuery(), extra);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
findOne(extra?: RequestExtra): Promise<(IBaseBlueprintEntity & T) | undefined> {
|
|
67
|
+
return this.entities
|
|
68
|
+
.getList({ ...this.toQuery(), $limit: 1 }, extra)
|
|
69
|
+
.then((results) => results?.[0]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
count(extra?: RequestExtra): Promise<number> {
|
|
73
|
+
return this.entities.count(this.toQuery(), extra);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
15
77
|
export default class QlBlueprintEntities<T = any> extends BaseSDK {
|
|
16
78
|
private relativePath = '/api/blueprints';
|
|
17
79
|
|
|
@@ -20,20 +82,20 @@ export default class QlBlueprintEntities<T = any> extends BaseSDK {
|
|
|
20
82
|
}
|
|
21
83
|
|
|
22
84
|
getEntity(identifier: string, extra?: RequestExtra) {
|
|
23
|
-
return this.callJsonApi<IBaseBlueprintEntity & T>(`${this.relativePath}/${this.blueprintKey}/entities/${identifier}${this.getQueryParams(extra?.query)}`, extra)
|
|
85
|
+
return this.callJsonApi<IBaseBlueprintEntity & T>(`${this.relativePath}/${this.blueprintKey}/entities/${identifier}${this.getQueryParams(withFlatDefault(extra?.query))}`, extra)
|
|
24
86
|
}
|
|
25
87
|
|
|
26
88
|
getList(query?: ICommonQueryFilters & Record<string, any>, extra?: RequestExtra) {
|
|
27
|
-
return this.callJsonApi<(IBaseBlueprintEntity & T)[]>(`${this.relativePath}/${this.blueprintKey}/entities${this.getQueryParams(query)}`, extra);
|
|
89
|
+
return this.callJsonApi<(IBaseBlueprintEntity & T)[]>(`${this.relativePath}/${this.blueprintKey}/entities${this.getQueryParams(withFlatDefault(query))}`, extra);
|
|
28
90
|
}
|
|
29
91
|
|
|
30
92
|
remove(identifier: string, extra?: RequestExtra): Promise<any> {
|
|
31
|
-
return this.callApi(`${this.relativePath}/${this.blueprintKey}/entities/${identifier}${this.getQueryParams(extra?.query)}`, { method: 'delete' });
|
|
93
|
+
return this.callApi(`${this.relativePath}/${this.blueprintKey}/entities/${identifier}${this.getQueryParams(withFlatDefault(extra?.query))}`, { method: 'delete' });
|
|
32
94
|
}
|
|
33
95
|
|
|
34
96
|
update(identifier: string, changes: Partial<T & IBaseBlueprintEntity>, extra?: RequestExtra): Promise<IBaseBlueprintEntity & T> {
|
|
35
97
|
return this.callJsonApi<IBaseBlueprintEntity & T>(
|
|
36
|
-
`${this.relativePath}/${this.blueprintKey}/entities/${identifier}${this.getQueryParams(extra?.query)}`,
|
|
98
|
+
`${this.relativePath}/${this.blueprintKey}/entities/${identifier}${this.getQueryParams(withFlatDefault(extra?.query))}`,
|
|
37
99
|
{
|
|
38
100
|
method: 'put',
|
|
39
101
|
headers: { 'content-type': 'application/json' },
|
|
@@ -44,11 +106,57 @@ export default class QlBlueprintEntities<T = any> extends BaseSDK {
|
|
|
44
106
|
}
|
|
45
107
|
|
|
46
108
|
create(entity: T & IBaseBlueprintEntity, extra?: RequestExtra): Promise<IBaseBlueprintEntity & T> {
|
|
47
|
-
return this.callJsonApi<IBaseBlueprintEntity & T>(`${this.relativePath}/${this.blueprintKey}/entities${this.getQueryParams(extra?.query)}`, {
|
|
109
|
+
return this.callJsonApi<IBaseBlueprintEntity & T>(`${this.relativePath}/${this.blueprintKey}/entities${this.getQueryParams(withFlatDefault(extra?.query))}`, {
|
|
48
110
|
method: 'post',
|
|
49
111
|
headers: { 'content-type': 'application/json' },
|
|
50
112
|
body: JSON.stringify(entity),
|
|
51
113
|
...(extra || {}),
|
|
52
114
|
})
|
|
53
115
|
}
|
|
116
|
+
|
|
117
|
+
query(): QlBlueprintEntitiesQuery<T> {
|
|
118
|
+
return new QlBlueprintEntitiesQuery<T>(this);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
where(filters: BlueprintEntityFilters): QlBlueprintEntitiesQuery<T> {
|
|
122
|
+
return this.query().where(filters);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
limit(value: number): QlBlueprintEntitiesQuery<T> {
|
|
126
|
+
return this.query().limit(value);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
skip(value: number): QlBlueprintEntitiesQuery<T> {
|
|
130
|
+
return this.query().skip(value);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
sort(value: string): QlBlueprintEntitiesQuery<T> {
|
|
134
|
+
return this.query().sort(value);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
select(fields: string | string[]): QlBlueprintEntitiesQuery<T> {
|
|
138
|
+
return this.query().select(fields);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
find(extra?: RequestExtra): Promise<(IBaseBlueprintEntity & T)[]> {
|
|
142
|
+
return this.getList(undefined, extra);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
findOne(identifier: string, extra?: RequestExtra): Promise<IBaseBlueprintEntity & T> {
|
|
146
|
+
return this.getEntity(identifier, extra);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
count(query?: BlueprintEntityFilters, extra?: RequestExtra): Promise<number> {
|
|
150
|
+
return this
|
|
151
|
+
.callJsonApi<{ count: number }>(`${this.relativePath}/${this.blueprintKey}/charts/count${this.getQueryParams(query)}`, extra)
|
|
152
|
+
.then((res) => res?.count ?? 0);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
updateOne(identifier: string, changes: Partial<T & IBaseBlueprintEntity>, extra?: RequestExtra): Promise<IBaseBlueprintEntity & T> {
|
|
156
|
+
return this.update(identifier, changes, extra);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
deleteOne(identifier: string, extra?: RequestExtra): Promise<any> {
|
|
160
|
+
return this.remove(identifier, extra);
|
|
161
|
+
}
|
|
54
162
|
}
|
package/src/blueprints.ts
CHANGED
|
@@ -3,6 +3,11 @@ import BaseSDK from './base-sdk';
|
|
|
3
3
|
import type { IBlueprint } from '@qelos/global-types';
|
|
4
4
|
import QlBlueprintEntities from './blueprints-entities';
|
|
5
5
|
|
|
6
|
+
// Empty by design — extended via TypeScript declaration merging from
|
|
7
|
+
// generated `.d.ts` files (`qelos interfaces build`) so `entitiesOf("todo")`
|
|
8
|
+
// resolves to the typed entity without callers passing a generic.
|
|
9
|
+
export interface BlueprintEntitiesRegistry {}
|
|
10
|
+
|
|
6
11
|
export default class QlBlueprints extends BaseSDK {
|
|
7
12
|
private relativePath = '/api/blueprints';
|
|
8
13
|
|
|
@@ -20,9 +25,11 @@ export default class QlBlueprints extends BaseSDK {
|
|
|
20
25
|
return this.callJsonApi<IBlueprint[]>(this.relativePath);
|
|
21
26
|
}
|
|
22
27
|
|
|
23
|
-
entitiesOf<
|
|
28
|
+
entitiesOf<K extends keyof BlueprintEntitiesRegistry>(blueprintKey: K): QlBlueprintEntities<BlueprintEntitiesRegistry[K]>;
|
|
29
|
+
entitiesOf<T = any>(blueprintKey: string): QlBlueprintEntities<T>;
|
|
30
|
+
entitiesOf(blueprintKey: string): QlBlueprintEntities {
|
|
24
31
|
if (!this.#entities.has(blueprintKey)) {
|
|
25
|
-
this.#entities.set(blueprintKey, new QlBlueprintEntities
|
|
32
|
+
this.#entities.set(blueprintKey, new QlBlueprintEntities(this.options, blueprintKey));
|
|
26
33
|
}
|
|
27
34
|
return this.#entities.get(blueprintKey);
|
|
28
35
|
}
|
package/src/index.ts
CHANGED
|
@@ -6,11 +6,32 @@ import QlAuthentication from './authentication';
|
|
|
6
6
|
import QlWorkspaces from './workspaces';
|
|
7
7
|
import QlInvites from './invites';
|
|
8
8
|
import QlBlueprints from './blueprints';
|
|
9
|
+
import QlBlueprintEntities from './blueprints-entities';
|
|
9
10
|
import QlAI from './ai';
|
|
10
11
|
import QlLambdas from './lambdas';
|
|
11
12
|
import QlPayments from './payments';
|
|
12
13
|
|
|
13
|
-
const noExtraHeadersUrls = new Set([
|
|
14
|
+
const noExtraHeadersUrls = new Set([
|
|
15
|
+
'/api/token/refresh',
|
|
16
|
+
'/api/signin',
|
|
17
|
+
'/api/signup',
|
|
18
|
+
'/api/auth/callback',
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
export type {
|
|
22
|
+
SocialCallbackInput,
|
|
23
|
+
SocialAuthCallbackPayload,
|
|
24
|
+
SocialProvider,
|
|
25
|
+
SocialLoginOptions,
|
|
26
|
+
} from './authentication';
|
|
27
|
+
|
|
28
|
+
export type { BlueprintEntitiesRegistry } from './blueprints';
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
parseSocialCallbackRefreshToken,
|
|
32
|
+
getSocialAuthSetCookieParts,
|
|
33
|
+
applySocialAuthCookiesToServerResponse,
|
|
34
|
+
} from './authentication';
|
|
14
35
|
|
|
15
36
|
export default class QelosSDK extends BaseSDK {
|
|
16
37
|
#customHeaders = {}
|
|
@@ -64,6 +85,10 @@ export default class QelosSDK extends BaseSDK {
|
|
|
64
85
|
}
|
|
65
86
|
}
|
|
66
87
|
|
|
88
|
+
entities<T = any>(blueprintKey: string): QlBlueprintEntities<T> {
|
|
89
|
+
return this.blueprints.entitiesOf<T>(blueprintKey);
|
|
90
|
+
}
|
|
91
|
+
|
|
67
92
|
setCustomHeader(key: string, value: string) {
|
|
68
93
|
this.#customHeaders[key] = value;
|
|
69
94
|
}
|
package/src/workspaces.ts
CHANGED
|
@@ -8,8 +8,11 @@ export interface IWorkspaceMember {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export interface IInvite {
|
|
11
|
+
_id?: string;
|
|
11
12
|
name?: string;
|
|
12
13
|
email: string;
|
|
14
|
+
phone?: string;
|
|
15
|
+
roles?: string[];
|
|
13
16
|
created?: string | Date;
|
|
14
17
|
}
|
|
15
18
|
|
|
@@ -76,4 +79,64 @@ export default class QlWorkspaces extends BaseSDK {
|
|
|
76
79
|
}
|
|
77
80
|
)
|
|
78
81
|
}
|
|
82
|
+
|
|
83
|
+
async inviteUser(
|
|
84
|
+
workspaceId: string,
|
|
85
|
+
email: string,
|
|
86
|
+
roles?: string[]
|
|
87
|
+
): Promise<IWorkspace> {
|
|
88
|
+
const ws = await this.getWorkspace(workspaceId);
|
|
89
|
+
const inviteRoles = roles && roles.length > 0 ? roles : ['member'];
|
|
90
|
+
const invites: IInvite[] = [...(ws.invites || [])];
|
|
91
|
+
const normalized = email.trim().toLowerCase();
|
|
92
|
+
const index = invites.findIndex((i) => (i.email || '').trim().toLowerCase() === normalized);
|
|
93
|
+
if (index >= 0) {
|
|
94
|
+
invites[index] = {...invites[index], email: invites[index].email || email, roles: inviteRoles};
|
|
95
|
+
} else {
|
|
96
|
+
invites.push({email, roles: inviteRoles});
|
|
97
|
+
}
|
|
98
|
+
return this.update(workspaceId, {invites});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
removeMember(workspaceId: string, userId: string): Promise<{
|
|
102
|
+
message: string;
|
|
103
|
+
removedMemberId?: string;
|
|
104
|
+
userId?: string;
|
|
105
|
+
}> {
|
|
106
|
+
const path = `${this.relativePath}/${workspaceId}/members/${encodeURIComponent(userId)}`;
|
|
107
|
+
return this.callJsonApi(path, {method: 'delete'});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
updateMemberRoles(
|
|
111
|
+
workspaceId: string,
|
|
112
|
+
userId: string,
|
|
113
|
+
roles: string[]
|
|
114
|
+
): Promise<{
|
|
115
|
+
message: string;
|
|
116
|
+
updatedMember?: IWorkspaceMember;
|
|
117
|
+
workspaceId?: string;
|
|
118
|
+
}> {
|
|
119
|
+
const path = `${this.relativePath}/${workspaceId}/members/${encodeURIComponent(userId)}`;
|
|
120
|
+
return this.callJsonApi(path, {
|
|
121
|
+
method: 'put',
|
|
122
|
+
headers: {'content-type': 'application/json'},
|
|
123
|
+
body: JSON.stringify({roles}),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async listInvites(workspaceId: string): Promise<IInvite[]> {
|
|
128
|
+
const ws = await this.getWorkspace(workspaceId);
|
|
129
|
+
return ws.invites ?? [];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async revokeInvite(workspaceId: string, inviteId: string): Promise<IWorkspace> {
|
|
133
|
+
const ws = await this.getWorkspace(workspaceId);
|
|
134
|
+
const prev = ws.invites || [];
|
|
135
|
+
const idStr = String(inviteId);
|
|
136
|
+
const invites = prev.filter((inv) => String(inv._id ?? '') !== idStr);
|
|
137
|
+
if (invites.length === prev.length) {
|
|
138
|
+
throw new Error('Invitation not found');
|
|
139
|
+
}
|
|
140
|
+
return this.update(workspaceId, {invites});
|
|
141
|
+
}
|
|
79
142
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as assert from 'node:assert/strict';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import type { SocialAuthCallbackPayload } from '../src/authentication';
|
|
4
|
+
import {
|
|
5
|
+
parseSocialCallbackRefreshToken,
|
|
6
|
+
getSocialAuthSetCookieParts,
|
|
7
|
+
} from '../src/authentication';
|
|
8
|
+
|
|
9
|
+
test('parseSocialCallbackRefreshToken', async (t) => {
|
|
10
|
+
await t.test('parses full URL and path-only string', () => {
|
|
11
|
+
assert.equal(
|
|
12
|
+
parseSocialCallbackRefreshToken('https://app.example/api/cb?rt=abc&x=1'),
|
|
13
|
+
'abc',
|
|
14
|
+
);
|
|
15
|
+
assert.equal(parseSocialCallbackRefreshToken('/cb?rt=xyz'), 'xyz');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
await t.test('parses URL / URLSearchParams / query objects', () => {
|
|
19
|
+
assert.equal(parseSocialCallbackRefreshToken(new URL('http://_/p?rt=u')), 'u');
|
|
20
|
+
assert.equal(parseSocialCallbackRefreshToken(new URLSearchParams('rt=v')), 'v');
|
|
21
|
+
assert.equal(parseSocialCallbackRefreshToken({ rt: 'w' }), 'w');
|
|
22
|
+
assert.equal(parseSocialCallbackRefreshToken({ rt: ['z'] }), 'z');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
await t.test('returns undefined when rt missing', () => {
|
|
26
|
+
assert.equal(parseSocialCallbackRefreshToken('/no-query'), undefined);
|
|
27
|
+
assert.equal(parseSocialCallbackRefreshToken({}), undefined);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('getSocialAuthSetCookieParts', async (t) => {
|
|
32
|
+
await t.test('prefers setCookieHeaders array', () => {
|
|
33
|
+
const r = {
|
|
34
|
+
payload: { user: {} },
|
|
35
|
+
headers: { 'set-cookie': 'a=1' },
|
|
36
|
+
setCookieHeaders: ['a=1', 'b=2'],
|
|
37
|
+
} as SocialAuthCallbackPayload;
|
|
38
|
+
assert.deepEqual(getSocialAuthSetCookieParts(r), ['a=1', 'b=2']);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
await t.test('falls back to single header', () => {
|
|
42
|
+
const r = {
|
|
43
|
+
payload: { user: {} },
|
|
44
|
+
headers: { 'set-cookie': 'x=9' },
|
|
45
|
+
} as SocialAuthCallbackPayload;
|
|
46
|
+
assert.deepEqual(getSocialAuthSetCookieParts(r), ['x=9']);
|
|
47
|
+
});
|
|
48
|
+
});
|
package/test/base-sdk.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as assert from 'node:assert/strict';
|
|
2
2
|
import { test } from 'node:test';
|
|
3
|
-
import BaseSDK from '../src/base-sdk';
|
|
3
|
+
import BaseSDK, { relativeUrlPath } from '../src/base-sdk';
|
|
4
4
|
import { QelosSDKOptions } from '../src/types';
|
|
5
5
|
|
|
6
6
|
test('BaseSDK', async (t) => {
|
|
@@ -77,4 +77,29 @@ test('BaseSDK', async (t) => {
|
|
|
77
77
|
const queryString = sdkWithExtraParams.getQueryParams({ local: 'param' });
|
|
78
78
|
assert.equal(queryString, '?global=param&local=param');
|
|
79
79
|
});
|
|
80
|
+
|
|
81
|
+
await t.test('relativeUrlPath strips query and hash', () => {
|
|
82
|
+
assert.equal(relativeUrlPath('/api/auth/callback?rt=x'), '/api/auth/callback');
|
|
83
|
+
assert.equal(relativeUrlPath('/path#frag'), '/path');
|
|
84
|
+
assert.equal(relativeUrlPath('/plain'), '/plain');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await t.test('extraHeaders receives path without query string', async () => {
|
|
88
|
+
const seen: string[] = [];
|
|
89
|
+
options = {
|
|
90
|
+
appUrl: 'http://localhost:3000',
|
|
91
|
+
fetch: async (url) => {
|
|
92
|
+
const res = new Response(JSON.stringify({ ok: true }));
|
|
93
|
+
res.headers.set('Content-Type', 'application/json');
|
|
94
|
+
return res;
|
|
95
|
+
},
|
|
96
|
+
extraHeaders: async (relativeUrl) => {
|
|
97
|
+
seen.push(relativeUrl);
|
|
98
|
+
return {};
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
const s = new BaseSDK(options);
|
|
102
|
+
await s.callApi('/api/auth/callback?rt=tok');
|
|
103
|
+
assert.deepEqual(seen, ['/api/auth/callback']);
|
|
104
|
+
});
|
|
80
105
|
});
|
package/test/index.test.ts
CHANGED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import * as assert from 'node:assert/strict';
|
|
2
|
+
import {test} from 'node:test';
|
|
3
|
+
import QlWorkspaces from '../src/workspaces';
|
|
4
|
+
import {QelosSDKOptions} from '../src/types';
|
|
5
|
+
|
|
6
|
+
function jsonResponse(body: unknown, ok = true) {
|
|
7
|
+
const res = new Response(JSON.stringify(body));
|
|
8
|
+
res.headers.set('Content-Type', 'application/json');
|
|
9
|
+
if (!ok) {
|
|
10
|
+
Object.defineProperty(res, 'ok', {value: false});
|
|
11
|
+
Object.defineProperty(res, 'status', {value: 400});
|
|
12
|
+
}
|
|
13
|
+
return res;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test('QlWorkspaces', async (t) => {
|
|
17
|
+
await t.test('removeMember sends DELETE to members path', async () => {
|
|
18
|
+
const calls: string[] = [];
|
|
19
|
+
const options: QelosSDKOptions = {
|
|
20
|
+
appUrl: 'http://localhost:3000',
|
|
21
|
+
fetch: async (url) => {
|
|
22
|
+
calls.push(url.toString());
|
|
23
|
+
return jsonResponse({message: 'Member removed from workspace.', userId: 'u1'});
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
const ws = new QlWorkspaces(options);
|
|
27
|
+
await ws.removeMember('ws-1', 'user-456');
|
|
28
|
+
assert.equal(calls[0], 'http://localhost:3000/api/workspaces/ws-1/members/user-456');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
await t.test('updateMemberRoles sends PUT with roles body', async () => {
|
|
32
|
+
let init: RequestInit | undefined;
|
|
33
|
+
const options: QelosSDKOptions = {
|
|
34
|
+
appUrl: 'http://localhost:3000',
|
|
35
|
+
fetch: async (_url, requestInit) => {
|
|
36
|
+
init = requestInit;
|
|
37
|
+
return jsonResponse({
|
|
38
|
+
message: 'Member roles updated successfully.',
|
|
39
|
+
workspaceId: 'ws-1',
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
const ws = new QlWorkspaces(options);
|
|
44
|
+
await ws.updateMemberRoles('ws-1', 'user-456', ['admin']);
|
|
45
|
+
assert.equal(init?.method, 'put');
|
|
46
|
+
assert.equal((init?.headers as Record<string, string>)['content-type'], 'application/json');
|
|
47
|
+
assert.deepEqual(JSON.parse(init?.body as string), {roles: ['admin']});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await t.test('inviteUser merges invites then updates workspace', async () => {
|
|
51
|
+
const bodies: unknown[] = [];
|
|
52
|
+
let step = 0;
|
|
53
|
+
const options: QelosSDKOptions = {
|
|
54
|
+
appUrl: 'http://localhost:3000',
|
|
55
|
+
fetch: async (_url, requestInit) => {
|
|
56
|
+
if (step === 0) {
|
|
57
|
+
step++;
|
|
58
|
+
assert.equal(requestInit?.method, undefined);
|
|
59
|
+
return jsonResponse({
|
|
60
|
+
name: 'W',
|
|
61
|
+
labels: [],
|
|
62
|
+
invites: [{email: 'old@x.com', roles: ['member'], _id: 'i1'}],
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
bodies.push(JSON.parse(requestInit?.body as string));
|
|
66
|
+
return jsonResponse({name: 'W', labels: [], invites: []});
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
const ws = new QlWorkspaces(options);
|
|
70
|
+
await ws.inviteUser('ws-1', 'newdev@company.com', ['editor']);
|
|
71
|
+
assert.equal(step, 1);
|
|
72
|
+
assert.deepEqual(bodies[0], {
|
|
73
|
+
invites: [
|
|
74
|
+
{email: 'old@x.com', roles: ['member'], _id: 'i1'},
|
|
75
|
+
{email: 'newdev@company.com', roles: ['editor']},
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await t.test('inviteUser updates roles for existing email (case-insensitive)', async () => {
|
|
81
|
+
let putBody: unknown;
|
|
82
|
+
let step = 0;
|
|
83
|
+
const options: QelosSDKOptions = {
|
|
84
|
+
appUrl: 'http://localhost:3000',
|
|
85
|
+
fetch: async (_url, requestInit) => {
|
|
86
|
+
if (step === 0) {
|
|
87
|
+
step++;
|
|
88
|
+
return jsonResponse({
|
|
89
|
+
name: 'W',
|
|
90
|
+
labels: [],
|
|
91
|
+
invites: [{email: 'A@X.COM', roles: ['member'], _id: 'i1'}],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
putBody = JSON.parse(requestInit?.body as string);
|
|
95
|
+
return jsonResponse({name: 'W', labels: []});
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
const ws = new QlWorkspaces(options);
|
|
99
|
+
await ws.inviteUser('ws-1', 'a@x.com', ['admin']);
|
|
100
|
+
assert.deepEqual(putBody, {
|
|
101
|
+
invites: [{email: 'A@X.COM', roles: ['admin'], _id: 'i1'}],
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await t.test('listInvites returns invites from getWorkspace', async () => {
|
|
106
|
+
const options: QelosSDKOptions = {
|
|
107
|
+
appUrl: 'http://localhost:3000',
|
|
108
|
+
fetch: async () =>
|
|
109
|
+
jsonResponse({
|
|
110
|
+
name: 'W',
|
|
111
|
+
labels: [],
|
|
112
|
+
invites: [{email: 'x@y.com', roles: ['editor'], _id: 'inv1'}],
|
|
113
|
+
}),
|
|
114
|
+
};
|
|
115
|
+
const ws = new QlWorkspaces(options);
|
|
116
|
+
const list = await ws.listInvites('ws-1');
|
|
117
|
+
assert.equal(list.length, 1);
|
|
118
|
+
assert.equal(list[0].email, 'x@y.com');
|
|
119
|
+
assert.equal(list[0]._id, 'inv1');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await t.test('revokeInvite filters invite and updates workspace', async () => {
|
|
123
|
+
let putBody: unknown;
|
|
124
|
+
let step = 0;
|
|
125
|
+
const options: QelosSDKOptions = {
|
|
126
|
+
appUrl: 'http://localhost:3000',
|
|
127
|
+
fetch: async (_url, requestInit) => {
|
|
128
|
+
if (step === 0) {
|
|
129
|
+
step++;
|
|
130
|
+
return jsonResponse({
|
|
131
|
+
name: 'W',
|
|
132
|
+
labels: [],
|
|
133
|
+
invites: [
|
|
134
|
+
{_id: 'keep', email: 'a@b.com', roles: ['member']},
|
|
135
|
+
{_id: 'drop', email: 'c@d.com', roles: ['member']},
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
putBody = JSON.parse(requestInit?.body as string);
|
|
140
|
+
return jsonResponse({name: 'W', labels: []});
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
const ws = new QlWorkspaces(options);
|
|
144
|
+
await ws.revokeInvite('ws-1', 'drop');
|
|
145
|
+
assert.deepEqual(putBody, {
|
|
146
|
+
invites: [{_id: 'keep', email: 'a@b.com', roles: ['member']}],
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await t.test('revokeInvite throws when id is not found', async () => {
|
|
151
|
+
let step = 0;
|
|
152
|
+
const options: QelosSDKOptions = {
|
|
153
|
+
appUrl: 'http://localhost:3000',
|
|
154
|
+
fetch: async () => {
|
|
155
|
+
if (step === 0) {
|
|
156
|
+
step++;
|
|
157
|
+
return jsonResponse({
|
|
158
|
+
name: 'W',
|
|
159
|
+
labels: [],
|
|
160
|
+
invites: [{_id: 'x', email: 'a@b.com', roles: ['member']}],
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return jsonResponse({});
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
const ws = new QlWorkspaces(options);
|
|
167
|
+
await assert.rejects(() => ws.revokeInvite('ws-1', 'missing'), /Invitation not found/);
|
|
168
|
+
assert.equal(step, 1);
|
|
169
|
+
});
|
|
170
|
+
});
|