@nickchristensen/ppls 1.0.5 → 1.2.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.
@@ -1,14 +1,178 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { dateLike } from '../../flags/date-like.js';
3
+ import { isDateOnly } from '../../helpers/date-utils.js';
1
4
  import { ListCommand } from '../../list-command.js';
5
+ const DATE_RANGE_FLAGS = {
6
+ added: {
7
+ after: 'added-after',
8
+ before: 'added-before',
9
+ },
10
+ created: {
11
+ after: 'created-after',
12
+ before: 'created-before',
13
+ },
14
+ modified: {
15
+ after: 'modified-after',
16
+ before: 'modified-before',
17
+ },
18
+ };
19
+ const addDateRangeParams = (params, field, flags) => {
20
+ const { after, before } = DATE_RANGE_FLAGS[field];
21
+ const typedFlags = flags;
22
+ const afterValue = typedFlags[after];
23
+ const beforeValue = typedFlags[before];
24
+ if (afterValue) {
25
+ const key = isDateOnly(afterValue) ? `${field}__date__gte` : `${field}__gte`;
26
+ params[key] = afterValue;
27
+ }
28
+ if (beforeValue) {
29
+ const key = isDateOnly(beforeValue) ? `${field}__date__lte` : `${field}__lte`;
30
+ params[key] = beforeValue;
31
+ }
32
+ };
2
33
  export default class DocumentsList extends ListCommand {
3
34
  static description = 'List documents';
4
35
  static examples = ['<%= config.bin %> <%= command.id %>'];
36
+ static flags = {
37
+ ...ListCommand.baseFlags,
38
+ 'added-after': dateLike({
39
+ description: 'Filter by added date (YYYY-MM-DD) or datetime (ISO 8601) >= value',
40
+ exclusive: ['id-in'],
41
+ helpGroup: 'FILTER',
42
+ }),
43
+ 'added-before': dateLike({
44
+ description: 'Filter by added date (YYYY-MM-DD) or datetime (ISO 8601) <= value',
45
+ exclusive: ['id-in'],
46
+ helpGroup: 'FILTER',
47
+ }),
48
+ correspondent: Flags.integer({
49
+ delimiter: ',',
50
+ description: 'Filter by correspondent ids (repeatable or comma-separated)',
51
+ exclusive: ['id-in'],
52
+ helpGroup: 'FILTER',
53
+ multiple: true,
54
+ }),
55
+ 'correspondent-not': Flags.integer({
56
+ delimiter: ',',
57
+ description: 'Exclude correspondent ids (repeatable or comma-separated)',
58
+ exclusive: ['id-in'],
59
+ helpGroup: 'FILTER',
60
+ multiple: true,
61
+ }),
62
+ 'created-after': dateLike({
63
+ description: 'Filter by created date (YYYY-MM-DD) or datetime (ISO 8601) >= value',
64
+ exclusive: ['id-in'],
65
+ helpGroup: 'FILTER',
66
+ }),
67
+ 'created-before': dateLike({
68
+ description: 'Filter by created date (YYYY-MM-DD) or datetime (ISO 8601) <= value',
69
+ exclusive: ['id-in'],
70
+ helpGroup: 'FILTER',
71
+ }),
72
+ 'document-type': Flags.integer({
73
+ delimiter: ',',
74
+ description: 'Filter by document type ids (repeatable or comma-separated)',
75
+ exclusive: ['id-in'],
76
+ helpGroup: 'FILTER',
77
+ multiple: true,
78
+ }),
79
+ 'document-type-not': Flags.integer({
80
+ delimiter: ',',
81
+ description: 'Exclude document type ids (repeatable or comma-separated)',
82
+ exclusive: ['id-in'],
83
+ helpGroup: 'FILTER',
84
+ multiple: true,
85
+ }),
86
+ 'modified-after': dateLike({
87
+ description: 'Filter by modified date (YYYY-MM-DD) or datetime (ISO 8601) >= value',
88
+ exclusive: ['id-in'],
89
+ helpGroup: 'FILTER',
90
+ }),
91
+ 'modified-before': dateLike({
92
+ description: 'Filter by modified date (YYYY-MM-DD) or datetime (ISO 8601) <= value',
93
+ exclusive: ['id-in'],
94
+ helpGroup: 'FILTER',
95
+ }),
96
+ 'no-correspondent': Flags.boolean({
97
+ description: 'Filter documents with no correspondent',
98
+ exclusive: ['correspondent', 'correspondent-not', 'id-in'],
99
+ helpGroup: 'FILTER',
100
+ }),
101
+ 'no-document-type': Flags.boolean({
102
+ description: 'Filter documents with no document type',
103
+ exclusive: ['document-type', 'document-type-not', 'id-in'],
104
+ helpGroup: 'FILTER',
105
+ }),
106
+ 'no-tag': Flags.boolean({
107
+ description: 'Filter documents with no tags',
108
+ exclusive: ['tag', 'tag-all', 'tag-not', 'id-in'],
109
+ helpGroup: 'FILTER',
110
+ }),
111
+ tag: Flags.integer({
112
+ delimiter: ',',
113
+ description: 'Filter by tag ids (repeatable or comma-separated, OR)',
114
+ exclusive: ['id-in'],
115
+ helpGroup: 'FILTER',
116
+ multiple: true,
117
+ }),
118
+ 'tag-all': Flags.integer({
119
+ delimiter: ',',
120
+ description: 'Filter by tag ids (repeatable or comma-separated, AND)',
121
+ exclusive: ['id-in'],
122
+ helpGroup: 'FILTER',
123
+ multiple: true,
124
+ }),
125
+ 'tag-not': Flags.integer({
126
+ delimiter: ',',
127
+ description: 'Exclude tag ids (repeatable or comma-separated)',
128
+ exclusive: ['id-in'],
129
+ helpGroup: 'FILTER',
130
+ multiple: true,
131
+ }),
132
+ };
5
133
  listPath = '/api/documents/';
6
134
  tableAttrs = ['id', 'title', 'created', 'added', 'correspondent', 'document_type', 'tags'];
135
+ extraListFlags(flags) {
136
+ const typedFlags = flags;
137
+ return {
138
+ 'added-after': typedFlags['added-after'],
139
+ 'added-before': typedFlags['added-before'],
140
+ correspondent: typedFlags.correspondent,
141
+ 'correspondent-not': typedFlags['correspondent-not'],
142
+ 'created-after': typedFlags['created-after'],
143
+ 'created-before': typedFlags['created-before'],
144
+ 'document-type': typedFlags['document-type'],
145
+ 'document-type-not': typedFlags['document-type-not'],
146
+ 'modified-after': typedFlags['modified-after'],
147
+ 'modified-before': typedFlags['modified-before'],
148
+ 'no-correspondent': typedFlags['no-correspondent'],
149
+ 'no-document-type': typedFlags['no-document-type'],
150
+ 'no-tag': typedFlags['no-tag'],
151
+ tag: typedFlags.tag,
152
+ 'tag-all': typedFlags['tag-all'],
153
+ 'tag-not': typedFlags['tag-not'],
154
+ };
155
+ }
7
156
  listParams(flags) {
157
+ const typedFlags = flags;
8
158
  const params = super.listParams(flags);
9
159
  delete params.name__icontains;
10
- // eslint-disable-next-line camelcase -- API uses double-underscore field names.
160
+ /* eslint-disable camelcase -- API uses double-underscore field names. */
11
161
  params.title__icontains = flags['name-contains'];
162
+ params.tags__id__in = typedFlags.tag;
163
+ params.tags__id__all = typedFlags['tag-all'];
164
+ params.tags__id__none = typedFlags['tag-not'];
165
+ params.is_tagged = typedFlags['no-tag'] ? 'false' : undefined;
166
+ params.correspondent__id__in = typedFlags.correspondent;
167
+ params.correspondent__id__none = typedFlags['correspondent-not'];
168
+ params.correspondent__isnull = typedFlags['no-correspondent'] ? 'true' : undefined;
169
+ params.document_type__id__in = typedFlags['document-type'];
170
+ params.document_type__id__none = typedFlags['document-type-not'];
171
+ params.document_type__isnull = typedFlags['no-document-type'] ? 'true' : undefined;
172
+ /* eslint-enable camelcase */
173
+ addDateRangeParams(params, 'added', typedFlags);
174
+ addDateRangeParams(params, 'created', typedFlags);
175
+ addDateRangeParams(params, 'modified', typedFlags);
12
176
  return params;
13
177
  }
14
178
  plainTemplate(document) {
@@ -0,0 +1,4 @@
1
+ export declare const dateLike: import("@oclif/core/interfaces").FlagDefinition<string, import("@oclif/core/interfaces").CustomOptions, {
2
+ multiple: false;
3
+ requiredOrDefaulted: false;
4
+ }>;
@@ -0,0 +1,11 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { isDateOnly, isDateTime } from '../helpers/date-utils.js';
3
+ export const dateLike = Flags.custom({
4
+ helpValue: 'YYYY-MM-DD|ISO-8601',
5
+ async parse(input) {
6
+ if (isDateOnly(input) || isDateTime(input)) {
7
+ return input;
8
+ }
9
+ throw new Error('Use YYYY-MM-DD or an ISO 8601 datetime.');
10
+ },
11
+ });
@@ -1,11 +1,11 @@
1
1
  import type { ApiFlags } from './base-command.js';
2
+ import { BaseCommand } from './base-command.js';
2
3
  import { type TableColumn, type TableRow } from './helpers/table.js';
3
- import { PaginatedCommand } from './paginated-command.js';
4
4
  type ListCommandFlags = ApiFlags & {
5
- 'id-in'?: string;
5
+ 'id-in'?: string[];
6
6
  'name-contains'?: string;
7
7
  page?: number;
8
- 'page-size'?: number;
8
+ 'page-size': number;
9
9
  sort?: string;
10
10
  };
11
11
  type ListOutputFlags = ListCommandFlags & {
@@ -14,13 +14,13 @@ type ListOutputFlags = ListCommandFlags & {
14
14
  table?: boolean;
15
15
  };
16
16
  type TableColumnInput = string | TableColumn;
17
- export declare abstract class ListCommand<TRaw extends TableRow = TableRow, TOutput extends TableRow = TRaw> extends PaginatedCommand {
17
+ export declare abstract class ListCommand<TRaw extends TableRow = TableRow, TOutput extends TableRow = TRaw> extends BaseCommand {
18
18
  static baseFlags: {
19
- 'id-in': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ 'id-in': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
20
  'name-contains': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
- sort: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
22
21
  page: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
23
- 'page-size': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
22
+ 'page-size': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
23
+ sort: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
24
24
  'date-format': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
25
25
  header: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
26
26
  hostname: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -30,20 +30,21 @@ export declare abstract class ListCommand<TRaw extends TableRow = TableRow, TOut
30
30
  };
31
31
  protected abstract listPath: string;
32
32
  protected abstract tableAttrs: TableColumnInput[];
33
+ protected extraListFlags(_flags: Record<string, unknown>): Record<string, unknown>;
33
34
  protected fetchListResults<T>(options: {
34
35
  flags: ListCommandFlags;
35
- params?: Record<string, number | string | undefined>;
36
+ params?: Record<string, number | number[] | string | string[] | undefined>;
36
37
  path: string;
37
38
  }): Promise<T[]>;
38
- protected listParams(flags: ListCommandFlags): Record<string, number | string | undefined>;
39
+ protected listParams(flags: ListCommandFlags): Record<string, number | number[] | string | string[] | undefined>;
39
40
  protected abstract plainTemplate(item: TOutput): null | string | undefined;
40
41
  protected renderListOutput(options: {
41
42
  flags: ListOutputFlags;
42
43
  results: TOutput[];
43
44
  }): void;
44
45
  run(): Promise<TOutput[]>;
45
- protected shouldAutoPaginate(flags: ListCommandFlags): boolean;
46
46
  protected transformResult(result: TRaw): TOutput;
47
47
  protected transformResults(results: TRaw[]): TOutput[];
48
+ private buildListUrl;
48
49
  }
49
50
  export {};
@@ -1,22 +1,40 @@
1
1
  import { Flags } from '@oclif/core';
2
+ import { BaseCommand } from './base-command.js';
2
3
  import { createValueFormatter } from './helpers/table.js';
3
- import { PaginatedCommand } from './paginated-command.js';
4
- export class ListCommand extends PaginatedCommand {
4
+ export class ListCommand extends BaseCommand {
5
5
  static baseFlags = {
6
- ...PaginatedCommand.baseFlags,
6
+ ...BaseCommand.baseFlags,
7
7
  'id-in': Flags.string({
8
- description: 'Filter by id list (comma-separated)',
8
+ delimiter: ',',
9
+ description: 'Filter by id list (repeatable or comma-separated)',
9
10
  exclusive: ['name-contains'],
11
+ helpGroup: 'FILTER',
12
+ multiple: true,
10
13
  }),
11
14
  'name-contains': Flags.string({
12
15
  description: 'Filter by name substring',
13
16
  exclusive: ['id-in'],
17
+ helpGroup: 'FILTER',
18
+ }),
19
+ page: Flags.integer({
20
+ dependsOn: ['page-size'],
21
+ description: 'Page number to fetch',
22
+ min: 1,
23
+ }),
24
+ 'page-size': Flags.integer({
25
+ default: async ({ flags }) => (flags.page === undefined ? Number.MAX_SAFE_INTEGER : undefined),
26
+ defaultHelp: async () => 'disable pagination, all results',
27
+ description: 'Number of results per page',
28
+ min: 1,
14
29
  }),
15
30
  sort: Flags.string({ description: 'Sort results by the provided field' }),
16
31
  };
32
+ extraListFlags(_flags) {
33
+ return {};
34
+ }
17
35
  async fetchListResults(options) {
18
36
  const { flags, params = {}, path } = options;
19
- const url = this.buildPaginatedUrlFromFlags({
37
+ const url = this.buildListUrl({
20
38
  flags,
21
39
  params: {
22
40
  ordering: flags.sort,
@@ -24,11 +42,14 @@ export class ListCommand extends PaginatedCommand {
24
42
  },
25
43
  path,
26
44
  });
27
- return this.fetchPaginatedResultsFromFlags({
28
- autoPaginate: this.shouldAutoPaginate(flags),
29
- flags,
30
- url,
31
- });
45
+ const spinner = this.startSpinner(`Fetching ${url.pathname}`);
46
+ try {
47
+ const payload = await this.fetchJson(url, flags.token, flags.headers);
48
+ return payload.results ?? [];
49
+ }
50
+ finally {
51
+ spinner?.stop();
52
+ }
32
53
  }
33
54
  listParams(flags) {
34
55
  return {
@@ -67,6 +88,7 @@ export class ListCommand extends PaginatedCommand {
67
88
  'page-size': flags['page-size'],
68
89
  sort: flags.sort,
69
90
  token: apiFlags.token,
91
+ ...this.extraListFlags(flags),
70
92
  };
71
93
  const outputFlags = {
72
94
  ...listFlags,
@@ -86,13 +108,20 @@ export class ListCommand extends PaginatedCommand {
86
108
  });
87
109
  return results;
88
110
  }
89
- shouldAutoPaginate(flags) {
90
- return flags.page === undefined && flags['page-size'] === undefined;
91
- }
92
111
  transformResult(result) {
93
112
  return result;
94
113
  }
95
114
  transformResults(results) {
96
115
  return results.map((result) => this.transformResult(result));
97
116
  }
117
+ buildListUrl(options) {
118
+ const { flags, params = {}, path } = options;
119
+ const { page } = flags;
120
+ const pageSize = flags['page-size'];
121
+ return this.buildApiUrl(flags.hostname, path, {
122
+ ...params,
123
+ page,
124
+ 'page_size': pageSize,
125
+ });
126
+ }
98
127
  }