diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 407261c6f1d7e..4853d859b371a 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -71,7 +71,7 @@ as uiSettings within the code. |{kib-repo}blob/{branch}/src/plugins/data_views/README.mdx[dataViews] |The data views API provides a consistent method of structuring and formatting documents -and field lists across the various Kibana apps. Its typically used in conjunction with +and field lists across the various Kibana apps. It's typically used in conjunction with for composing queries. diff --git a/src/plugins/share/server/url_service/short_urls/short_url_client.test.ts b/src/plugins/share/server/url_service/short_urls/short_url_client.test.ts index fe6365d498628..4578f63ca1a6e 100644 --- a/src/plugins/share/server/url_service/short_urls/short_url_client.test.ts +++ b/src/plugins/share/server/url_service/short_urls/short_url_client.test.ts @@ -44,6 +44,15 @@ const setup = () => { }; }; +const tick = (ms: number = 1) => new Promise((r) => setTimeout(r, ms)); + +const until = async (check: () => Promise, pollInterval: number = 1) => { + do { + if (await check()) return; + await tick(pollInterval); + } while (true); +}; + describe('ServerShortUrlClient', () => { describe('.create()', () => { test('can create a short URL', async () => { @@ -72,6 +81,20 @@ describe('ServerShortUrlClient', () => { }, }); }); + + test('initializes "accessDate" and "accessCount" fields on URL creation', async () => { + const { client, locator } = setup(); + const { data } = await client.create({ + locator, + slug: 'lala', + params: { + url: '/app/test#foo/bar/baz', + }, + }); + + expect(data.accessDate).toBeGreaterThan(Date.now() - 1000000); + expect(data.accessCount).toBe(0); + }); }); describe('.resolve()', () => { @@ -85,7 +108,7 @@ describe('ServerShortUrlClient', () => { }); const shortUrl2 = await client.resolve(shortUrl1.data.slug); - expect(shortUrl2.data).toMatchObject(shortUrl1.data); + expect(shortUrl2.data).toStrictEqual(shortUrl1.data); }); test('can create short URL with custom slug', async () => { @@ -128,6 +151,33 @@ describe('ServerShortUrlClient', () => { }) ).rejects.toThrowError(new UrlServiceError(`Slug "lala" already exists.`, 'SLUG_EXISTS')); }); + + test('updates "accessCount" and "accessDate" on URL resolution by slug', async () => { + const { client, locator } = setup(); + const shortUrl1 = await client.create({ + locator, + params: { + url: '/app/test#foo/bar/baz', + }, + }); + + expect(shortUrl1.data.accessDate).toBeGreaterThan(Date.now() - 1000000); + expect(shortUrl1.data.accessCount).toBe(0); + + await client.resolve(shortUrl1.data.slug); + await until(async () => (await client.get(shortUrl1.data.id)).data.accessCount === 1); + const shortUrl2 = await client.get(shortUrl1.data.id); + + expect(shortUrl2.data.accessDate).toBeGreaterThanOrEqual(shortUrl1.data.accessDate); + expect(shortUrl2.data.accessCount).toBe(1); + + await client.resolve(shortUrl1.data.slug); + await until(async () => (await client.get(shortUrl1.data.id)).data.accessCount === 2); + const shortUrl3 = await client.get(shortUrl1.data.id); + + expect(shortUrl3.data.accessDate).toBeGreaterThanOrEqual(shortUrl2.data.accessDate); + expect(shortUrl3.data.accessCount).toBe(2); + }); }); describe('.get()', () => { @@ -141,7 +191,7 @@ describe('ServerShortUrlClient', () => { }); const shortUrl2 = await client.get(shortUrl1.data.id); - expect(shortUrl2.data).toMatchObject(shortUrl1.data); + expect(shortUrl2.data).toStrictEqual(shortUrl1.data); }); test('throws when fetching non-existing short URL', async () => { diff --git a/src/plugins/share/server/url_service/short_urls/short_url_client.ts b/src/plugins/share/server/url_service/short_urls/short_url_client.ts index cecc4c3127135..096b2610b916a 100644 --- a/src/plugins/share/server/url_service/short_urls/short_url_client.ts +++ b/src/plugins/share/server/url_service/short_urls/short_url_client.ts @@ -143,12 +143,29 @@ export class ServerShortUrlClient implements IShortUrlClient { const { storage } = this.dependencies; const record = await storage.getBySlug(slug); const data = this.injectReferences(record); + this.updateAccessFields(record); return { data, }; } + /** + * Access field updates are executed in the background as we don't need to + * wait for them and confirm that they were successful. + */ + protected updateAccessFields(record: ShortUrlRecord) { + const { storage } = this.dependencies; + const { id, ...attributes } = record.data; + storage + .update(id, { + ...attributes, + accessDate: Date.now(), + accessCount: (attributes.accessCount || 0) + 1, + }) + .catch(() => {}); // We are not interested if it succeeds or not. + } + public async delete(id: string): Promise { const { storage } = this.dependencies; await storage.delete(id);