erpnext-queue-client 2.7.8 → 2.8.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.
@@ -10,7 +10,7 @@ export declare class ERPNextDoctypeResourceRequest<TModel extends AnyZodObject>
10
10
  protected resourceModel: TModel;
11
11
  protected baseRequest: ERPNextResourceRequest;
12
12
  constructor(temporalClient: TemporalClient, resourceName: string, resourceModel: TModel);
13
- getList<TFieldOptions extends KeysOfModel<TModel>, TSelectedFields extends readonly TFieldOptions[] | readonly ["*"] | undefined = undefined, TAsDict extends boolean | undefined = undefined>(args: {
13
+ getList<TFieldOptions extends KeysOfModel<TModel>, TSelectedFields extends readonly TFieldOptions[] | readonly ["*"] | undefined = undefined, TAsDict extends boolean | undefined = undefined, TReturnPaginationMeta extends boolean | undefined = undefined>(args: {
14
14
  fields?: TSelectedFields;
15
15
  filters?: (string | string[])[][];
16
16
  skip?: number;
@@ -18,9 +18,10 @@ export declare class ERPNextDoctypeResourceRequest<TModel extends AnyZodObject>
18
18
  priority?: number;
19
19
  asDict?: TAsDict;
20
20
  params?: Record<string, string>;
21
+ returnPaginationMeta?: TReturnPaginationMeta;
21
22
  resourceModel?: undefined;
22
- }): Promise<GetListReturnValue<TModel, TFieldOptions, TSelectedFields, TAsDict>>;
23
- getList<TOverrideModel extends AnyZodObject, TFieldOptions extends KeysOfModel<TOverrideModel>, TSelectedFields extends readonly TFieldOptions[] | readonly ["*"] | undefined = undefined, TAsDict extends boolean | undefined = undefined>(args: {
23
+ }): Promise<GetListReturnValue<TModel, TFieldOptions, TSelectedFields, TAsDict, TReturnPaginationMeta>>;
24
+ getList<TOverrideModel extends AnyZodObject, TFieldOptions extends KeysOfModel<TOverrideModel>, TSelectedFields extends readonly TFieldOptions[] | readonly ["*"] | undefined = undefined, TAsDict extends boolean | undefined = undefined, TReturnPaginationMeta extends boolean | undefined = undefined>(args: {
24
25
  fields?: TSelectedFields;
25
26
  filters?: (string | string[])[][];
26
27
  skip?: number;
@@ -29,7 +30,8 @@ export declare class ERPNextDoctypeResourceRequest<TModel extends AnyZodObject>
29
30
  priority?: number;
30
31
  asDict?: TAsDict;
31
32
  params?: Record<string, string>;
32
- }): Promise<GetListReturnValue<TOverrideModel, TFieldOptions, TSelectedFields, TAsDict>>;
33
+ returnPaginationMeta?: TReturnPaginationMeta;
34
+ }): Promise<GetListReturnValue<TOverrideModel, TFieldOptions, TSelectedFields, TAsDict, TReturnPaginationMeta>>;
33
35
  getById({ resourceId, priority, }: {
34
36
  resourceId: string;
35
37
  priority?: number;
@@ -13,7 +13,7 @@ class ERPNextDoctypeResourceRequest {
13
13
  this.resourceModel = resourceModel.describe(resourceName);
14
14
  this.baseRequest = new resourceRequest_1.ERPNextResourceRequest(this.temporalClient);
15
15
  }
16
- async getList({ fields, filters, skip, limit, resourceModel, priority = 5, asDict, params, } = {}) {
16
+ async getList({ fields, filters, skip, limit, resourceModel, priority = 5, asDict, params, returnPaginationMeta, } = {}) {
17
17
  return await this.baseRequest.getList({
18
18
  resourceName: this.resourceName,
19
19
  resourceModel: resourceModel ?? this.resourceModel,
@@ -24,6 +24,7 @@ class ERPNextDoctypeResourceRequest {
24
24
  ...(skip !== undefined ? { skip } : {}),
25
25
  ...(params !== undefined ? { params } : {}),
26
26
  ...(asDict !== undefined ? { asDict } : {}),
27
+ ...(returnPaginationMeta !== undefined ? { returnPaginationMeta } : {}),
27
28
  });
28
29
  }
29
30
  async getById({ resourceId, priority = 5, }) {
@@ -2,6 +2,12 @@ import { AnyZodObject, z, ZodArray, ZodNullable, ZodObject, ZodOptional, ZodRawS
2
2
  import { Prettify } from "../../utils/utils";
3
3
  import { KeysOf } from "../../utils/zodUtils";
4
4
  import { DocTypeChildTableEntryMeta, DocTypeMeta, SubmittableMeta } from "./ERPNextDocTypeMeta";
5
+ type ListFilterValue = string | string[];
6
+ export type ListFilter<TValue extends ListFilterValue = ListFilterValue> = [
7
+ field: string,
8
+ operator: string,
9
+ value: TValue
10
+ ];
5
11
  export type KeysOfModel<T> = Extract<KeysOf<T extends AnyZodObject ? DocModelListEntryType<T> : never>, string>;
6
12
  /**
7
13
  * Extracts the alias from a field name if it contains " as ".
@@ -16,9 +22,19 @@ type ExtractAlias<T extends string> = T extends `${infer _Before} as ${infer Ali
16
22
  type PickWithAliases<TModel extends AnyZodObject, TFields extends readonly string[]> = {
17
23
  [K in TFields[number] as ExtractAlias<K>]: ExtractAlias<K> extends keyof z.infer<DocModelListEntryType<TModel>> ? z.infer<DocModelListEntryType<TModel>>[ExtractAlias<K>] : never;
18
24
  };
19
- export type GetListReturnValue<TModel, TFieldOptions extends string, TSelectedFields, TAsDict> = Prettify<TAsDict extends false ? Array<Array<string>> : TModel extends AnyZodObject ? TSelectedFields extends undefined ? Array<{
25
+ export type ListPaginationMeta = {
26
+ totalCount: number;
27
+ page?: number;
28
+ pageSize?: number;
29
+ };
30
+ export type GetListPaginatedReturnValue<TListData> = Prettify<{
31
+ page: TListData;
32
+ pagination: ListPaginationMeta;
33
+ }>;
34
+ type GetListData<TModel, TFieldOptions extends string, TSelectedFields, TAsDict> = Prettify<TAsDict extends false ? Array<Array<string>> : TModel extends AnyZodObject ? TSelectedFields extends undefined ? Array<{
20
35
  name: string;
21
36
  }> : TSelectedFields extends readonly ["*"] ? Array<z.infer<DocModelListEntryType<TModel>>> : TSelectedFields extends readonly TFieldOptions[] ? Array<PickWithAliases<TModel, TSelectedFields>> : any : any>;
37
+ export type GetListReturnValue<TModel, TFieldOptions extends string, TSelectedFields, TAsDict, TReturnPaginationMeta extends boolean | undefined = undefined> = TReturnPaginationMeta extends true ? GetListPaginatedReturnValue<GetListData<TModel, TFieldOptions, TSelectedFields, TAsDict>> : GetListData<TModel, TFieldOptions, TSelectedFields, TAsDict>;
22
38
  export declare const LoadDocumentWrapper: <T extends AnyZodObject>(BaseModel: T) => z.ZodObject<{
23
39
  [x: string]: any;
24
40
  } & {
@@ -10,7 +10,9 @@ export declare class ERPNextResourceRequest {
10
10
  protected temporalClient: TemporalClient;
11
11
  constructor(temporalClient: TemporalClient);
12
12
  private getParams;
13
- getList<TModel extends AnyZodObject | undefined = undefined, TFieldOptions extends GetListFieldOptions<TModel> = GetListFieldOptions<TModel>, TSelectedFields extends readonly TFieldOptions[] | readonly ["*"] | undefined = undefined, TAsDict extends boolean | undefined = undefined>({ resourceName, fields, filters, resourceModel, skip, limit, priority, asDict, params, }: {
13
+ private buildListPaginationMeta;
14
+ private getListTotalCount;
15
+ getList<TModel extends AnyZodObject | undefined = undefined, TFieldOptions extends GetListFieldOptions<TModel> = GetListFieldOptions<TModel>, TSelectedFields extends readonly TFieldOptions[] | readonly ["*"] | undefined = undefined, TAsDict extends boolean | undefined = undefined, TReturnPaginationMeta extends boolean | undefined = undefined>({ resourceName, fields, filters, resourceModel, skip, limit, priority, asDict, params, returnPaginationMeta, }: {
14
16
  resourceName: string;
15
17
  fields?: TSelectedFields;
16
18
  filters?: (string | string[])[][];
@@ -20,7 +22,8 @@ export declare class ERPNextResourceRequest {
20
22
  priority?: number;
21
23
  asDict?: TAsDict;
22
24
  params?: Record<string, string>;
23
- }): Promise<GetListReturnValue<TModel, TFieldOptions, TSelectedFields, TAsDict>>;
25
+ returnPaginationMeta?: TReturnPaginationMeta;
26
+ }): Promise<GetListReturnValue<TModel, TFieldOptions, TSelectedFields, TAsDict, TReturnPaginationMeta>>;
24
27
  getById<TModel extends AnyZodObject | undefined = undefined>({ resourceName, resourceId, resourceModel, priority, }: {
25
28
  resourceName: string;
26
29
  resourceId: string;
@@ -45,7 +45,32 @@ class ERPNextResourceRequest {
45
45
  };
46
46
  return allParams;
47
47
  };
48
- async getList({ resourceName, fields, filters, resourceModel, skip, limit, priority = 5, asDict, params, }) {
48
+ buildListPaginationMeta({ totalCount, skip, limit, }) {
49
+ const pagination = { totalCount };
50
+ if (limit !== undefined && Number.isInteger(limit) && limit > 0) {
51
+ pagination.pageSize = limit;
52
+ pagination.page = Math.floor((skip ?? 0) / limit) + 1;
53
+ }
54
+ return pagination;
55
+ }
56
+ async getListTotalCount({ resourceName, filters, priority, params, }) {
57
+ const countSchema = zod_1.z
58
+ .object({
59
+ data: zod_1.z.array(zod_1.z.object({ "count(name)": zod_1.z.number() })),
60
+ })
61
+ .describe("Count Response");
62
+ const paramsString = (0, utils_1.paramsToString)({
63
+ ...this.getParams(["count(name)"], filters, true, params),
64
+ });
65
+ const result = await this.temporalClient.executeERPNextRequestWorkflow(`GET-${resourceName}-List-count`, {
66
+ requestMethod: "GET",
67
+ resourceName,
68
+ responseValidationModel: countSchema,
69
+ params: paramsString,
70
+ }, "erpnext", priority);
71
+ return result.data[0]?.["count(name)"] ?? 0;
72
+ }
73
+ async getList({ resourceName, fields, filters, resourceModel, skip, limit, priority = 5, asDict, params, returnPaginationMeta, }) {
49
74
  const erpNextFields = fields?.length
50
75
  ? fields
51
76
  : ["name"]; // default field is name
@@ -89,7 +114,7 @@ class ERPNextResourceRequest {
89
114
  ? {}
90
115
  : { limit_start: String(skip ?? loopSkip) }),
91
116
  });
92
- const result = await this.temporalClient.executeERPNextRequestWorkflow(`GET-${resourceName.toLowerCase()}-List`, {
117
+ const result = await this.temporalClient.executeERPNextRequestWorkflow(`GET-${resourceName}-List`, {
93
118
  requestMethod: "GET",
94
119
  resourceName,
95
120
  responseValidationModel: schema,
@@ -99,7 +124,23 @@ class ERPNextResourceRequest {
99
124
  results = [...results, ...currentResult];
100
125
  loopSkip = loopSkip + loopLimit;
101
126
  } while (autoPaginate && currentResult.length);
102
- return results;
127
+ if (!returnPaginationMeta) {
128
+ return results;
129
+ }
130
+ const totalCount = await this.getListTotalCount({
131
+ resourceName,
132
+ priority,
133
+ ...(filters !== undefined ? { filters } : {}),
134
+ ...(params !== undefined ? { params } : {}),
135
+ });
136
+ return {
137
+ page: results,
138
+ pagination: this.buildListPaginationMeta({
139
+ totalCount,
140
+ ...(skip !== undefined ? { skip } : {}),
141
+ ...(limit !== undefined ? { limit } : {}),
142
+ }),
143
+ };
103
144
  }
104
145
  async getById({ resourceName, resourceId, resourceModel, priority = 5, }) {
105
146
  if (!resourceId)
@@ -29,9 +29,8 @@ describe("stringifyFiltersForParams", () => {
29
29
  test("adds context when filter serialization fails", () => {
30
30
  const cyclicFilterValue = {};
31
31
  cyclicFilterValue.self = cyclicFilterValue;
32
- const filters = [
33
- ["custom", "=", cyclicFilterValue],
34
- ];
32
+ const filters = [["custom", "=", cyclicFilterValue]];
33
+ // @ts-expect-error - we want to test the error case, filters has wrong format here
35
34
  expect(() => (0, resourceRequest_1.stringifyFiltersForParams)(filters)).toThrow("Failed to serialize ERPNext filters for query params");
36
35
  });
37
36
  test("passes typed getList filters as encoded JSON query params", async () => {
@@ -61,3 +60,91 @@ describe("stringifyFiltersForParams", () => {
61
60
  expect((0, resourceRequest_1.stringifyFiltersForParams)(undefined)).toBeUndefined();
62
61
  });
63
62
  });
63
+ describe("ERPNextResourceRequest.getList returnPaginationMeta", () => {
64
+ const createResourceRequest = (listData, totalCount, mockOptions) => {
65
+ let listRequestCount = 0;
66
+ const executeERPNextRequestWorkflow = vi
67
+ .fn()
68
+ .mockImplementation((_workflowId, requestOptions) => {
69
+ const params = new URLSearchParams(requestOptions.params.slice(1));
70
+ const fields = params.get("fields");
71
+ if (fields?.includes("count(name)")) {
72
+ return Promise.resolve({ data: [{ "count(name)": totalCount }] });
73
+ }
74
+ if (mockOptions?.paginateOnce) {
75
+ listRequestCount += 1;
76
+ return Promise.resolve({
77
+ data: listRequestCount === 1 ? listData : [],
78
+ });
79
+ }
80
+ return Promise.resolve({ data: listData });
81
+ });
82
+ return new resourceRequest_1.ERPNextResourceRequest({
83
+ executeERPNextRequestWorkflow,
84
+ });
85
+ };
86
+ test("returns a plain array by default", async () => {
87
+ const resourceRequest = createResourceRequest([{ name: "ITEM-0001" }], 42);
88
+ const result = await resourceRequest.getList({
89
+ resourceName: "Item",
90
+ fields: ["name"],
91
+ limit: 10,
92
+ skip: 0,
93
+ });
94
+ expect(result).toEqual([{ name: "ITEM-0001" }]);
95
+ });
96
+ test("wraps results with page and pagination when returnPaginationMeta is true", async () => {
97
+ const resourceRequest = createResourceRequest([{ name: "ITEM-0001" }, { name: "ITEM-0002" }], 42);
98
+ const filters = [["disabled", "=", "0"]];
99
+ const result = await resourceRequest.getList({
100
+ resourceName: "Item",
101
+ fields: ["name"],
102
+ filters,
103
+ limit: 10,
104
+ skip: 10,
105
+ returnPaginationMeta: true,
106
+ });
107
+ expect(result).toEqual({
108
+ page: [{ name: "ITEM-0001" }, { name: "ITEM-0002" }],
109
+ pagination: {
110
+ totalCount: 42,
111
+ pageSize: 10,
112
+ page: 2,
113
+ },
114
+ });
115
+ });
116
+ test("requests count(name) with the same filters as the list query", async () => {
117
+ const resourceRequest = createResourceRequest([], 7);
118
+ const filters = [["item_group", "=", "Products"]];
119
+ const executeERPNextRequestWorkflow = resourceRequest
120
+ .temporalClient.executeERPNextRequestWorkflow;
121
+ await resourceRequest.getList({
122
+ resourceName: "Item",
123
+ fields: ["name"],
124
+ filters,
125
+ limit: 5,
126
+ returnPaginationMeta: true,
127
+ });
128
+ const countCall = executeERPNextRequestWorkflow.mock.calls.find((call) => call[1].params.includes("count(name)"));
129
+ const listCall = executeERPNextRequestWorkflow.mock.calls.find((call) => !call[1].params.includes("count(name)"));
130
+ const countParams = new URLSearchParams((countCall?.[1]).params.slice(1));
131
+ const listParams = new URLSearchParams((listCall?.[1]).params.slice(1));
132
+ expect(countParams.get("fields")).toBe('["count(name)"]');
133
+ expect(countParams.get("filters")).toBe(listParams.get("filters"));
134
+ expect(JSON.parse(countParams.get("filters") ?? "")).toEqual(filters);
135
+ });
136
+ test("includes only totalCount when limit is not set", async () => {
137
+ const resourceRequest = createResourceRequest([{ name: "ITEM-0001" }], 3, {
138
+ paginateOnce: true,
139
+ });
140
+ const result = await resourceRequest.getList({
141
+ resourceName: "Item",
142
+ fields: ["name"],
143
+ returnPaginationMeta: true,
144
+ });
145
+ expect(result).toEqual({
146
+ page: [{ name: "ITEM-0001" }],
147
+ pagination: { totalCount: 3 },
148
+ });
149
+ });
150
+ });
package/package.json CHANGED
@@ -29,7 +29,7 @@
29
29
  "winston": "^3.15.0",
30
30
  "zod": "3.25.76"
31
31
  },
32
- "version": "2.7.8",
32
+ "version": "2.8.0",
33
33
  "engines": {
34
34
  "node": ">=22"
35
35
  },