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);