feat: doc status & share status (#14426)

#### PR Dependency Tree


* **PR #14426** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Admin dashboard: view workspace analytics (storage, sync activity, top
shared links) with charts and configurable windows.
* Document analytics tab: see total/unique/guest views and trends over
selectable time windows.
* Last-accessed members: view who last accessed a document, with
pagination.
* Shared links analytics: browse and paginate all shared links with
view/unique/guest metrics and share URLs.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-02-13 01:01:29 +08:00
committed by GitHub
parent b46bf91575
commit b4be9118ad
44 changed files with 5701 additions and 86 deletions
@@ -82,7 +82,7 @@ test('should decode pagination input', async t => {
await app.gql(query, {
input: {
first: 5,
offset: 1,
offset: 0,
after: Buffer.from('4').toString('base64'),
},
});
@@ -90,12 +90,34 @@ test('should decode pagination input', async t => {
t.true(
paginationStub.calledOnceWithExactly({
first: 5,
offset: 1,
offset: 0,
after: '4',
})
);
});
test('should reject mixed pagination cursor and offset', async t => {
const res = await app.POST('/graphql').send({
query,
variables: {
input: {
first: 5,
offset: 1,
after: Buffer.from('4').toString('base64'),
},
},
});
t.is(res.status, 200);
t.truthy(res.body.errors?.length);
t.is(
res.body.errors[0].message,
'pagination.after and pagination.offset cannot be used together'
);
t.is(res.body.errors[0].extensions.status, 400);
t.is(res.body.errors[0].extensions.name, 'BAD_REQUEST');
});
test('should return encode pageInfo', async t => {
const result = paginate(
ITEMS.slice(10, 20),
@@ -1,6 +1,8 @@
import { PipeTransform, Type } from '@nestjs/common';
import { Field, InputType, Int, ObjectType } from '@nestjs/graphql';
import { BadRequest } from '../error';
@InputType()
export class PaginationInput {
/**
@@ -13,11 +15,15 @@ export class PaginationInput {
*/
static decode: PipeTransform<PaginationInput, PaginationInput> = {
transform: value => {
return {
const input = {
...value,
first: Math.min(Math.max(value?.first ?? 10, 1), 100),
offset: Math.max(value?.offset ?? 0, 0),
after: decode(value?.after),
// before: decode(value.before),
};
assertPaginationInput(input);
return input;
},
};
@@ -51,6 +57,18 @@ export class PaginationInput {
// before?: string | null;
}
export function assertPaginationInput(paginationInput?: PaginationInput) {
if (!paginationInput) {
return;
}
if (paginationInput.after && paginationInput.offset > 0) {
throw new BadRequest(
'pagination.after and pagination.offset cannot be used together'
);
}
}
const encode = (input: unknown) => {
let inputStr: string;
if (input instanceof Date) {
@@ -65,7 +83,7 @@ const encode = (input: unknown) => {
const decode = (base64String?: string | null) =>
base64String ? Buffer.from(base64String, 'base64').toString('utf-8') : null;
function encodeWithJson(input: unknown) {
export function encodeWithJson(input: unknown) {
return encode(JSON.stringify(input ?? null));
}