Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/api/methods/api-files.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { ApiBase } from '@/api/methods/api-base.js';
import type { FileGetContentsRequest } from '@/types/file-get-contents-request.js';
import type { FileListKeysRequest } from '@/types/file-list-keys-request.js';
import type {
FileListKeysSinceEventRequest,
FileListKeysSinceEventResult,
} from '@/types/file-list-keys-since-event-request.js';
import type { File } from '@/types/file.js';
import type { FilesListRequest } from '@/types/files-list-request.js';
import type { Key } from '@/types/key.js';
Expand Down Expand Up @@ -90,6 +94,36 @@ export class ApiFiles extends ApiBase {
})) as KeysPaginated;
}

/**
* List {@link Key keys} for the language in the {@link File file} with event-based filtering.
* Fetches all keys with `event=true`, computes the maximum event number, and optionally
* filters to only keys changed since a given event cursor.
*
* This method is designed for incremental sync: pass `sinceEvent` from a previous sync
* to receive only keys that have changed since then.
*
* @param request File list keys since event request config.
* @param config Request config.
* @returns An object containing the filtered keys and the maximum event number.
*/
public async listKeysSinceEvent(
request: FileListKeysSinceEventRequest,
config?: RequestConfig,
): Promise<FileListKeysSinceEventResult> {
const { sinceEvent, ...rest } = request;
const allKeys: Key[] = await this.listKeys({ ...rest, event: true }, config);

const maxEvent: number | null =
allKeys.length > 0 ? allKeys.reduce((max, key) => Math.max(max, key.event ?? 0), 0) : null;

const keys: Key[] =
sinceEvent !== null && sinceEvent !== undefined
? allKeys.filter((key) => (key.event ?? 0) > sinceEvent)
: allKeys;

return { keys, maxEvent };
}

/**
* Get the contents of the {@link File file}.
*
Expand Down
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export * from '@/types/api-client-options.js';
export * from '@/types/export-json-request.js';
export * from '@/types/file-get-contents-request.js';
export * from '@/types/file-list-keys-request.js';
export * from '@/types/file-list-keys-since-event-request.js';
export * from '@/types/file.js';
export * from '@/types/files-list-request.js';
export * from '@/types/format-array-meta.js';
Expand Down
6 changes: 6 additions & 0 deletions src/types/file-list-keys-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,10 @@ export type FileListKeysRequest = {
* Receive also metadata for the key.
*/
metadata?: boolean;

/**
* When true, the response includes an `event` number on each key
* representing the last modification event. Used for incremental sync.
*/
event?: boolean;
};
55 changes: 55 additions & 0 deletions src/types/file-list-keys-since-event-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { File } from '@/types/file.js';
import type { Key } from '@/types/key.js';
import type { Project } from '@/types/project.js';
import type { Locales } from '@localazy/languages';

export type FileListKeysSinceEventRequest = {
/**
* Project object or Project ID.
*/
project: Project | string;

/**
* File object or File ID.
*/
file: File | string;

/**
* Locale code. See Locales enum with all available codes.
*/
lang: `${Locales}`;

/**
* Only return keys with event number greater than this value.
* Pass null or omit to return all keys.
*/
sinceEvent?: number | null;

/**
* Returns also deprecated keys.
*/
deprecated?: boolean;

/**
* Receive additional info such as translation note, whether it's hidden etc.
*/
extra_info?: boolean;

/**
* Receive also metadata for the key.
*/
metadata?: boolean;
};

export type FileListKeysSinceEventResult = {
/**
* The filtered keys (only those with event > sinceEvent, or all if sinceEvent is null).
*/
keys: Key[];

/**
* The maximum event number found across all returned keys (before filtering).
* Null if no keys were returned.
*/
maxEvent: number | null;
};
6 changes: 6 additions & 0 deletions src/types/key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,10 @@ export type Key = {
* Only available when extra_info=true.
*/
limit?: number;

/**
* Event number representing the last modification event for this key.
* Only available when event=true in the request.
*/
event?: number;
};
32 changes: 32 additions & 0 deletions tests/fixtures/full-project/fileKeysWithEvents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"keys": [
{
"id": "_a0000000000000000001",
"key": ["app_title"],
"value": "My Application",
"vid": -7407901648464613000,
"event": 100
},
{
"id": "_a0000000000000000002",
"key": ["welcome_message"],
"value": "Welcome to My Application!",
"vid": -7407901648464613000,
"event": 200
},
{
"id": "_a0000000000000000003",
"key": ["login_button"],
"value": "Log In",
"vid": -7407901648464613000,
"event": 200
},
{
"id": "_a0000000000000000004",
"key": ["headers", "role"],
"value": "Project role",
"vid": -7407781050279235000,
"event": 300
}
]
}
6 changes: 6 additions & 0 deletions tests/fixtures/full-project/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import aiTranslate from '@tests/fixtures/full-project/aiTranslate.json';
import aiTranslatePlurals from '@tests/fixtures/full-project/aiTranslatePlurals.json';
import fileDownload from '@tests/fixtures/full-project/fileDownload.json';
import fileKeys from '@tests/fixtures/full-project/fileKeys.json';
import fileKeysWithEvents from '@tests/fixtures/full-project/fileKeysWithEvents.json';
import files from '@tests/fixtures/full-project/files.json';
import formats from '@tests/fixtures/full-project/formats.json';
import glossary from '@tests/fixtures/full-project/glossary.json';
Expand All @@ -25,6 +26,7 @@ export const serverResponses = {
glossary,
files,
fileKeys,
fileKeysWithEvents,
fileDownload,
screenshots,
screenshotTags,
Expand Down Expand Up @@ -105,6 +107,10 @@ export const mockResponses = (): void => {
`${baseUrl}/projects/_a0000000000000000001/files/_e000000000001/keys/en?next=`,
serverResponses.fileKeys,
);
fetchMock.get(
`${baseUrl}/projects/_a0000000000000000001/files/_e000000000001/keys/en?event=true&next=`,
serverResponses.fileKeysWithEvents,
);
fetchMock.put(
`${baseUrl}/projects/_a0000000000000000001/keys/_a0000000000000000001`,
serverResponses.resultPut,
Expand Down
42 changes: 42 additions & 0 deletions tests/specs/files.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
File,
FileGetContentsRequest,
FileListKeysRequest,
FileListKeysSinceEventResult,
Key,
KeysPaginated,
Project,
Expand Down Expand Up @@ -102,6 +103,47 @@ describe('Files', (): void => {
expect(keys.length).toBe(7);
});

test('api.files.listKeysSinceEvent returns all keys and maxEvent when sinceEvent is null', async (): Promise<void> => {
const file: File = await api.files.first({ project });
const result: FileListKeysSinceEventResult = await api.files.listKeysSinceEvent({
project,
file,
lang: Locales.ENGLISH,
sinceEvent: null,
});

expect(result.keys.length).toBe(4);
expect(result.maxEvent).toBe(300);
});

test('api.files.listKeysSinceEvent filters keys by sinceEvent', async (): Promise<void> => {
const file: File = await api.files.first({ project });
const result: FileListKeysSinceEventResult = await api.files.listKeysSinceEvent({
project,
file,
lang: Locales.ENGLISH,
sinceEvent: 100,
});

// Keys with event 200 and 300 (3 keys), excludes event 100
expect(result.keys.length).toBe(3);
expect(result.maxEvent).toBe(300);
expect(result.keys.every((k) => (k.event ?? 0) > 100)).toBe(true);
});

test('api.files.listKeysSinceEvent returns empty keys when sinceEvent >= maxEvent', async (): Promise<void> => {
const file: File = await api.files.first({ project });
const result: FileListKeysSinceEventResult = await api.files.listKeysSinceEvent({
project,
file,
lang: Locales.ENGLISH,
sinceEvent: 300,
});

expect(result.keys.length).toBe(0);
expect(result.maxEvent).toBe(300);
});

test('api.files.getContents', async (): Promise<void> => {
// const files: File[] = await api.files.list({ project });
const file: File = await api.files.first({ project });
Expand Down
Loading