@abyss-project/link 1.0.0 → 1.1.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/dist/api/index.d.ts +3 -0
- package/dist/api/index.js +3 -0
- package/dist/api/index.js.map +1 -1
- package/dist/api/link.admin.api.d.ts +14 -0
- package/dist/api/link.admin.api.js +64 -0
- package/dist/api/link.admin.api.js.map +1 -0
- package/dist/api/link.api.d.ts +15 -0
- package/dist/api/link.api.js +69 -0
- package/dist/api/link.api.js.map +1 -0
- package/dist/api/link.public.api.d.ts +2 -0
- package/dist/api/link.public.api.js +9 -0
- package/dist/api/link.public.api.js.map +1 -0
- package/dist/constants/link.constants.d.ts +11 -0
- package/dist/constants/link.constants.js +12 -1
- package/dist/constants/link.constants.js.map +1 -1
- package/dist/index.d.ts +37 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -1
- package/dist/types/enum/api-error.enum.d.ts +9 -1
- package/dist/types/enum/api-error.enum.js +8 -0
- package/dist/types/enum/api-error.enum.js.map +1 -1
- package/dist/types/enum/index.d.ts +2 -0
- package/dist/types/enum/index.js +2 -0
- package/dist/types/enum/index.js.map +1 -1
- package/dist/types/enum/link-share.enum.d.ts +11 -0
- package/dist/types/enum/link-share.enum.js +17 -0
- package/dist/types/enum/link-share.enum.js.map +1 -0
- package/dist/types/enum/link-stats.enum.d.ts +5 -0
- package/dist/types/enum/link-stats.enum.js +10 -0
- package/dist/types/enum/link-stats.enum.js.map +1 -0
- package/dist/types/interface/api/requests/index.d.ts +3 -0
- package/dist/types/interface/api/requests/index.js +3 -0
- package/dist/types/interface/api/requests/index.js.map +1 -1
- package/dist/types/interface/api/requests/link.admin.request.d.ts +69 -0
- package/dist/types/interface/api/requests/link.admin.request.js +3 -0
- package/dist/types/interface/api/requests/link.admin.request.js.map +1 -0
- package/dist/types/interface/api/requests/link.public.request.d.ts +13 -0
- package/dist/types/interface/api/requests/link.public.request.js +3 -0
- package/dist/types/interface/api/requests/link.public.request.js.map +1 -0
- package/dist/types/interface/api/requests/link.request.d.ts +74 -0
- package/dist/types/interface/api/requests/link.request.js +3 -0
- package/dist/types/interface/api/requests/link.request.js.map +1 -0
- package/dist/types/interface/api/responses/index.d.ts +3 -0
- package/dist/types/interface/api/responses/index.js +3 -0
- package/dist/types/interface/api/responses/index.js.map +1 -1
- package/dist/types/interface/api/responses/link.admin.response.d.ts +43 -0
- package/dist/types/interface/api/responses/link.admin.response.js +3 -0
- package/dist/types/interface/api/responses/link.admin.response.js.map +1 -0
- package/dist/types/interface/api/responses/link.public.response.d.ts +5 -0
- package/dist/types/interface/api/responses/link.public.response.js +3 -0
- package/dist/types/interface/api/responses/link.public.response.js.map +1 -0
- package/dist/types/interface/api/responses/link.response.d.ts +43 -0
- package/dist/types/interface/api/responses/link.response.js +3 -0
- package/dist/types/interface/api/responses/link.response.js.map +1 -0
- package/dist/types/interface/models/index.d.ts +1 -0
- package/dist/types/interface/models/index.js +1 -0
- package/dist/types/interface/models/index.js.map +1 -1
- package/dist/types/interface/models/link-list.dto.d.ts +20 -0
- package/dist/types/interface/models/link-list.dto.js +3 -0
- package/dist/types/interface/models/link-list.dto.js.map +1 -0
- package/package.json +15 -10
- package/src/api/index.ts +3 -0
- package/src/api/link.admin.api.ts +168 -0
- package/src/api/link.api.ts +158 -0
- package/src/api/link.public.api.ts +19 -0
- package/src/constants/link.constants.ts +17 -0
- package/src/index.ts +71 -0
- package/src/tests/link-public-api.spec.ts +37 -0
- package/src/tests/link-sdk-surface.spec.ts +191 -0
- package/src/tests/sdk-surface.spec.ts +1 -1
- package/src/types/enum/api-error.enum.ts +13 -0
- package/src/types/enum/index.ts +2 -0
- package/src/types/enum/link-share.enum.ts +17 -0
- package/src/types/enum/link-stats.enum.ts +6 -0
- package/src/types/interface/api/requests/index.ts +3 -0
- package/src/types/interface/api/requests/link.admin.request.ts +104 -0
- package/src/types/interface/api/requests/link.public.request.ts +18 -0
- package/src/types/interface/api/requests/link.request.ts +100 -0
- package/src/types/interface/api/responses/index.ts +3 -0
- package/src/types/interface/api/responses/link.admin.response.ts +56 -0
- package/src/types/interface/api/responses/link.public.response.ts +7 -0
- package/src/types/interface/api/responses/link.response.ts +55 -0
- package/src/types/interface/models/index.ts +1 -0
- package/src/types/interface/models/link-list.dto.ts +28 -0
- package/src/types/tests/link-enums.spec.ts +2 -0
|
@@ -43,7 +43,24 @@ export const MAX_LINK_PASSWORD_LENGTH = 128;
|
|
|
43
43
|
export const LINK_RESOURCE_CODE_LENGTH = 10;
|
|
44
44
|
/** Base62 campaign token. */
|
|
45
45
|
export const LINK_CAMPAIGN_TOKEN_LENGTH = 12;
|
|
46
|
+
/** Campaign tokens a product resource (a Spotlight project) holds at most (O5). */
|
|
46
47
|
export const LINK_CAMPAIGN_TOKENS_PER_PROJECT = 50;
|
|
48
|
+
/** The query parameter carrying a campaign token on the product's page: `?lnk=<token>`. */
|
|
49
|
+
export const CAMPAIGN_TOKEN_QUERY_PARAM = 'lnk';
|
|
47
50
|
|
|
48
51
|
/** Raw visits are kept this long (O2); daily aggregates live as long as the link. */
|
|
49
52
|
export const LINK_VISIT_RETENTION_DAYS = 90;
|
|
53
|
+
|
|
54
|
+
/** Schemes a link may target. */
|
|
55
|
+
export const LINK_ALLOWED_URL_SCHEMES = ['http', 'https'] as const;
|
|
56
|
+
/** Length of the slug drawn when a SHORTENED link is created without one. */
|
|
57
|
+
export const LINK_GENERATED_SLUG_LENGTH = 6;
|
|
58
|
+
export const MAX_LINK_TARGET_URL_LENGTH = 2048;
|
|
59
|
+
export const MAX_LINK_TITLE_LENGTH = 255;
|
|
60
|
+
export const MAX_LINK_DESCRIPTION_LENGTH = 1000;
|
|
61
|
+
export const MIN_LINK_MAX_CLICKS = 1;
|
|
62
|
+
|
|
63
|
+
export const MIN_LINK_PAGE = 1;
|
|
64
|
+
export const MIN_LINK_LIMIT = 1;
|
|
65
|
+
export const MAX_LINK_LIMIT = 100;
|
|
66
|
+
export const DEFAULT_LINK_LIMIT = 20;
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,9 @@ export * from './utils';
|
|
|
7
7
|
export * from './constants';
|
|
8
8
|
|
|
9
9
|
import * as abyssAdminApi from './api/abyss.admin.api';
|
|
10
|
+
import * as linkApi from './api/link.api';
|
|
11
|
+
import * as linkAdminApi from './api/link.admin.api';
|
|
12
|
+
import * as linkPublicApi from './api/link.public.api';
|
|
10
13
|
|
|
11
14
|
const DEFAULT_BASE_URL = 'https://link-api.abyss-project.fr/api/';
|
|
12
15
|
|
|
@@ -35,6 +38,40 @@ type AbyssLinkCoreSDK = {
|
|
|
35
38
|
syncProject: typeof abyssAdminApi.syncProjectAbyssAdmin;
|
|
36
39
|
};
|
|
37
40
|
};
|
|
41
|
+
link: {
|
|
42
|
+
get: typeof linkApi.getLink;
|
|
43
|
+
paginate: typeof linkApi.paginateLink;
|
|
44
|
+
paginateProject: typeof linkApi.paginateProjectLink;
|
|
45
|
+
paginateOrganization: typeof linkApi.paginateOrganizationLink;
|
|
46
|
+
countOrganization: typeof linkApi.countOrganizationLink;
|
|
47
|
+
create: typeof linkApi.createLink;
|
|
48
|
+
createForOrganization: typeof linkApi.createForOrganizationLink;
|
|
49
|
+
update: typeof linkApi.updateLink;
|
|
50
|
+
delete: typeof linkApi.deleteLink;
|
|
51
|
+
transfer: typeof linkApi.transferLink;
|
|
52
|
+
getStats: typeof linkApi.getStatsLink;
|
|
53
|
+
getVisits: typeof linkApi.getVisitsLink;
|
|
54
|
+
getByOrigin: typeof linkApi.getByOriginLink;
|
|
55
|
+
getStatsByOrigin: typeof linkApi.getStatsByOriginLink;
|
|
56
|
+
public: {
|
|
57
|
+
unlock: typeof linkPublicApi.unlockLinkPublic;
|
|
58
|
+
};
|
|
59
|
+
admin: {
|
|
60
|
+
paginate: typeof linkAdminApi.paginateLinkAdmin;
|
|
61
|
+
get: typeof linkAdminApi.getLinkAdmin;
|
|
62
|
+
getVisits: typeof linkAdminApi.getVisitsLinkAdmin;
|
|
63
|
+
getCampaignStats: typeof linkAdminApi.getCampaignStatsLinkAdmin;
|
|
64
|
+
setProtection: typeof linkAdminApi.setProtectionLinkAdmin;
|
|
65
|
+
update: typeof linkAdminApi.updateLinkAdmin;
|
|
66
|
+
delete: typeof linkAdminApi.deleteLinkAdmin;
|
|
67
|
+
create: typeof linkAdminApi.createLinkAdmin;
|
|
68
|
+
getByOrigin: typeof linkAdminApi.getByOriginLinkAdmin;
|
|
69
|
+
getStatsByOrigin: typeof linkAdminApi.getStatsByOriginLinkAdmin;
|
|
70
|
+
recordCampaignView: typeof linkAdminApi.recordCampaignViewLinkAdmin;
|
|
71
|
+
listCampaignTokens: typeof linkAdminApi.listCampaignTokensLinkAdmin;
|
|
72
|
+
deleteCampaignToken: typeof linkAdminApi.deleteCampaignTokenLinkAdmin;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
38
75
|
};
|
|
39
76
|
|
|
40
77
|
export class AbyssLinkCore {
|
|
@@ -67,6 +104,40 @@ export class AbyssLinkCore {
|
|
|
67
104
|
syncProject: abyssAdminApi.syncProjectAbyssAdmin,
|
|
68
105
|
},
|
|
69
106
|
},
|
|
107
|
+
link: {
|
|
108
|
+
get: linkApi.getLink,
|
|
109
|
+
paginate: linkApi.paginateLink,
|
|
110
|
+
paginateProject: linkApi.paginateProjectLink,
|
|
111
|
+
paginateOrganization: linkApi.paginateOrganizationLink,
|
|
112
|
+
countOrganization: linkApi.countOrganizationLink,
|
|
113
|
+
create: linkApi.createLink,
|
|
114
|
+
createForOrganization: linkApi.createForOrganizationLink,
|
|
115
|
+
update: linkApi.updateLink,
|
|
116
|
+
delete: linkApi.deleteLink,
|
|
117
|
+
transfer: linkApi.transferLink,
|
|
118
|
+
getStats: linkApi.getStatsLink,
|
|
119
|
+
getVisits: linkApi.getVisitsLink,
|
|
120
|
+
getByOrigin: linkApi.getByOriginLink,
|
|
121
|
+
getStatsByOrigin: linkApi.getStatsByOriginLink,
|
|
122
|
+
public: {
|
|
123
|
+
unlock: linkPublicApi.unlockLinkPublic,
|
|
124
|
+
},
|
|
125
|
+
admin: {
|
|
126
|
+
paginate: linkAdminApi.paginateLinkAdmin,
|
|
127
|
+
get: linkAdminApi.getLinkAdmin,
|
|
128
|
+
getVisits: linkAdminApi.getVisitsLinkAdmin,
|
|
129
|
+
getCampaignStats: linkAdminApi.getCampaignStatsLinkAdmin,
|
|
130
|
+
setProtection: linkAdminApi.setProtectionLinkAdmin,
|
|
131
|
+
update: linkAdminApi.updateLinkAdmin,
|
|
132
|
+
delete: linkAdminApi.deleteLinkAdmin,
|
|
133
|
+
create: linkAdminApi.createLinkAdmin,
|
|
134
|
+
getByOrigin: linkAdminApi.getByOriginLinkAdmin,
|
|
135
|
+
getStatsByOrigin: linkAdminApi.getStatsByOriginLinkAdmin,
|
|
136
|
+
recordCampaignView: linkAdminApi.recordCampaignViewLinkAdmin,
|
|
137
|
+
listCampaignTokens: linkAdminApi.listCampaignTokensLinkAdmin,
|
|
138
|
+
deleteCampaignToken: linkAdminApi.deleteCampaignTokenLinkAdmin,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
70
141
|
};
|
|
71
142
|
}
|
|
72
143
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { AbyssLinkCore } from '..';
|
|
2
|
+
|
|
3
|
+
describe('AbyssLinkCore.sdk.link.public', () => {
|
|
4
|
+
let post: jest.SpyInstance;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
post = jest
|
|
8
|
+
.spyOn(AbyssLinkCore.axios, 'post')
|
|
9
|
+
.mockResolvedValue({ data: { data: { url: 'https://example.com/landing' } } });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
jest.restoreAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('unlocks with POST link/public/<slug>/unlock, the password and the visit data in the body', async () => {
|
|
17
|
+
const answer = await AbyssLinkCore.sdk.link.public.unlock({
|
|
18
|
+
slug: 'Launch',
|
|
19
|
+
password: 'open sesame',
|
|
20
|
+
ref: 'https://news.example.org/a',
|
|
21
|
+
utmSource: 'mail',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(post).toHaveBeenCalledWith('link/public/Launch/unlock', {
|
|
25
|
+
password: 'open sesame',
|
|
26
|
+
ref: 'https://news.example.org/a',
|
|
27
|
+
utmSource: 'mail',
|
|
28
|
+
});
|
|
29
|
+
expect(answer).toEqual({ data: { url: 'https://example.com/landing' } });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('encodes the slug in the path', async () => {
|
|
33
|
+
await AbyssLinkCore.sdk.link.public.unlock({ slug: 'a/b', password: 'x' });
|
|
34
|
+
|
|
35
|
+
expect(post).toHaveBeenCalledWith('link/public/a%2Fb/unlock', { password: 'x' });
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { AbyssLinkCore, LinkKind, LinkResourceProduct, LinkStatsGranularity } from '..';
|
|
2
|
+
|
|
3
|
+
const LINK_ID = '11111111-1111-4111-8111-111111111111';
|
|
4
|
+
const ORGANIZATION_ID = '22222222-2222-4222-8222-222222222222';
|
|
5
|
+
const PROJECT_ID = '33333333-3333-4333-8333-333333333333';
|
|
6
|
+
const ORIGIN = {
|
|
7
|
+
service: LinkResourceProduct.BEAM,
|
|
8
|
+
resourceType: 'beam_transfer',
|
|
9
|
+
resourceId: '44444444-4444-4444-8444-444444444444',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `sdk.link.*` (the user routes of the Links screen) and `sdk.link.admin.*` (the back-office and
|
|
14
|
+
* Monitor): each call hits the route `link.controller.ts` / `link.admin.controller.ts` serve.
|
|
15
|
+
*/
|
|
16
|
+
describe('AbyssLinkCore sdk.link surface', () => {
|
|
17
|
+
let get: jest.SpyInstance;
|
|
18
|
+
let post: jest.SpyInstance;
|
|
19
|
+
let put: jest.SpyInstance;
|
|
20
|
+
let del: jest.SpyInstance;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
const ok = { data: { data: {} } };
|
|
24
|
+
get = jest.spyOn(AbyssLinkCore.axios, 'get').mockResolvedValue(ok);
|
|
25
|
+
post = jest.spyOn(AbyssLinkCore.axios, 'post').mockResolvedValue(ok);
|
|
26
|
+
put = jest.spyOn(AbyssLinkCore.axios, 'put').mockResolvedValue(ok);
|
|
27
|
+
del = jest.spyOn(AbyssLinkCore.axios, 'delete').mockResolvedValue(ok);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
jest.restoreAllMocks();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('exposes the user routes and the admin routes', () => {
|
|
35
|
+
expect(Object.keys(AbyssLinkCore.sdk.link)).toEqual(
|
|
36
|
+
expect.arrayContaining([
|
|
37
|
+
'get',
|
|
38
|
+
'paginate',
|
|
39
|
+
'paginateProject',
|
|
40
|
+
'paginateOrganization',
|
|
41
|
+
'countOrganization',
|
|
42
|
+
'create',
|
|
43
|
+
'createForOrganization',
|
|
44
|
+
'update',
|
|
45
|
+
'delete',
|
|
46
|
+
'transfer',
|
|
47
|
+
'getStats',
|
|
48
|
+
'getVisits',
|
|
49
|
+
'getByOrigin',
|
|
50
|
+
'getStatsByOrigin',
|
|
51
|
+
'admin',
|
|
52
|
+
]),
|
|
53
|
+
);
|
|
54
|
+
expect(Object.keys(AbyssLinkCore.sdk.link.admin).sort()).toEqual([
|
|
55
|
+
'create',
|
|
56
|
+
'delete',
|
|
57
|
+
'deleteCampaignToken',
|
|
58
|
+
'get',
|
|
59
|
+
'getByOrigin',
|
|
60
|
+
'getCampaignStats',
|
|
61
|
+
'getStatsByOrigin',
|
|
62
|
+
'getVisits',
|
|
63
|
+
'listCampaignTokens',
|
|
64
|
+
'paginate',
|
|
65
|
+
'recordCampaignView',
|
|
66
|
+
'setProtection',
|
|
67
|
+
'update',
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('reads the user routes', async () => {
|
|
72
|
+
const { link } = AbyssLinkCore.sdk;
|
|
73
|
+
await link.get({ linkId: LINK_ID });
|
|
74
|
+
expect(get).toHaveBeenLastCalledWith(`link/${LINK_ID}`);
|
|
75
|
+
await link.paginate({ page: 2, search: 'promo' });
|
|
76
|
+
expect(get).toHaveBeenLastCalledWith('link/paginate', { params: { page: 2, search: 'promo' } });
|
|
77
|
+
await link.paginateProject({ projectId: PROJECT_ID }, { page: 1 });
|
|
78
|
+
expect(get).toHaveBeenLastCalledWith(`link/project/${PROJECT_ID}/paginate`, {
|
|
79
|
+
params: { page: 1 },
|
|
80
|
+
});
|
|
81
|
+
await link.paginateOrganization({ organizationId: ORGANIZATION_ID }, { adminView: true });
|
|
82
|
+
expect(get).toHaveBeenLastCalledWith(`link/organization/${ORGANIZATION_ID}/paginate`, {
|
|
83
|
+
params: { adminView: true },
|
|
84
|
+
});
|
|
85
|
+
await link.countOrganization({ organizationId: ORGANIZATION_ID });
|
|
86
|
+
expect(get).toHaveBeenLastCalledWith(`link/organization/${ORGANIZATION_ID}/count`, {
|
|
87
|
+
params: {},
|
|
88
|
+
});
|
|
89
|
+
await link.getStats({ linkId: LINK_ID }, { granularity: LinkStatsGranularity.WEEK });
|
|
90
|
+
expect(get).toHaveBeenLastCalledWith(`link/${LINK_ID}/stats`, {
|
|
91
|
+
params: { granularity: LinkStatsGranularity.WEEK },
|
|
92
|
+
});
|
|
93
|
+
await link.getVisits({ linkId: LINK_ID }, { page: 3 });
|
|
94
|
+
expect(get).toHaveBeenLastCalledWith(`link/${LINK_ID}/visits`, { params: { page: 3 } });
|
|
95
|
+
await link.getByOrigin(ORIGIN);
|
|
96
|
+
expect(get).toHaveBeenLastCalledWith('link/origin', { params: ORIGIN });
|
|
97
|
+
await link.getStatsByOrigin({ ...ORIGIN, from: '2026-11-01' });
|
|
98
|
+
expect(get).toHaveBeenLastCalledWith('link/origin/stats', {
|
|
99
|
+
params: { ...ORIGIN, from: '2026-11-01' },
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('writes through the user routes', async () => {
|
|
104
|
+
const { link } = AbyssLinkCore.sdk;
|
|
105
|
+
await link.create({ targetUrl: 'https://example.com' });
|
|
106
|
+
expect(post).toHaveBeenLastCalledWith('link', { targetUrl: 'https://example.com' });
|
|
107
|
+
await link.createForOrganization(
|
|
108
|
+
{ organizationId: ORGANIZATION_ID },
|
|
109
|
+
{ targetUrl: 'https://example.com', projectId: PROJECT_ID },
|
|
110
|
+
);
|
|
111
|
+
expect(post).toHaveBeenLastCalledWith(`link/organization/${ORGANIZATION_ID}`, {
|
|
112
|
+
targetUrl: 'https://example.com',
|
|
113
|
+
projectId: PROJECT_ID,
|
|
114
|
+
});
|
|
115
|
+
await link.update({ linkId: LINK_ID }, { isActive: false });
|
|
116
|
+
expect(put).toHaveBeenLastCalledWith(`link/${LINK_ID}`, { isActive: false });
|
|
117
|
+
await link.transfer({ linkId: LINK_ID }, { newOwnerId: PROJECT_ID });
|
|
118
|
+
expect(post).toHaveBeenLastCalledWith(`link/${LINK_ID}/transfer`, { newOwnerId: PROJECT_ID });
|
|
119
|
+
await link.delete({ linkId: LINK_ID });
|
|
120
|
+
expect(del).toHaveBeenLastCalledWith(`link/${LINK_ID}`);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('calls the admin routes', async () => {
|
|
124
|
+
const { admin } = AbyssLinkCore.sdk.link;
|
|
125
|
+
await admin.paginate({ isProtected: true });
|
|
126
|
+
expect(get).toHaveBeenLastCalledWith('admin/link/paginate', {
|
|
127
|
+
params: { isProtected: true },
|
|
128
|
+
});
|
|
129
|
+
await admin.get({ linkId: LINK_ID });
|
|
130
|
+
expect(get).toHaveBeenLastCalledWith(`admin/link/${LINK_ID}`);
|
|
131
|
+
await admin.getVisits({ linkId: LINK_ID }, { limit: 10 });
|
|
132
|
+
expect(get).toHaveBeenLastCalledWith(`admin/link/${LINK_ID}/visits`, {
|
|
133
|
+
params: { limit: 10 },
|
|
134
|
+
});
|
|
135
|
+
await admin.getCampaignStats(
|
|
136
|
+
{ linkId: LINK_ID },
|
|
137
|
+
{ from: '2026-11-01T00:00:00.000Z', to: '2026-11-11T00:00:00.000Z' },
|
|
138
|
+
);
|
|
139
|
+
expect(get).toHaveBeenLastCalledWith(`admin/link/${LINK_ID}/campaign-stats`, {
|
|
140
|
+
params: { from: '2026-11-01T00:00:00.000Z', to: '2026-11-11T00:00:00.000Z' },
|
|
141
|
+
});
|
|
142
|
+
await admin.setProtection({ linkId: LINK_ID }, { isProtected: true, adminId: PROJECT_ID });
|
|
143
|
+
expect(put).toHaveBeenLastCalledWith(`admin/link/${LINK_ID}/protection`, {
|
|
144
|
+
isProtected: true,
|
|
145
|
+
adminId: PROJECT_ID,
|
|
146
|
+
});
|
|
147
|
+
await admin.update({ linkId: LINK_ID }, { isActive: false });
|
|
148
|
+
expect(put).toHaveBeenLastCalledWith(`admin/link/${LINK_ID}`, { isActive: false });
|
|
149
|
+
await admin.delete({ linkId: LINK_ID });
|
|
150
|
+
expect(del).toHaveBeenLastCalledWith(`admin/link/${LINK_ID}`);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('calls the product routes (T3.1)', async () => {
|
|
154
|
+
const { admin } = AbyssLinkCore.sdk.link;
|
|
155
|
+
const scope = { userId: null, organizationId: ORGANIZATION_ID, projectId: PROJECT_ID };
|
|
156
|
+
await admin.create({
|
|
157
|
+
kind: LinkKind.RESOURCE,
|
|
158
|
+
origin: ORIGIN,
|
|
159
|
+
targetUrl: 'https://beam.example.com/b/x',
|
|
160
|
+
scope,
|
|
161
|
+
});
|
|
162
|
+
expect(post).toHaveBeenLastCalledWith('admin/link', {
|
|
163
|
+
kind: LinkKind.RESOURCE,
|
|
164
|
+
origin: ORIGIN,
|
|
165
|
+
targetUrl: 'https://beam.example.com/b/x',
|
|
166
|
+
scope,
|
|
167
|
+
});
|
|
168
|
+
await admin.getByOrigin(ORIGIN);
|
|
169
|
+
expect(get).toHaveBeenLastCalledWith('admin/link/origin', { params: ORIGIN });
|
|
170
|
+
await admin.getStatsByOrigin(ORIGIN, { granularity: LinkStatsGranularity.MONTH });
|
|
171
|
+
expect(get).toHaveBeenLastCalledWith('admin/link/origin/stats', {
|
|
172
|
+
params: { ...ORIGIN, granularity: LinkStatsGranularity.MONTH },
|
|
173
|
+
});
|
|
174
|
+
await admin.getStatsByOrigin(ORIGIN);
|
|
175
|
+
expect(get).toHaveBeenLastCalledWith('admin/link/origin/stats', { params: ORIGIN });
|
|
176
|
+
const view = { token: 'Ab3dE5gH9kLm', ip: '203.0.113.7', utm: { utmSource: 'mail' } };
|
|
177
|
+
await admin.recordCampaignView(view);
|
|
178
|
+
expect(post).toHaveBeenLastCalledWith('admin/link/campaign-view', view);
|
|
179
|
+
await admin.listCampaignTokens(ORIGIN);
|
|
180
|
+
expect(get).toHaveBeenLastCalledWith('admin/link/campaign-token', { params: ORIGIN });
|
|
181
|
+
await admin.deleteCampaignToken({ linkId: LINK_ID });
|
|
182
|
+
expect(del).toHaveBeenLastCalledWith(`admin/link/campaign-token/${LINK_ID}`);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('returns the response body', async () => {
|
|
186
|
+
get.mockResolvedValueOnce({ data: { data: { link: { id: LINK_ID } } } });
|
|
187
|
+
await expect(AbyssLinkCore.sdk.link.get({ linkId: LINK_ID })).resolves.toEqual({
|
|
188
|
+
data: { link: { id: LINK_ID } },
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -16,7 +16,7 @@ describe('AbyssLinkCore SDK surface', () => {
|
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
it('exposes exactly the three abyss.admin calls', () => {
|
|
19
|
-
expect(Object.keys(AbyssLinkCore.sdk)).toEqual(['abyss']);
|
|
19
|
+
expect(Object.keys(AbyssLinkCore.sdk)).toEqual(['abyss', 'link']);
|
|
20
20
|
expect(Object.keys(AbyssLinkCore.sdk.abyss)).toEqual(['admin']);
|
|
21
21
|
expect(Object.keys(AbyssLinkCore.sdk.abyss.admin).sort()).toEqual([
|
|
22
22
|
'activateUserService',
|
|
@@ -15,4 +15,17 @@ export enum Code {
|
|
|
15
15
|
internalServerError = 'internal_server_error',
|
|
16
16
|
serviceUnavailable = 'service_unavailable',
|
|
17
17
|
invalidData = 'invalid_data',
|
|
18
|
+
|
|
19
|
+
// Link management (422 / 409 / 403 with an explicit code the front can name)
|
|
20
|
+
linkSlugForbidden = 'link_slug_forbidden',
|
|
21
|
+
linkTargetForbidden = 'link_target_forbidden',
|
|
22
|
+
linkSlugTaken = 'link_slug_taken',
|
|
23
|
+
linkProtected = 'link_protected',
|
|
24
|
+
linkManagedByProduct = 'link_managed_by_product',
|
|
25
|
+
// 422: the product resource already holds LINK_CAMPAIGN_TOKENS_PER_PROJECT campaign tokens (O5)
|
|
26
|
+
linkCampaignTokenLimit = 'link_campaign_token_limit',
|
|
27
|
+
|
|
28
|
+
// Link unlock (401 wrong password, 410 a link that can no longer be followed)
|
|
29
|
+
linkWrongPassword = 'link_wrong_password',
|
|
30
|
+
linkExpired = 'link_expired',
|
|
18
31
|
}
|
package/src/types/enum/index.ts
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission of the caller on a link, from abyss-main's content sharing (spec §5). Same values as
|
|
3
|
+
* `SharePermission` of `@abyss-project/main`, which this SDK does not depend on.
|
|
4
|
+
*/
|
|
5
|
+
export enum LinkSharePermission {
|
|
6
|
+
VIEW = 'VIEW',
|
|
7
|
+
EDIT = 'EDIT',
|
|
8
|
+
MANAGE = 'MANAGE',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Scope of the organization list, same values as `ContentShareScope` of `@abyss-project/main`. */
|
|
12
|
+
export enum LinkShareScope {
|
|
13
|
+
ORGANIZATION = 'ORGANIZATION',
|
|
14
|
+
ORG_USER = 'ORG_USER',
|
|
15
|
+
ORG_TEAM = 'ORG_TEAM',
|
|
16
|
+
ORG_PROJECT = 'ORG_PROJECT',
|
|
17
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { LinkKind } from '../../../enum/link.enum';
|
|
2
|
+
import { ILinkOrigin } from '../../models/link.dto';
|
|
3
|
+
import { QueryPaginate } from '../type-message/base-paginate';
|
|
4
|
+
import { IGetLinkStatsQuery, ILinkIdParams } from './link.request';
|
|
5
|
+
|
|
6
|
+
export interface IPaginateLinkAdminQuery extends QueryPaginate {
|
|
7
|
+
search?: string;
|
|
8
|
+
kind?: LinkKind;
|
|
9
|
+
userId?: string;
|
|
10
|
+
projectId?: string;
|
|
11
|
+
organizationId?: string;
|
|
12
|
+
isActive?: boolean;
|
|
13
|
+
isProtected?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type IGetLinkAdminParams = ILinkIdParams;
|
|
17
|
+
export type IGetVisitsLinkAdminParams = ILinkIdParams;
|
|
18
|
+
export type IGetVisitsLinkAdminQuery = QueryPaginate;
|
|
19
|
+
export type IGetCampaignStatsLinkAdminParams = ILinkIdParams;
|
|
20
|
+
export type ISetProtectionLinkAdminParams = ILinkIdParams;
|
|
21
|
+
export type IUpdateLinkAdminParams = ILinkIdParams;
|
|
22
|
+
export type IDeleteLinkAdminParams = ILinkIdParams;
|
|
23
|
+
|
|
24
|
+
/** Human visits of `[from, to)`, read from the raw visits: days older than the retention count nothing. */
|
|
25
|
+
export interface IGetCampaignStatsLinkAdminQuery {
|
|
26
|
+
from: Date | string;
|
|
27
|
+
to: Date | string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ISetProtectionLinkAdminBody {
|
|
31
|
+
isProtected: boolean;
|
|
32
|
+
/** The back-office administrator, recorded on the link and in abyss-main's audit. */
|
|
33
|
+
adminId: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** What the back-office changes on a link: no password, which stays the owner's. */
|
|
37
|
+
export interface IUpdateLinkAdminBody {
|
|
38
|
+
targetUrl?: string;
|
|
39
|
+
slug?: string;
|
|
40
|
+
title?: string | null;
|
|
41
|
+
description?: string | null;
|
|
42
|
+
isActive?: boolean;
|
|
43
|
+
expiresAt?: Date | string | null;
|
|
44
|
+
maxClicks?: number | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The kinds a product creates through `sdk.link.admin.create`; SHORTENED belongs to the user. */
|
|
48
|
+
export type LinkProductKind = LinkKind.RESOURCE | LinkKind.CAMPAIGN_TOKEN;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The container of a product's link (spec §4): kept for the deletion fan-out and the access, never
|
|
52
|
+
* a billing scope (a product's link is free on Link's lines).
|
|
53
|
+
*/
|
|
54
|
+
export interface ILinkContainerScope {
|
|
55
|
+
userId: string | null;
|
|
56
|
+
organizationId: string | null;
|
|
57
|
+
projectId: string | null;
|
|
58
|
+
/** The user acting in the product, when there is one. */
|
|
59
|
+
createdByUserId?: string | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A RESOURCE address (`<host>/<prefix>/<code>`) or a CAMPAIGN_TOKEN for a product resource. A
|
|
64
|
+
* RESOURCE is one per origin: a second call returns the existing link. A campaign token is a new
|
|
65
|
+
* token on each call, at most `LINK_CAMPAIGN_TOKENS_PER_PROJECT` per origin (422
|
|
66
|
+
* `link_campaign_token_limit` beyond).
|
|
67
|
+
*/
|
|
68
|
+
export interface ICreateLinkAdminBody {
|
|
69
|
+
kind: LinkProductKind;
|
|
70
|
+
origin: ILinkOrigin;
|
|
71
|
+
targetUrl: string;
|
|
72
|
+
scope: ILinkContainerScope;
|
|
73
|
+
/** A label the product shows, for example the channel of a campaign token. */
|
|
74
|
+
title?: string | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The product resource, flattened in the query string. */
|
|
78
|
+
export type IGetByOriginLinkAdminQuery = ILinkOrigin;
|
|
79
|
+
export type IGetStatsByOriginLinkAdminQuery = ILinkOrigin & IGetLinkStatsQuery;
|
|
80
|
+
export type IListCampaignTokensLinkAdminQuery = ILinkOrigin;
|
|
81
|
+
export type IDeleteCampaignTokenLinkAdminParams = ILinkIdParams;
|
|
82
|
+
|
|
83
|
+
/** The campaign parameters of a page view, in the SDK's camelCase. */
|
|
84
|
+
export interface ILinkVisitCampaign {
|
|
85
|
+
utmSource?: string;
|
|
86
|
+
utmMedium?: string;
|
|
87
|
+
utmCampaign?: string;
|
|
88
|
+
utmTerm?: string;
|
|
89
|
+
utmContent?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A page view a product reports for a campaign token, after answering its visitor. Link hashes the
|
|
94
|
+
* visitor with the day's salt, looks the country and the city up locally and discards the IP: it is
|
|
95
|
+
* never stored, logged nor sent on.
|
|
96
|
+
*/
|
|
97
|
+
export interface IRecordCampaignViewLinkAdminBody {
|
|
98
|
+
token: string;
|
|
99
|
+
ip: string;
|
|
100
|
+
userAgent?: string;
|
|
101
|
+
/** The full referer: only its domain is kept. */
|
|
102
|
+
referer?: string;
|
|
103
|
+
utm?: ILinkVisitCampaign;
|
|
104
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface IUnlockLinkPublicParams {
|
|
2
|
+
slug: string;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The password of a shortened link, plus what the visit records about where the visitor came from:
|
|
7
|
+
* the unlock page reads them from the address the Link host sent it to (`?ref`, `?lang`, `utm_*`).
|
|
8
|
+
*/
|
|
9
|
+
export interface IUnlockLinkPublicBody {
|
|
10
|
+
password: string;
|
|
11
|
+
ref?: string;
|
|
12
|
+
lang?: string;
|
|
13
|
+
utmSource?: string;
|
|
14
|
+
utmMedium?: string;
|
|
15
|
+
utmCampaign?: string;
|
|
16
|
+
utmTerm?: string;
|
|
17
|
+
utmContent?: string;
|
|
18
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { LinkShareScope } from '../../../enum/link-share.enum';
|
|
2
|
+
import { LinkStatsGranularity } from '../../../enum/link-stats.enum';
|
|
3
|
+
import { ILinkOrigin } from '../../models/link.dto';
|
|
4
|
+
import { QueryPaginate } from '../type-message/base-paginate';
|
|
5
|
+
|
|
6
|
+
export interface ILinkIdParams {
|
|
7
|
+
linkId: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type IGetLinkParams = ILinkIdParams;
|
|
11
|
+
export type IUpdateLinkParams = ILinkIdParams;
|
|
12
|
+
export type IDeleteLinkParams = ILinkIdParams;
|
|
13
|
+
export type ITransferLinkParams = ILinkIdParams;
|
|
14
|
+
export type IGetLinkStatsParams = ILinkIdParams;
|
|
15
|
+
export type IGetLinkVisitsParams = ILinkIdParams;
|
|
16
|
+
|
|
17
|
+
/** Filters of the SHORTENED lists: `search` matches the title, the slug and the target URL. */
|
|
18
|
+
export interface IPaginateLinkQuery extends QueryPaginate {
|
|
19
|
+
search?: string;
|
|
20
|
+
isActive?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface IPaginateProjectLinkParams {
|
|
24
|
+
projectId: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface IPaginateOrganizationLinkParams {
|
|
28
|
+
organizationId: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface IPaginateOrganizationLinkQuery extends IPaginateLinkQuery {
|
|
32
|
+
scope?: LinkShareScope;
|
|
33
|
+
teamId?: string;
|
|
34
|
+
projectId?: string;
|
|
35
|
+
/** Honoured only for a member holding ORG_FULL_ACCESS. */
|
|
36
|
+
adminView?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ICountOrganizationLinkParams {
|
|
40
|
+
organizationId: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ICountOrganizationLinkQuery {
|
|
44
|
+
adminView?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A SHORTENED link. Without `slug`, a random one is drawn. */
|
|
48
|
+
export interface ICreateLinkBody {
|
|
49
|
+
targetUrl: string;
|
|
50
|
+
slug?: string;
|
|
51
|
+
password?: string;
|
|
52
|
+
title?: string;
|
|
53
|
+
description?: string;
|
|
54
|
+
expiresAt?: Date | string;
|
|
55
|
+
maxClicks?: number;
|
|
56
|
+
forwardCampaignParams?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ICreateForOrganizationLinkParams {
|
|
60
|
+
organizationId: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `projectId` puts the link in a project; `scope: 'USER'` makes it the caller's within the organization. */
|
|
64
|
+
export interface ICreateForOrganizationLinkBody extends ICreateLinkBody {
|
|
65
|
+
projectId?: string;
|
|
66
|
+
scope?: 'USER';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** `password: null` removes the password; `expiresAt: null` and `maxClicks: null` lift the limit. */
|
|
70
|
+
export interface IUpdateLinkBody {
|
|
71
|
+
targetUrl?: string;
|
|
72
|
+
slug?: string;
|
|
73
|
+
password?: string | null;
|
|
74
|
+
title?: string | null;
|
|
75
|
+
description?: string | null;
|
|
76
|
+
isActive?: boolean;
|
|
77
|
+
expiresAt?: Date | string | null;
|
|
78
|
+
maxClicks?: number | null;
|
|
79
|
+
forwardCampaignParams?: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ITransferLinkBody {
|
|
83
|
+
newOwnerId: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Period `[from, to)`, 30 days up to now by default. */
|
|
87
|
+
export interface IGetLinkStatsQuery {
|
|
88
|
+
from?: Date | string;
|
|
89
|
+
to?: Date | string;
|
|
90
|
+
granularity?: LinkStatsGranularity;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type IGetLinkVisitsQuery = QueryPaginate;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The product resource of a RESOURCE address, flattened in the query string: the product page
|
|
97
|
+
* finds its address and its statistics, from EDIT on the resource (spec §5).
|
|
98
|
+
*/
|
|
99
|
+
export type IGetByOriginLinkQuery = ILinkOrigin;
|
|
100
|
+
export type IGetStatsByOriginLinkQuery = ILinkOrigin & IGetLinkStatsQuery;
|