sg-widgets-core 0.1.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.
@@ -0,0 +1,740 @@
1
+ //#region src/field-types.d.ts
2
+ /**
3
+ * Field data types and their filter vocabularies.
4
+ *
5
+ * Every list here is what a live Flow Production Tracking site answered when a
6
+ * probe sent it an unknown operator: the 400 names the legal set. Source:
7
+ * sg-groundtruth corpus, `findings/field_types/*.md` and `findings/017_filter_operators.md`.
8
+ */
9
+ declare const DATA_TYPES: readonly ['text', 'number', 'float', 'percent', 'duration', 'timecode', 'currency', 'checkbox', 'date', 'date_time', 'list', 'status_list', 'entity', 'multi_entity', 'entity_type', 'color', 'image', 'url', 'jsonb', 'serializable', 'uuid', 'calculated', 'summary', 'password', 'pivot_column', 'footage', 'tag_list'];
10
+ type DataType = (typeof DATA_TYPES)[number];
11
+ declare const OPERATORS: readonly ['is', 'is_not', 'in', 'not_in', 'contains', 'not_contains', 'starts_with', 'ends_with', 'greater_than', 'less_than', 'between', 'in_last', 'not_in_last', 'in_next', 'not_in_next', 'in_calendar_day', 'in_calendar_week', 'in_calendar_month', 'in_calendar_year', 'name_is', 'name_contains', 'name_not_contains', 'type_is', 'type_is_not'];
12
+ type Operator = (typeof OPERATORS)[number];
13
+ declare const TIME_UNITS: readonly ['HOUR', 'DAY', 'WEEK', 'MONTH', 'YEAR'];
14
+ type TimeUnit = (typeof TIME_UNITS)[number];
15
+ /**
16
+ * Operators each data type accepts in a `_search` filter. An empty list means the
17
+ * type "cannot be used in a filter" and the API 400s on every operator.
18
+ */
19
+ declare const OPERATORS_BY_TYPE: Readonly<Record<DataType, readonly Operator[]>>;
20
+ /** Shape of the value an operator expects on the wire. */
21
+ type ValueShape = 'scalar' | 'list' | 'range' | 'relative' | 'calendar' | 'string' | 'none';
22
+ declare const VALUE_SHAPE: Readonly<Record<Operator, ValueShape>>;
23
+ /**
24
+ * Operators whose result set includes rows where the field is null.
25
+ * `is_not X` is not the complement of `is X` on Flow PT: every negating operator
26
+ * matches unset rows, while comparisons exclude them.
27
+ */
28
+ declare const NEGATING_OPERATORS: ReadonlySet<Operator>;
29
+ declare function isDataType(value: string): value is DataType;
30
+ declare function isOperator(value: string): value is Operator;
31
+ /** Operators legal for a data type. Unknown types get the equality set, the smallest one the API prints. */
32
+ declare function operatorsFor(dataType: string): readonly Operator[];
33
+ declare function isFilterable(dataType: string): boolean;
34
+ declare function supportsOperator(dataType: string, operator: Operator): boolean;
35
+ /** Types whose scalar value is a `{type, id}` entity hash. */
36
+ declare function isLinkType(dataType: string): boolean;
37
+ /** Types whose scalar value is a number (integer unless noted). */
38
+ declare function isNumericType(dataType: string): boolean;
39
+ declare function isTemporalType(dataType: string): boolean;
40
+ //#endregion
41
+ //#region src/filter.d.ts
42
+ type LogicalOperator = 'and' | 'or';
43
+ interface EntityRef {
44
+ type: string;
45
+ id: number;
46
+ /** `cached_display_name` of the target when known. Never sent to the API. */
47
+ name?: string;
48
+ }
49
+ type Scalar = string | number | boolean | null | EntityRef;
50
+ type ConditionValue = Scalar | Scalar[] | [Scalar, Scalar] | [number, TimeUnit];
51
+ interface FilterCondition {
52
+ kind: 'condition';
53
+ /** Dotted path such as `entity.Shot.code`. */
54
+ path: string;
55
+ operator: Operator;
56
+ value: ConditionValue;
57
+ }
58
+ interface FilterGroup {
59
+ kind: 'group';
60
+ logicalOperator: LogicalOperator;
61
+ conditions: FilterNode[];
62
+ }
63
+ type FilterNode = FilterGroup | FilterCondition;
64
+ /** Wire shapes. */
65
+ type WireCondition = [string, Operator, unknown];
66
+ interface WireGroup {
67
+ logical_operator: LogicalOperator;
68
+ conditions: Array<WireCondition | WireGroup>;
69
+ }
70
+ /**
71
+ * What one type's entry in `_text_search`'s `entity_types` map accepts: a filter
72
+ * array, or a group this converts to one.
73
+ */
74
+ type TextSearchFilter = WireGroup | WireCondition[] | null | undefined;
75
+ /**
76
+ * The filter array `entity_types` wants, `[]` for no filter
77
+ * (post_entity_text_search). The array form is `and` only and cannot express a
78
+ * nested group, so one is refused here rather than sent and guessed at.
79
+ */
80
+ declare function toFilterArray(filter: TextSearchFilter): WireCondition[];
81
+ declare function group(logicalOperator: LogicalOperator, conditions?: FilterNode[]): FilterGroup;
82
+ declare function condition(path: string, operator: Operator, value: ConditionValue): FilterCondition;
83
+ declare function emptyFilter(): FilterGroup;
84
+ /**
85
+ * A condition is blank when it has no path, or when its operator needs a value
86
+ * and none was given. Blank conditions are dropped on serialisation rather than
87
+ * sent as a degenerate filter.
88
+ */
89
+ declare function isBlankCondition(c: FilterCondition): boolean;
90
+ /** True when serialisation would produce no conditions at all. */
91
+ declare function isEmptyFilter(node: FilterNode): boolean;
92
+ /**
93
+ * Serialise a tree to the `api3_hash` `filters` value. Blank conditions and
94
+ * groups that end up empty are dropped. Returns `null` when nothing remains,
95
+ * so callers can omit `filters` rather than send `[]` (which matches every
96
+ * row on the site).
97
+ */
98
+ declare function toApi3Hash(node: FilterNode): WireGroup | null;
99
+ /** Parse a wire group (or a flat `api3_array` list) back into the editor tree. */
100
+ declare function fromWire(wire: WireGroup | WireCondition[]): FilterGroup;
101
+ /** Depth-first walk, useful for collecting referenced paths or validating against a schema. */
102
+ declare function walk(node: FilterNode): Generator<FilterNode>;
103
+ declare function referencedPaths(node: FilterNode): string[];
104
+ //#endregion
105
+ //#region src/schema.d.ts
106
+ /** One property as the API returns it. */
107
+ interface RawProperty<T = unknown> {
108
+ value: T;
109
+ editable: boolean;
110
+ }
111
+ interface RawFieldSchema {
112
+ name: RawProperty<string>;
113
+ entity_type: RawProperty<string>;
114
+ data_type: RawProperty<string>;
115
+ editable: RawProperty<boolean>;
116
+ mandatory: RawProperty<boolean>;
117
+ unique: RawProperty<boolean>;
118
+ properties: Record<string, RawProperty<unknown>>;
119
+ }
120
+ type RawFieldsResponse = {
121
+ data: Record<string, RawFieldSchema>;
122
+ };
123
+ interface FieldSchema {
124
+ /** Programmatic name, e.g. `sg_status_list`. */
125
+ name: string;
126
+ /** Human label, e.g. `Status`. */
127
+ displayName: string;
128
+ entityType: string;
129
+ dataType: DataType | string;
130
+ editable: boolean;
131
+ mandatory: boolean;
132
+ unique: boolean;
133
+ /** For `entity` and `multi_entity`: the types the field may link to. Advisory on most fields. */
134
+ validTypes?: string[];
135
+ /** For `list` and `status_list`: the site-wide vocabulary of codes. */
136
+ validValues?: string[];
137
+ /** For `status_list`: code to label map. Absent when labels equal codes. */
138
+ displayValues?: Record<string, string>;
139
+ /** For `list` and `status_list`, only when read with `project_id`. May contain codes outside `validValues`. */
140
+ hiddenValues?: string[];
141
+ /**
142
+ * The operators the API evaluates on this field, where they are fewer than the
143
+ * vocabulary it advertises. Absent means the whole vocabulary.
144
+ */
145
+ operators?: Operator[];
146
+ defaultValue?: unknown;
147
+ description?: string;
148
+ }
149
+ /**
150
+ * Fields the schema types one way and the API answers another, keyed
151
+ * `<Type>.<field>`. `Note.read_by_current_user` holds the codes `unread` and
152
+ * `read`, never a boolean, so every control derived from it is a list and not a
153
+ * checkbox (067_notes_in_the_stream, entity_types/Note).
154
+ *
155
+ * An entry carries the whole field, because a site may leave it out of the schema
156
+ * and still answer it on the row: `GET /schema/Note/fields` answers 33 fields and
157
+ * no `read_by_current_user` among them, while `GET /schema/Note/fields/read_by_current_user`
158
+ * answers 200 with `data: null`, which is what an undeclared field reads as and a
159
+ * name that is nothing at all answers 404 (068_note_read_state). `normalizeFields`
160
+ * adds what the schema left out. A site that declares the field keeps its own
161
+ * display name and editability and is corrected on the data type, the values and
162
+ * the operators alone.
163
+ *
164
+ * `operators` is the set the API evaluates, which is not always the set it
165
+ * advertises. `read_by_current_user` names `is`, `is_not`, `in` and `not_in` in its
166
+ * own `Valid relations`, and evaluates only the first two: `in`, `not_in` and an
167
+ * `is` value outside the vocabulary all answer 200 with the caller's unread rows
168
+ * whatever the list holds, in `_search` and in `_summarize`, under both body shapes
169
+ * (068_note_read_state).
170
+ *
171
+ * The value is per person: a person's `PUT` answers 200 and the re-read follows it,
172
+ * so the field is editable. A script has no read state, and its own write answers
173
+ * 200 and stores nothing (068_note_read_state).
174
+ */
175
+ declare const FIELD_SCHEMA_OVERRIDES: Readonly<Record<string, Readonly<Omit<FieldSchema, 'name' | 'entityType'>>>>;
176
+ /** What a declared field's schema has to be corrected to, when it is one of those. */
177
+ declare function fieldSchemaOverride(entityType: string, name: string): Readonly<Partial<FieldSchema>> | undefined;
178
+ /** The whole field, for a site whose schema does not declare it; undefined when it is not one of those. */
179
+ declare function undeclaredField(entityType: string, name: string): FieldSchema | undefined;
180
+ declare function normalizeField(name: string, raw: RawFieldSchema): FieldSchema;
181
+ /**
182
+ * Every field of a type, with the fields an override declares and the schema omitted.
183
+ *
184
+ * `entityType` is only needed for a response with no fields at all; otherwise the
185
+ * fields name their own type.
186
+ */
187
+ declare function normalizeFields(response: RawFieldsResponse, entityType?: string): Record<string, FieldSchema>;
188
+ /**
189
+ * The field name a type's own status lives under. Project's is `sg_status`, a plain
190
+ * `list` with the four values a bid goes through and no Status row behind it
191
+ * (probe 018, entity_types/Project).
192
+ */
193
+ declare function statusFieldNameFor(entityType: string): string;
194
+ /**
195
+ * The status field of an entity type. Every type but Project uses `sg_status_list`
196
+ * (data type `status_list`); Project's is `sg_status`, a plain `list`. Prefer the
197
+ * schema when you have it; the name guess is the fallback.
198
+ *
199
+ * The conventional name wins over any other `status_list` field on the type. A site
200
+ * is free to add its own — a client status, a delivery status — and a schema read
201
+ * answers them in no order the caller controls, so picking the first one found makes
202
+ * a type's status whichever field the response happened to list first.
203
+ */
204
+ declare function statusFieldFor(entityType: string, fields?: Record<string, FieldSchema>): FieldSchema | string;
205
+ /**
206
+ * The fields Flow PT conventionally uses as a row's human label, in order.
207
+ * `cached_display_name` is the server's own copy, the same string an entity dict
208
+ * carries as its `name`, and wins when present (060_entity_dict_name).
209
+ */
210
+ declare const DISPLAY_NAME_FIELDS: readonly ['cached_display_name', 'code', 'name', 'title', 'content', 'subject'];
211
+ declare function displayNameOf(attributes: Record<string, unknown> | null | undefined, fallback?: string): string;
212
+ //#endregion
213
+ //#region src/status-icons.d.ts
214
+ /**
215
+ * Stock status icons.
216
+ *
217
+ * A status with an `image_map` icon names a CSS class such as `icon_apr`. The image
218
+ * behind it is one sprite the web app serves unauthenticated at
219
+ * `/images/sg_icon_image_map.png`, positioned by rules in
220
+ * `/dist/production/stylesheets/login.css` (probe 010). Neither is in the REST API.
221
+ * The offsets below are those rules, read once. The data URLs are the sprite cells of
222
+ * every icon the status picker offers (`icon_type: permanent_status`, 94 on the probed
223
+ * site), so a badge renders any stock status icon with no site access.
224
+ *
225
+ * A shipped status is a row with no `created_by`. `system` marks a subset of them
226
+ * (act, dis, ip, na, cfrm, pndng) and no schema flag marks the set; a site may
227
+ * have retired some of the shipped rows (probe 061).
228
+ */
229
+ interface SpriteCell {
230
+ x: number;
231
+ y: number;
232
+ w: number;
233
+ h: number;
234
+ }
235
+ /** Path of the stock sprite on any site. The stylesheet appends a cache-busting query; the bare path answers 200. */
236
+ declare const STOCK_SPRITE_PATH = "/images/sg_icon_image_map.png";
237
+ interface NativeStatus {
238
+ code: string;
239
+ name: string;
240
+ /** `image_map_key` of the shipped icon. `act` has an html icon and no key. */
241
+ imageMapKey: string | null;
242
+ /** Locked by the system: cannot be deleted. */
243
+ system: boolean;
244
+ }
245
+ /** Shipped statuses the probed site keeps live, in id order (probe 061). */
246
+ declare const NATIVE_STATUSES: readonly NativeStatus[];
247
+ /** Sprite cell per `image_map_key`, from the site stylesheet. Covers every stock icon a status may use. */
248
+ declare const STOCK_ICON_CELLS: Readonly<Record<string, SpriteCell>>;
249
+ /** Every stock status icon as a PNG data URL, keyed by `image_map_key`. */
250
+ declare const STOCK_ICON_DATA_URLS: Readonly<Record<string, string>>;
251
+ /**
252
+ * The keys the status picker offers, in the site's order. Annotated pure, so a
253
+ * bundle that imports something else from this module drops the icon data with it.
254
+ */
255
+ declare const STOCK_ICON_KEYS: readonly string[];
256
+ declare function isNativeStatus(code: string): boolean;
257
+ declare function nativeStatus(code: string): NativeStatus | undefined;
258
+ /** How to draw a stock icon: a bundled image, the site's sprite for a key outside the status set, or nothing. */
259
+ type StockIconSource = {
260
+ kind: 'data';
261
+ src: string;
262
+ cell: SpriteCell;
263
+ } | {
264
+ kind: 'sprite';
265
+ src: string;
266
+ cell: SpriteCell;
267
+ } | {
268
+ kind: 'none';
269
+ };
270
+ /**
271
+ * Resolve an `image_map_key`. Status icons come from the bundle. A key outside the
272
+ * status set is still a sprite cell, so it draws only when `siteUrl` is given.
273
+ */
274
+ declare function stockIconSource(imageMapKey: string | null | undefined, siteUrl?: string): StockIconSource;
275
+ /** Inline style for a sprite-backed icon element sized to its cell. */
276
+ declare function spriteStyle(source: Extract<StockIconSource, {
277
+ kind: 'sprite';
278
+ }>): Record<string, string>;
279
+ //#endregion
280
+ //#region src/status.d.ts
281
+ interface StatusOption {
282
+ code: string;
283
+ label: string;
284
+ }
285
+ /** Codes a picker should offer for the project the schema was read with. */
286
+ declare function usableStatuses(field: Pick<FieldSchema, 'validValues' | 'hiddenValues' | 'displayValues'>): StatusOption[];
287
+ /** Codes usable in every one of several projects: the intersection of their usable sets, in the first schema's order. */
288
+ declare function intersectStatuses(fields: Array<Pick<FieldSchema, 'validValues' | 'hiddenValues' | 'displayValues'>>): StatusOption[];
289
+ declare function statusLabel(field: Pick<FieldSchema, 'displayValues'>, code: string): string;
290
+ /**
291
+ * Status entity as `GET /entity/statuses?fields=code,name,bg_color,icon` returns it,
292
+ * flattened. `bg_color` is comma-separated decimal RGB (`"25,118,27"`), never hex.
293
+ */
294
+ interface StatusRecord {
295
+ id: number;
296
+ code: string;
297
+ name: string;
298
+ bgColor: string | null;
299
+ icon: StatusIcon | null;
300
+ }
301
+ /** Icon entity. `displayType` picks one of three renderings (probe 010). */
302
+ type StatusIcon = {
303
+ displayType: 'image_map';
304
+ imageMapKey: string;
305
+ } | {
306
+ displayType: 'image';
307
+ dataUrl: string;
308
+ } | {
309
+ displayType: 'html';
310
+ html: string;
311
+ };
312
+ interface Rgb {
313
+ r: number;
314
+ g: number;
315
+ b: number;
316
+ }
317
+ /** Parse `"25,118,27"` into channels. Returns null for anything else, including hex. */
318
+ declare function parseBgColor(value: string | null | undefined): Rgb | null;
319
+ declare function rgbToCss({ r, g, b }: Rgb): string;
320
+ /** WCAG relative luminance, for choosing a readable foreground on a status badge. */
321
+ declare function relativeLuminance({ r, g, b }: Rgb): number;
322
+ /** Black or white, whichever contrasts more with the given colour. */
323
+ declare function foregroundFor(rgb: Rgb): 'black' | 'white';
324
+ /**
325
+ * What a status paints with: its own `bg_color` and a readable ink on it. Null when
326
+ * the status carries no colour, which is every option of a plain `list` field.
327
+ */
328
+ interface StatusPaint {
329
+ background: string;
330
+ foreground: string;
331
+ }
332
+ declare function statusPaint(status: Pick<StatusRecord, 'bgColor'> | null | undefined): StatusPaint | null;
333
+ /**
334
+ * What a status draws as its glyph (010_status_icons). `image` is a self-contained
335
+ * data URI; `cell` is a bundled sprite cell and `sprite` one the site serves, both
336
+ * sized by that cell; `html` is the label itself, so it replaces the text rather than
337
+ * preceding it; `dot` stands in for a stock key this client has no cell for, and
338
+ * `none` is a status naming no icon at all.
339
+ */
340
+ type StatusGlyph = {
341
+ kind: 'none';
342
+ } | {
343
+ kind: 'html';
344
+ html: string;
345
+ } | {
346
+ kind: 'image';
347
+ src: string;
348
+ } | {
349
+ kind: 'cell';
350
+ imageMapKey: string;
351
+ src: string;
352
+ cell: SpriteCell;
353
+ } | {
354
+ kind: 'sprite';
355
+ imageMapKey: string;
356
+ style: Record<string, string>;
357
+ } | {
358
+ kind: 'dot';
359
+ imageMapKey: string;
360
+ };
361
+ /** The glyph a status draws, from its icon and whichever sprite can serve it. */
362
+ declare function statusGlyph(status: Pick<StatusRecord, 'icon'> | null | undefined, siteUrl?: string): StatusGlyph;
363
+ //#endregion
364
+ //#region src/client.d.ts
365
+ interface SearchOptions {
366
+ filters?: WireGroup | null;
367
+ fields?: string[];
368
+ sort?: string;
369
+ page?: {
370
+ size?: number;
371
+ number?: number;
372
+ };
373
+ }
374
+ /** One row of a `_search` response, kept in the API's shape. */
375
+ interface EntityRow {
376
+ type: string;
377
+ id: number;
378
+ attributes: Record<string, unknown>;
379
+ relationships: Record<string, {
380
+ data: EntityRef | EntityRef[] | null;
381
+ }>;
382
+ }
383
+ interface SearchResult {
384
+ data: EntityRow[];
385
+ /** True when another page exists. Computed from data length, not `links.next`, which is emitted on empty pages too (probe 006). */
386
+ hasMore: boolean;
387
+ }
388
+ /**
389
+ * Row of `_text_search`, which is flattened and not the `_search` shape. There is
390
+ * no `fields` parameter: every row is name, links and status whatever the type, so
391
+ * a caller that needs a thumbnail or a project re-reads with `search`
392
+ * (post_entity_text_search).
393
+ */
394
+ interface TextSearchRow {
395
+ type: string;
396
+ id: number;
397
+ name: string;
398
+ /** The linked row's type and name, `['', '']` when it links to nothing. Two bare strings, not a reference. */
399
+ links: [string, string];
400
+ /** Status code, never a label. */
401
+ status: string | null;
402
+ }
403
+ /**
404
+ * What a navigation node stands for. `entity` carries a `{type, id}`, `entity_type`
405
+ * a bare schema name; other kinds are passed through as the site sends them.
406
+ */
407
+ interface HierarchyRef {
408
+ kind: string;
409
+ value: EntityRef | string | null;
410
+ }
411
+ /** One level of the navigation tree the web interface draws (post_hierarchy_expand). */
412
+ interface HierarchyNode {
413
+ label: string;
414
+ ref: HierarchyRef;
415
+ /** The path to pass back to `hierarchyExpand` to open this node. */
416
+ path: string;
417
+ parentPath: string | null;
418
+ /** False when expanding this node would return nothing. */
419
+ hasChildren: boolean;
420
+ /** One level only: a child's own children come from its own call. */
421
+ children: HierarchyNode[];
422
+ }
423
+ /** Where one row sits in the navigation tree, as `POST /hierarchy/_search` answers it. */
424
+ interface HierarchyPath {
425
+ /** The row's own display name. */
426
+ label: string;
427
+ /** The same breadcrumb rendered for a person. The project is not in it. */
428
+ pathLabel: string;
429
+ /** One path per level, root first; the last entry is the row itself. */
430
+ incrementalPath: string[];
431
+ ref: EntityRef;
432
+ projectId: number | null;
433
+ }
434
+ /** The row a node stands for, or null when it stands for a type or nothing. */
435
+ declare function hierarchyEntity(ref: HierarchyRef | null | undefined): EntityRef | null;
436
+ /**
437
+ * The aggregates `_summarize` offers. The endpoint prints the whole set in the 400 it
438
+ * answers a bogus one (020_summarize).
439
+ */
440
+ type SummaryType = 'record_count' | 'count' | 'sum' | 'maximum' | 'minimum' | 'average' | 'earliest' | 'latest' | 'percentage' | 'status_percentage' | 'status_percentage_as_float' | 'status_list' | 'checked' | 'unchecked';
441
+ interface SummaryField {
442
+ field: string;
443
+ type: SummaryType;
444
+ }
445
+ interface SummaryGrouping {
446
+ field: string;
447
+ /** Default `exact`, one group per distinct value. */
448
+ type?: string;
449
+ direction?: 'asc' | 'desc';
450
+ }
451
+ interface SummarizeOptions {
452
+ filters?: WireGroup | null;
453
+ /** Default `[{field: 'id', type: 'count'}]`. One type per field per call: the last entry wins (020_summarize). */
454
+ summaryFields?: SummaryField[];
455
+ grouping?: SummaryGrouping[];
456
+ }
457
+ interface SummaryGroup {
458
+ /** The server's render of the value, for display. Not unique. */
459
+ groupName: string;
460
+ /** What the grouping was computed on. Key on this (020_summarize). */
461
+ groupValue: unknown;
462
+ summaries: Record<string, number>;
463
+ }
464
+ interface SummarizeResult {
465
+ /** Keyed by field name. A field that cannot be summarized answers 200 with the key absent. */
466
+ summaries: Record<string, number>;
467
+ groups: SummaryGroup[];
468
+ }
469
+ interface EntityTypeInfo {
470
+ name: string;
471
+ displayName: string;
472
+ }
473
+ /**
474
+ * Who wrote a thread row. A Reply's hash carries a fourth key, `image`, a
475
+ * presigned avatar re-signed on every read; the `created_by` hash on a Note and
476
+ * an Attachment has none (get_entity_notes_id_thread_contents).
477
+ */
478
+ interface ThreadAuthor extends EntityRef {
479
+ image?: string | null;
480
+ }
481
+ /** One row of a note thread, flat and not the `_search` shape. */
482
+ interface ThreadRow {
483
+ /** `Note`, `Attachment` or `Reply`. */
484
+ type: string;
485
+ id: number;
486
+ createdAt: string | null;
487
+ /** The body. An Attachment row has none. */
488
+ content: string | null;
489
+ author: ThreadAuthor | null;
490
+ /** The row as it arrived, including whatever `entityFields` widened it by. */
491
+ fields: Record<string, unknown>;
492
+ }
493
+ /**
494
+ * Which rows the event log answers. `meta` holds what changed and takes neither a
495
+ * filter nor a sort, so the cut is made on these and `meta` is read off the row
496
+ * (025_event_log).
497
+ */
498
+ interface EventLogOptions {
499
+ projectId?: number;
500
+ /**
501
+ * The row the events are about. It is null on an event whose target has been
502
+ * deleted, so a deleted row's history is reachable by `eventType` and dates alone.
503
+ */
504
+ entity?: EntityRef;
505
+ /** One event type or several, e.g. `Shotgun_Shot_Change`. */
506
+ eventType?: string | string[];
507
+ /** The field that changed. Site-wide on its own, so pair it with `entity` or `eventType`. */
508
+ attributeName?: string;
509
+ /** Keep events after this `date_time`, as `created_at greater_than`. */
510
+ since?: string;
511
+ /** Keep events before this `date_time`, as `created_at less_than`. */
512
+ until?: string;
513
+ page?: {
514
+ size?: number;
515
+ number?: number;
516
+ };
517
+ }
518
+ /**
519
+ * The cut the event log takes, as the `and` group `_search` wants. Nothing here
520
+ * touches `meta`: it takes no filter (025_event_log).
521
+ */
522
+ declare function eventLogFilters(options?: EventLogOptions): WireGroup;
523
+ /**
524
+ * What an event is read with. `audit_trail` is never returned even when it is
525
+ * named, so it is not asked for (025_event_log).
526
+ */
527
+ declare const EVENT_LOG_FIELDS: readonly ['event_type', 'attribute_name', 'description', 'created_at', 'meta', 'entity', 'project', 'user'];
528
+ /** One event, `meta` decoded, with the two values an attribute change carries lifted out. */
529
+ interface EventLogEntry {
530
+ id: number;
531
+ eventType: string | null;
532
+ attributeName: string | null;
533
+ /** The rendered English sentence the server writes. */
534
+ description: string | null;
535
+ createdAt: string | null;
536
+ /** Null once the row the event is about is deleted; `meta` still names it. */
537
+ entity: EntityRef | null;
538
+ project: EntityRef | null;
539
+ user: EntityRef | null;
540
+ /** The whole decoded `meta`. Its keys follow `meta.type`. */
541
+ meta: Record<string, unknown> | null;
542
+ /** `meta.old_value`, present on an `attribute_change` and nowhere else. */
543
+ oldValue: unknown;
544
+ /** `meta.new_value`, present on an `attribute_change` and nowhere else. */
545
+ newValue: unknown;
546
+ }
547
+ interface EventLogResult {
548
+ data: EventLogEntry[];
549
+ /** True when another page exists. Read from the row count, as on `search` (probe 006). */
550
+ hasMore: boolean;
551
+ }
552
+ /** Bytes to put on a row, and where they land. */
553
+ interface UploadFile {
554
+ /** The name the bytes are stored under. Its extension decides the upload type. */
555
+ filename: string;
556
+ /** The bytes themselves. From a browser `File`, `new Uint8Array(await file.arrayBuffer())`. */
557
+ data: Uint8Array;
558
+ /**
559
+ * The field in the path, which picks the kind: `image` is a Thumbnail, another
560
+ * field an Attachment on that field, and no field at all a generic Attachment on
561
+ * `attachment_links` (recipes/001).
562
+ */
563
+ field?: string;
564
+ }
565
+ /** What the handshake answered. The Attachment it made is visible only on the parent row. */
566
+ interface UploadResult {
567
+ /** `Thumbnail` on `image`, `Attachment` on any other form. Nothing in the request names it. */
568
+ uploadType: string;
569
+ /** The whole `upload_info` the ticket carried and the third call sent back. */
570
+ uploadInfo: Record<string, unknown>;
571
+ /** The storage's `ETag`, when it exposes one; the `PUT` status is the receipt (put_links_upload). */
572
+ etag: string | null;
573
+ }
574
+ /** The two cuts `following` takes. Both are made server-side. */
575
+ interface FollowingOptions {
576
+ /** One type to keep. The schema name and the snake_case plural both work. */
577
+ entity?: string;
578
+ projectId?: number;
579
+ }
580
+ interface SgClient {
581
+ /** Enabled entity types on the site with their display names. */
582
+ entityTypes(): Promise<EntityTypeInfo[]>;
583
+ /** All fields of a type. Pass `projectId` to get `hiddenValues` on status and list fields. */
584
+ fields(entityType: string, projectId?: number): Promise<Record<string, FieldSchema>>;
585
+ /**
586
+ * One field at project scope, 1.2KB against 48KB for the whole type (probe 002).
587
+ * Only `hiddenValues` differs from the site-scope read (probe 009).
588
+ */
589
+ fieldWithProject(entityType: string, field: string, projectId: number): Promise<FieldSchema>;
590
+ search(entityType: string, options: SearchOptions): Promise<SearchResult>;
591
+ /**
592
+ * Free-text search across several types at once. Every word must match, each as
593
+ * a case-insensitive substring of the row's name or of the linked row's name
594
+ * (probe 053). Page size is 1 to 25 and 25 is also the default; there is no
595
+ * `links`, so page until the answer is empty.
596
+ */
597
+ textSearch(text: string, entityTypes: Record<string, TextSearchFilter>, page?: {
598
+ size?: number;
599
+ number?: number;
600
+ }): Promise<TextSearchRow[]>;
601
+ /** Status entities with colour and icon. */
602
+ statuses(): Promise<StatusRecord[]>;
603
+ /**
604
+ * Change the named fields of one row and answer the whole record.
605
+ *
606
+ * A key left out of `patch` is unchanged, not cleared, and an empty patch is a
607
+ * no-op (put_entity_type_id). The answer never resolves a dotted path, so a
608
+ * caller that shows one re-reads the row (024_read_after_write).
609
+ */
610
+ update(entityType: string, id: number, patch: Record<string, unknown>): Promise<EntityRow>;
611
+ /**
612
+ * One level of the site's navigation tree. `path` is `/Project/<id>` at the root
613
+ * and a child's own `path` below it (post_hierarchy_expand).
614
+ */
615
+ hierarchyExpand(path: string): Promise<HierarchyNode>;
616
+ /**
617
+ * Where a row sits in the navigation tree, under `rootPath` (`/Project/<id>`).
618
+ * The search criteria takes the literal key `entity` and nothing else: any other
619
+ * key answers `search_criteria size must be 1`, which counts the keys it
620
+ * recognises rather than the ones sent (post_hierarchy_search).
621
+ */
622
+ hierarchySearch(rootPath: string, entity: EntityRef): Promise<HierarchyPath[]>;
623
+ /**
624
+ * Aggregate rows without paging them. One `grouping` returns a field's distinct
625
+ * values and their counts (020_summarize).
626
+ */
627
+ summarize(entityType: string, options?: SummarizeOptions): Promise<SummarizeResult>;
628
+ /**
629
+ * Create one row and answer it.
630
+ *
631
+ * `project` is the create contract on a project-scoped type and the schema's
632
+ * `mandatory` flags are not it: the identity field is flagged mandatory, is
633
+ * optional, and is generated by the server when it is left out (012_create_version).
634
+ * An entity link is a `{type, id}` hash; a bare id 400s. Nothing is unique on any
635
+ * type measured, so two creates of the same body make two rows: key on `id`.
636
+ */
637
+ create(entityType: string, body: Record<string, unknown>): Promise<EntityRow>;
638
+ /**
639
+ * Put a file on a row, in the three calls the API takes.
640
+ *
641
+ * A ticket, a `PUT` of the bytes to presigned storage, and a completing `POST`.
642
+ * The 201 the third answers proves the handshake and not that bytes exist:
643
+ * skipping the second is undetectable, and the row it leaves reads the same
644
+ * (039_upload_silent_failures). A media field is not readable straight after: it
645
+ * returns a placeholder under `/images/status/transient/` until the transcode
646
+ * lands (recipes/001).
647
+ */
648
+ upload(entityType: string, id: number, file: UploadFile): Promise<UploadResult>;
649
+ /**
650
+ * A Note, its Attachments and its Replies as one list in time order.
651
+ *
652
+ * It replaces a `_search` on each of the three types and orders them together.
653
+ * `entityFields` widens a row type, one entry per type; it is accepted and
654
+ * changes nothing for Reply, whose extra fields need a `_search` on replies
655
+ * (get_entity_notes_id_thread_contents).
656
+ */
657
+ threadContents(noteId: number, entityFields?: Record<string, string[]>): Promise<ThreadRow[]>;
658
+ /**
659
+ * What changed, newest first.
660
+ *
661
+ * This is the change log, not the activity stream: a status change written over
662
+ * the API was on no stream 430s later (067_notes_in_the_stream). Rows are sorted
663
+ * `-id`; `id` and `created_at` are the two orders the type answers, and ids at the
664
+ * head are sparse and fill in later, so a cursor on `max(id)` drops events: re-scan
665
+ * behind the head or drive the feed from `created_at` and deduplicate on `id`
666
+ * (025_event_log).
667
+ */
668
+ eventLog(options?: EventLogOptions): Promise<EventLogResult>;
669
+ /**
670
+ * Everything one person follows, in one unpaged body.
671
+ *
672
+ * Each row is a type and an id: neither the record's name nor the date the
673
+ * follow started is returned, so a display list costs a `_search` on the ids.
674
+ * The user must be a HumanUser; a script cannot ask what it follows
675
+ * (get_entity_human_users_id_following).
676
+ */
677
+ following(userId: number, options?: FollowingOptions): Promise<EntityRef[]>;
678
+ }
679
+ /** The node shape `/hierarchy/_expand` answers, before normalising. */
680
+ interface RawHierarchyNode {
681
+ label?: string;
682
+ ref?: {
683
+ kind?: string;
684
+ value?: unknown;
685
+ };
686
+ path?: string;
687
+ parent_path?: string | null;
688
+ has_children?: boolean;
689
+ children?: RawHierarchyNode[];
690
+ }
691
+ /**
692
+ * Normalise one node and the level below it.
693
+ *
694
+ * The sample response gives a child a `label`, a `ref` and `has_children` but not
695
+ * always a `path`, so a child without one is addressed by appending its ref to the
696
+ * parent's path (post_hierarchy_expand).
697
+ */
698
+ declare function normalizeHierarchyNode(raw: RawHierarchyNode, path: string): HierarchyNode;
699
+ interface RestClientOptions {
700
+ /** Site root, e.g. `https://studio.shotgrid.autodesk.com`. */
701
+ siteUrl: string;
702
+ /** Returns a bearer token. Called per request so callers can refresh. */
703
+ token: () => Promise<string> | string;
704
+ fetch?: typeof fetch;
705
+ }
706
+ declare class SgApiError extends Error {
707
+ status: number;
708
+ body: unknown;
709
+ constructor(status: number, body: unknown, message?: string);
710
+ }
711
+ declare class RestClient implements SgClient {
712
+ private readonly options;
713
+ private readonly base;
714
+ private readonly siteRoot;
715
+ private readonly fetchFn;
716
+ constructor(options: RestClientOptions);
717
+ private request;
718
+ entityTypes(): Promise<EntityTypeInfo[]>;
719
+ fields(entityType: string, projectId?: number): Promise<Record<string, FieldSchema>>;
720
+ fieldWithProject(entityType: string, field: string, projectId: number): Promise<FieldSchema>;
721
+ search(entityType: string, options: SearchOptions): Promise<SearchResult>;
722
+ textSearch(text: string, entityTypes: Record<string, TextSearchFilter>, page?: {
723
+ size?: number;
724
+ number?: number;
725
+ }): Promise<TextSearchRow[]>;
726
+ update(entityType: string, id: number, patch: Record<string, unknown>): Promise<EntityRow>;
727
+ create(entityType: string, body: Record<string, unknown>): Promise<EntityRow>;
728
+ upload(entityType: string, id: number, file: UploadFile): Promise<UploadResult>;
729
+ summarize(entityType: string, options?: SummarizeOptions): Promise<SummarizeResult>;
730
+ threadContents(noteId: number, entityFields?: Record<string, string[]>): Promise<ThreadRow[]>;
731
+ eventLog(options?: EventLogOptions): Promise<EventLogResult>;
732
+ following(userId: number, options?: FollowingOptions): Promise<EntityRef[]>;
733
+ hierarchyExpand(path: string): Promise<HierarchyNode>;
734
+ hierarchySearch(rootPath: string, entity: EntityRef): Promise<HierarchyPath[]>;
735
+ statuses(): Promise<StatusRecord[]>;
736
+ }
737
+ /** One `_search` row of `EventLogEntry`, with `meta` decoded as the API returns it. */
738
+ declare function normalizeEventLogEntry(row: EntityRow): EventLogEntry;
739
+ //#endregion
740
+ export { STOCK_SPRITE_PATH as $, isOperator as $t, hierarchyEntity as A, emptyFilter as At, intersectStatuses as B, DataType as Bt, SummaryType as C, FilterNode as Ct, UploadFile as D, WireCondition as Dt, ThreadRow as E, TextSearchFilter as Et, StatusIcon as F, referencedPaths as Ft, statusLabel as G, TIME_UNITS as Gt, relativeLuminance as H, OPERATORS as Ht, StatusOption as I, toApi3Hash as It, NATIVE_STATUSES as J, ValueShape as Jt, statusPaint as K, TimeUnit as Kt, StatusPaint as L, toFilterArray as Lt, normalizeHierarchyNode as M, group as Mt, Rgb as N, isBlankCondition as Nt, UploadResult as O, WireGroup as Ot, StatusGlyph as P, isEmptyFilter as Pt, STOCK_ICON_KEYS as Q, isNumericType as Qt, StatusRecord as R, walk as Rt, SummaryGrouping as S, FilterGroup as St, ThreadAuthor as T, Scalar as Tt, rgbToCss as U, OPERATORS_BY_TYPE as Ut, parseBgColor as V, NEGATING_OPERATORS as Vt, statusGlyph as W, Operator as Wt, STOCK_ICON_CELLS as X, isFilterable as Xt, NativeStatus as Y, isDataType as Yt, STOCK_ICON_DATA_URLS as Z, isLinkType as Zt, SgClient as _, statusFieldNameFor as _t, EventLogOptions as a, stockIconSource as at, SummaryField as b, EntityRef as bt, HierarchyNode as c, FieldSchema as ct, RawHierarchyNode as d, RawProperty as dt, isTemporalType as en, SpriteCell as et, RestClient as f, displayNameOf as ft, SgApiError as g, statusFieldFor as gt, SearchResult as h, normalizeFields as ht, EventLogEntry as i, spriteStyle as it, normalizeEventLogEntry as j, fromWire as jt, eventLogFilters as k, condition as kt, HierarchyPath as l, RawFieldSchema as lt, SearchOptions as m, normalizeField as mt, EntityRow as n, supportsOperator as nn, isNativeStatus as nt, EventLogResult as o, DISPLAY_NAME_FIELDS as ot, RestClientOptions as p, fieldSchemaOverride as pt, usableStatuses as q, VALUE_SHAPE as qt, EntityTypeInfo as r, nativeStatus as rt, FollowingOptions as s, FIELD_SCHEMA_OVERRIDES as st, EVENT_LOG_FIELDS as t, operatorsFor as tn, StockIconSource as tt, HierarchyRef as u, RawFieldsResponse as ut, SummarizeOptions as v, undeclaredField as vt, TextSearchRow as w, LogicalOperator as wt, SummaryGroup as x, FilterCondition as xt, SummarizeResult as y, ConditionValue as yt, foregroundFor as z, DATA_TYPES as zt };