@versionone/agility-sdk 1.0.0-alpha.4 → 1.0.0-alpha.5

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.
package/dist/index.d.ts CHANGED
@@ -75,7 +75,7 @@ interface RequestOptions {
75
75
  /**
76
76
  * Called with a successful response's status and headers before its body
77
77
  * is read — for what the body alone does not say (an attachment's
78
- * Content-Type, say). A transport that cannot see headers may skip it.
78
+ * Content-Type, an ETag). A transport that cannot see headers may skip it.
79
79
  */
80
80
  onResponse?: (response: {
81
81
  status: number;
@@ -83,6 +83,12 @@ interface RequestOptions {
83
83
  get(name: string): string | null;
84
84
  };
85
85
  }) => void;
86
+ /**
87
+ * The request is conditional (an If-None-Match header is set) and a
88
+ * 304 Not Modified is an answer, not a failure: the promise then resolves
89
+ * to undefined after onResponse has seen the 304 and its headers.
90
+ */
91
+ acceptNotModified?: boolean;
86
92
  }
87
93
  interface Transport {
88
94
  request<T>(options: RequestOptions): Promise<T>;
@@ -99,7 +105,9 @@ interface Transport {
99
105
  * failure from an answer, but they are narrow on purpose: only requests the
100
106
  * caller marks idempotent (every GET is; a POST to api/asset is not — a
101
107
  * retried batch write after a timeout is a double write), and only on a
102
- * network error or a 5xx. A 4xx is an answer and is never retried.
108
+ * network error, a 5xx, or a 429 (the instance throttles per token; it
109
+ * sends no Retry-After, but one is honoured when present, up to 30 s). Any
110
+ * other 4xx is an answer and is never retried.
103
111
  */
104
112
  interface FetchTransportConfig {
105
113
  baseUrl: string;
@@ -115,7 +123,12 @@ interface FetchTransportConfig {
115
123
  * timeouts that turn into retries.
116
124
  */
117
125
  timeoutMs?: number;
118
- /** Extra attempts after the first, for idempotent requests only. Default 0. */
126
+ /**
127
+ * Extra attempts after the first, for idempotent requests only (a GET,
128
+ * a query.v1 read, a guarded batch that says so), after a network
129
+ * failure or a 5xx. Default 1: one resend, 150 ms later. 0 turns it off.
130
+ * A plain write is never resent whatever this says.
131
+ */
119
132
  retries?: number;
120
133
  fetchImpl?: typeof fetch;
121
134
  /**
@@ -4287,7 +4300,13 @@ declare function fromWireRow<Row extends Asset = Asset>(row: Record<string, unkn
4287
4300
  interface CommandFailureDetail {
4288
4301
  index: number | null;
4289
4302
  message: string;
4290
- echoed: Record<string, unknown>;
4303
+ /**
4304
+ * The command as the server echoed it, under a generated alias: what a
4305
+ * caller resends after correcting it. Readable on every failure but not
4306
+ * enumerable, so a serialised failure (a receipt, a log line) carries
4307
+ * index, message and violation and not the whole command.
4308
+ */
4309
+ readonly echoed: Record<string, unknown>;
4291
4310
  /** The message read as a validation violation, when it is one. */
4292
4311
  violation: Violation | null;
4293
4312
  }
@@ -4544,6 +4563,10 @@ declare class TypeCache {
4544
4563
  /** Loaded types; null records that the instance has no such type, so it is not asked for again. */
4545
4564
  readonly types: Map<string, AssetType | null>;
4546
4565
  readonly loading: Map<string, Promise<AssetType | null>>;
4566
+ /** The schema's tag the loaded types came with (meta.v1's ETag, one for the whole schema); null until a load, or for a seeded cache. */
4567
+ etag: string | null;
4568
+ /** Forget every loaded type (and the tag): the next query loads afresh. In-flight loads finish into the cache as before. */
4569
+ clear(): void;
4547
4570
  /** A cache seeded from a metamodel document. */
4548
4571
  static fromMetamodel(metamodel: Metamodel): TypeCache;
4549
4572
  }
@@ -4576,6 +4599,18 @@ declare class Schema implements AttributeLookup {
4576
4599
  * any other failure (auth, network) is thrown, not swallowed.
4577
4600
  */
4578
4601
  load(token: AssetTypeToken): Promise<AssetType | null>;
4602
+ /**
4603
+ * Has the instance's schema changed since these types were loaded? One
4604
+ * small conditional read of meta.v1. Unchanged: nothing happens.
4605
+ * Changed (or never tagged, or the transport cannot see headers): the
4606
+ * loaded types are dropped, so the next query loads them again, and the
4607
+ * new tag is kept. Answers whether it changed and the current tag. A
4608
+ * schema without a transport (seeded) never changes.
4609
+ */
4610
+ revalidate(): Promise<{
4611
+ changed: boolean;
4612
+ etag: string | null;
4613
+ }>;
4579
4614
  /**
4580
4615
  * Load whatever the queries in a batch need to have their rows translated:
4581
4616
  * the `from` types, then the types behind every relation hop and sub-select,
@@ -4980,6 +5015,108 @@ declare namespace loc2 {
4980
5015
  export type { loc2_Localizations as Localizations, loc2_LocalizeOptions as LocalizeOptions };
4981
5016
  }
4982
5017
 
5018
+ interface TokenRef {
5019
+ href: string;
5020
+ tokenref: AttributeTokenRef;
5021
+ }
5022
+ interface NameRef {
5023
+ href: string;
5024
+ nameref: AssetTypeToken;
5025
+ }
5026
+ interface AttributeDefinitionDocument {
5027
+ HREF?: string;
5028
+ _type: "AttributeDefinition";
5029
+ Name: string;
5030
+ Token: AttributeTokenRef;
5031
+ DisplayName: string;
5032
+ AttributeType: AttributeType;
5033
+ IsReadOnly: boolean;
5034
+ IsRequired: boolean;
5035
+ IsMultivalue: boolean;
5036
+ IsCanned: boolean;
5037
+ IsCustom: boolean;
5038
+ /** Present on an inherited attribute: the defining ancestor's attribute. */
5039
+ Base?: TokenRef;
5040
+ ReciprocalRelation?: TokenRef;
5041
+ OrderByAttribute?: TokenRef;
5042
+ DisplayByAttribute?: TokenRef;
5043
+ /** Present on a Relation. */
5044
+ RelatedAsset?: NameRef;
5045
+ }
5046
+ interface AssetTypeDocument {
5047
+ HREF?: string;
5048
+ Version?: string;
5049
+ _type: "AssetType";
5050
+ Token: AssetTypeToken;
5051
+ /** The delivered Name is a reference to the type's Name attribute; the token is the name. */
5052
+ Name: TokenRef | string;
5053
+ DisplayName: string;
5054
+ Abstract: boolean;
5055
+ /** Absent on a root type. */
5056
+ Base?: NameRef;
5057
+ DefaultOrderBy?: TokenRef;
5058
+ DefaultDisplayBy?: TokenRef;
5059
+ ShortName?: TokenRef;
5060
+ Description?: TokenRef;
5061
+ Attributes: Record<string, AttributeDefinitionDocument>;
5062
+ Operations?: Record<string, unknown>;
5063
+ Rules?: Record<string, unknown>;
5064
+ }
5065
+ interface MetamodelDocument {
5066
+ HREF?: string;
5067
+ Version: string;
5068
+ AssetTypes: Record<string, AssetTypeDocument>;
5069
+ }
5070
+ declare function toAttributeDefinition(doc: AttributeDefinitionDocument): AttributeDefinition;
5071
+ declare function toAssetType(doc: AssetTypeDocument): AssetType;
5072
+ declare function toMetamodel(doc: MetamodelDocument): Metamodel;
5073
+ /** Is this a metamodel document as meta.v1 delivers it? (A downloaded metamodel.json, say.) */
5074
+ declare function isMetamodelDocument(value: unknown): value is MetamodelDocument;
5075
+ /** A conditional read: the tag a previous answer carried. With it, an unchanged resource is null. */
5076
+ interface ConditionalOptions {
5077
+ ifNoneMatch?: string | null;
5078
+ }
5079
+ /** What a read answered, with the schema's tag as the server sent it (null when it sent none). */
5080
+ interface Tagged<T> {
5081
+ value: T;
5082
+ etag: string | null;
5083
+ }
5084
+ declare function getMetamodel(transport: Transport): Promise<Metamodel>;
5085
+ /** The metamodel with its tag; null when `ifNoneMatch` still matches (nothing was downloaded). */
5086
+ declare function readMetamodel(transport: Transport, options?: ConditionalOptions): Promise<Tagged<Metamodel> | null>;
5087
+ declare function getAssetType(transport: Transport, token: AssetTypeToken): Promise<AssetType>;
5088
+ /** One type with the schema's tag; null when `ifNoneMatch` still matches. */
5089
+ declare function readAssetType(transport: Transport, token: AssetTypeToken, options?: ConditionalOptions): Promise<Tagged<AssetType> | null>;
5090
+ /**
5091
+ * The schema's current tag, from one small read, or null when the server
5092
+ * sends none. With `ifNoneMatch`, "unchanged" is answered as that same tag
5093
+ * without a body — the cheapest way to ask whether anything in meta moved.
5094
+ */
5095
+ declare function etag(transport: Transport, options?: ConditionalOptions): Promise<string | null>;
5096
+ declare function getAttributeDefinition(transport: Transport, tokenref: AttributeTokenRef): Promise<AttributeDefinition>;
5097
+
5098
+ type meta_AssetTypeDocument = AssetTypeDocument;
5099
+ type meta_AttributeDefinitionDocument = AttributeDefinitionDocument;
5100
+ type meta_ConditionalOptions = ConditionalOptions;
5101
+ type meta_MetamodelDocument = MetamodelDocument;
5102
+ type meta_NameRef = NameRef;
5103
+ type meta_Tagged<T> = Tagged<T>;
5104
+ type meta_TokenRef = TokenRef;
5105
+ declare const meta_etag: typeof etag;
5106
+ declare const meta_getAssetType: typeof getAssetType;
5107
+ declare const meta_getAttributeDefinition: typeof getAttributeDefinition;
5108
+ declare const meta_getMetamodel: typeof getMetamodel;
5109
+ declare const meta_isMetamodelDocument: typeof isMetamodelDocument;
5110
+ declare const meta_readAssetType: typeof readAssetType;
5111
+ declare const meta_readMetamodel: typeof readMetamodel;
5112
+ declare const meta_toAssetType: typeof toAssetType;
5113
+ declare const meta_toAttributeDefinition: typeof toAttributeDefinition;
5114
+ declare const meta_toMetamodel: typeof toMetamodel;
5115
+ declare namespace meta {
5116
+ export { meta_etag as etag, meta_getAssetType as getAssetType, meta_getAttributeDefinition as getAttributeDefinition, meta_getMetamodel as getMetamodel, meta_isMetamodelDocument as isMetamodelDocument, meta_readAssetType as readAssetType, meta_readMetamodel as readMetamodel, meta_toAssetType as toAssetType, meta_toAttributeDefinition as toAttributeDefinition, meta_toMetamodel as toMetamodel };
5117
+ export type { meta_AssetTypeDocument as AssetTypeDocument, meta_AttributeDefinitionDocument as AttributeDefinitionDocument, meta_ConditionalOptions as ConditionalOptions, meta_MetamodelDocument as MetamodelDocument, meta_NameRef as NameRef, meta_Tagged as Tagged, meta_TokenRef as TokenRef };
5118
+ }
5119
+
4983
5120
  interface AssetRefDocument {
4984
5121
  _type: "Asset";
4985
5122
  href: string;
@@ -5213,11 +5350,21 @@ declare class AgilityApiClient {
5213
5350
  all<Row extends Asset = Asset>(q: QueryBody, options?: PagesOptions): Promise<Row[]>;
5214
5351
  /** The member these credentials belong to (Member.IsSelf, live 2026-11-12): what a comment's Author is. */
5215
5352
  me(): Promise<Me>;
5216
- /** meta.v1: the instance's schema, raw. (The translated, cached view is `schema`.) */
5353
+ /**
5354
+ * meta.v1: the instance's schema, raw. (The translated, cached view is
5355
+ * `schema`; `schema.revalidate()` drops it when the schema's tag moves.)
5356
+ * `read` and `readType` are conditional: pass the tag a previous answer
5357
+ * carried and an unchanged schema answers null without a download —
5358
+ * meta.v1 sends one strong ETag for the whole schema. `etag()` reads that
5359
+ * tag alone, from the smallest resource.
5360
+ */
5217
5361
  readonly meta: {
5218
5362
  metamodel: () => Promise<Metamodel>;
5363
+ read: (options?: ConditionalOptions) => Promise<Tagged<Metamodel> | null>;
5219
5364
  assetType: (token: string) => Promise<AssetType>;
5365
+ readType: (token: string, options?: ConditionalOptions) => Promise<Tagged<AssetType> | null>;
5220
5366
  attributeDefinition: (tokenref: `${string}.${string}`) => Promise<AttributeDefinition>;
5367
+ etag: (options?: ConditionalOptions) => Promise<string | null>;
5221
5368
  };
5222
5369
  /**
5223
5370
  * loc-2.v1: labels for tags, in one request (meta's DisplayName values are
@@ -5379,84 +5526,11 @@ declare class AgilityApiClient {
5379
5526
  };
5380
5527
  }
5381
5528
 
5382
- interface TokenRef {
5383
- href: string;
5384
- tokenref: AttributeTokenRef;
5385
- }
5386
- interface NameRef {
5387
- href: string;
5388
- nameref: AssetTypeToken;
5389
- }
5390
- interface AttributeDefinitionDocument {
5391
- HREF?: string;
5392
- _type: "AttributeDefinition";
5393
- Name: string;
5394
- Token: AttributeTokenRef;
5395
- DisplayName: string;
5396
- AttributeType: AttributeType;
5397
- IsReadOnly: boolean;
5398
- IsRequired: boolean;
5399
- IsMultivalue: boolean;
5400
- IsCanned: boolean;
5401
- IsCustom: boolean;
5402
- /** Present on an inherited attribute: the defining ancestor's attribute. */
5403
- Base?: TokenRef;
5404
- ReciprocalRelation?: TokenRef;
5405
- OrderByAttribute?: TokenRef;
5406
- DisplayByAttribute?: TokenRef;
5407
- /** Present on a Relation. */
5408
- RelatedAsset?: NameRef;
5409
- }
5410
- interface AssetTypeDocument {
5411
- HREF?: string;
5412
- Version?: string;
5413
- _type: "AssetType";
5414
- Token: AssetTypeToken;
5415
- /** The delivered Name is a reference to the type's Name attribute; the token is the name. */
5416
- Name: TokenRef | string;
5417
- DisplayName: string;
5418
- Abstract: boolean;
5419
- /** Absent on a root type. */
5420
- Base?: NameRef;
5421
- DefaultOrderBy?: TokenRef;
5422
- DefaultDisplayBy?: TokenRef;
5423
- ShortName?: TokenRef;
5424
- Description?: TokenRef;
5425
- Attributes: Record<string, AttributeDefinitionDocument>;
5426
- Operations?: Record<string, unknown>;
5427
- Rules?: Record<string, unknown>;
5428
- }
5429
- interface MetamodelDocument {
5430
- HREF?: string;
5431
- Version: string;
5432
- AssetTypes: Record<string, AssetTypeDocument>;
5433
- }
5434
- declare function toAttributeDefinition(doc: AttributeDefinitionDocument): AttributeDefinition;
5435
- declare function toAssetType(doc: AssetTypeDocument): AssetType;
5436
- declare function toMetamodel(doc: MetamodelDocument): Metamodel;
5437
- /** Is this a metamodel document as meta.v1 delivers it? (A downloaded metamodel.json, say.) */
5438
- declare function isMetamodelDocument(value: unknown): value is MetamodelDocument;
5439
- declare function getMetamodel(transport: Transport): Promise<Metamodel>;
5440
- declare function getAssetType(transport: Transport, token: AssetTypeToken): Promise<AssetType>;
5441
- declare function getAttributeDefinition(transport: Transport, tokenref: AttributeTokenRef): Promise<AttributeDefinition>;
5442
-
5443
- type meta_AssetTypeDocument = AssetTypeDocument;
5444
- type meta_AttributeDefinitionDocument = AttributeDefinitionDocument;
5445
- type meta_MetamodelDocument = MetamodelDocument;
5446
- type meta_NameRef = NameRef;
5447
- type meta_TokenRef = TokenRef;
5448
- declare const meta_getAssetType: typeof getAssetType;
5449
- declare const meta_getAttributeDefinition: typeof getAttributeDefinition;
5450
- declare const meta_getMetamodel: typeof getMetamodel;
5451
- declare const meta_isMetamodelDocument: typeof isMetamodelDocument;
5452
- declare const meta_toAssetType: typeof toAssetType;
5453
- declare const meta_toAttributeDefinition: typeof toAttributeDefinition;
5454
- declare const meta_toMetamodel: typeof toMetamodel;
5455
- declare namespace meta {
5456
- export { meta_getAssetType as getAssetType, meta_getAttributeDefinition as getAttributeDefinition, meta_getMetamodel as getMetamodel, meta_isMetamodelDocument as isMetamodelDocument, meta_toAssetType as toAssetType, meta_toAttributeDefinition as toAttributeDefinition, meta_toMetamodel as toMetamodel };
5457
- export type { meta_AssetTypeDocument as AssetTypeDocument, meta_AttributeDefinitionDocument as AttributeDefinitionDocument, meta_MetamodelDocument as MetamodelDocument, meta_NameRef as NameRef, meta_TokenRef as TokenRef };
5529
+ /** What query.v1 said about each refused query: its exception type and message. */
5530
+ interface QueryRejection {
5531
+ type: string | null;
5532
+ message: string;
5458
5533
  }
5459
-
5460
5534
  /**
5461
5535
  * Several queries in one request; one array of rows per query, in order, []
5462
5536
  * where a query matched nothing. A page is required on every query. A
@@ -5467,13 +5541,12 @@ declare function queries<Row extends Asset = Asset>(transport: Transport, qs: Qu
5467
5541
  /** One query, its rows. */
5468
5542
  declare function query<Row extends Asset = Asset>(transport: Transport, q: QueryBody, options?: ReadOptions): Promise<Row[]>;
5469
5543
 
5544
+ type queryV1_QueryRejection = QueryRejection;
5470
5545
  declare const queryV1_queries: typeof queries;
5471
5546
  declare const queryV1_query: typeof query;
5472
5547
  declare namespace queryV1 {
5473
- export {
5474
- queryV1_queries as queries,
5475
- queryV1_query as query,
5476
- };
5548
+ export { queryV1_queries as queries, queryV1_query as query };
5549
+ export type { queryV1_QueryRejection as QueryRejection };
5477
5550
  }
5478
5551
 
5479
5552
  /** The attributes shown for each type in the scopes given: a map from type token to sorted attribute names. */
@@ -5491,4 +5564,4 @@ declare namespace fields {
5491
5564
  }
5492
5565
 
5493
5566
  export { ASSET_STATE_CODES, AgilityApiClient, ApiError, AttachmentUploadError, BatchRecordError, BatchSendError, CommandFailureError, INLINE_LIMIT, INVERSE, NUMBER_ROOTS, CHUNK as RESOLVE_CHUNK, Schema, TypeCache, UNGUARDABLE_TYPES, UnguardableError, UsageError, WrappedError, assetApi, assetStateOf, attachment, createFetchTransport, describe as describeBatch, fields, guard, importantName, isAssetNumber, isOidToken, kindOf, loc2, meta, momentless, namesOf, parseOid, parseViolation, queryV1, resolveNumbers, resolveRef, rest1, reverseOf, reversible, runBatch, select, sendGuarded, snapshot, stampsOf, toOid, touchedBy, typeOfOid, unguardable };
5494
- export type { Access, Actual, Allocation, Application, Asset, AssetApiResponse, AssetChangedEventDefinition, AssetCreatedEventDefinition, AssetDocument, AssetNumberAttributeDefinition, AssetStateValue, AssetType, AssetTypeMap, AssetTypeName, AssetTypeToken, AssetsDocument, Attachment, AddOptions as AttachmentAddOptions, AttachmentCategory, AttachmentInfo, AttributeAddedEventDefinition, AttributeChangedEventDefinition, AttributeDefinition, AttributeLookup, AttributeRemovedEventDefinition, AttributeTokenRef, AttributeType, AttributeValue, BaseAsset, BatchOptions, Budget, BuildProject, BuildRun, BuildSource, BuildStatus, Bundle, BusinessRule, Bytes, CalendarEvent, CalendarEventCategory, Capacity, ChangeSet, ClassOfService, ClientConfig, ClientOptions, Command, CommandBody, CommandFailureDetail, CommandValue, Community, Concept, ConcreteAssetTypeName, Conversation, CreateBody, CreateType, CredentialsAttributeDefinition, CustomAttributes, CustomList, DateString, DefaultRule, Defect, DefectResolution, DefectType, Delivery, DeliveryCategory, Described, EmbeddedImage$1 as EmbeddedImage, Environment, Epic, EpicCategory, EpicPriority, EpicStatus, ErrorKind, ExecuteBody, ExecuteSecurityCheckAttributeDefinition, Expression, ExpressionImage, ExpressionSpace, ExternalAction, ExternalActionInvocation, FetchTransportConfig, FieldsOf, From, Goal, GoalCategory, GoalPriority, Grant, GuardedResult, HierarchyRelationDefinition, Idea, IdeaCategory, IdeaChangeSet, IdeaComment, IdeaForum, IdeaStatus, IdeaUser, IdeasUserCache, Image, Issue, IssueCategory, IssuePriority, IssueResolution, IssueStatus, KeyResult, KeyResultCommitment, KeyResultUnit, Label, Link, LinkCategory, List, Localizations, LocalizeOptions, ManyToManyRelationDefinition, Me, Member, MemberLabel, Message, MessageReceipt, Metamodel, Milestone, MilestoneCategory, MultivalueAttributeDefinition, NameLookupFailure, Named, NoteCategory, Oid, OidParts, Okr, OkrObjective, OkrSession, OneToManyRelationDefinition, OperationExecutedEventDefinition, OperationResult, Outcome, Page, PagesOptions, PasswordAttributeDefinition, Phase, PlanningLevel, PlanningRoom, PrimaryWorkitem, PrincipalAttributeDefinition, Publication, QueryBody, ReadOptions, ReadParams, ReadResult, Receipt, Ref, RegressionPlan, RegressionSuite, RegressionTest, RegressionTestStatus, Release, Request, RequestBody, RequestCategory, RequestOptions, RequestPriority, RequestResolution, RequestStatus, Resolution, ResolvedAsset, ResponseType, Retrospective, RetrospectivePrompt, RetrospectiveTopic, Reverse, Roadmap, Role, Room, RowContext, RowOf, RowsOf, SavedView, Schedule, Scheme, Scope, ScopeLabel, ScopeStatus, SelectKey, SelectPath, SelectValue, Selected, SendOptions, SimpleAttributeDefinition, State, Status, Story, StoryCategory, StorySource, StoryStatus, StrategicTheme, StrategicThemeLevel, Subscription, SubscriptionTerm, SyntheticAttributeDefinition, Task, TaskCategory, TaskSource, TaskStatus, Team, TeamRoom, TeamWorkflow, Template, Test, TestCategory, TestRun, TestSet, TestStatus, TestSuite, Theme, ThemeCategory, ThemeLabel, ThemeSource, ThemeStatus, Timebox, Timesheet, Topic, Transport, TriggerType, TypeOf, UnauthorizedContext, UnauthorizedDecision, Unguardable, UpdateBody, UpdateSecurityCheckAttributeDefinition, ValueStream, VersionLabel, Violation, Workflow, WorkflowTransition, Workitem, WorkitemPriority, WorkitemRisk };
5567
+ export type { Access, Actual, Allocation, Application, Asset, AssetApiResponse, AssetChangedEventDefinition, AssetCreatedEventDefinition, AssetDocument, AssetNumberAttributeDefinition, AssetStateValue, AssetType, AssetTypeMap, AssetTypeName, AssetTypeToken, AssetsDocument, Attachment, AddOptions as AttachmentAddOptions, AttachmentCategory, AttachmentInfo, AttributeAddedEventDefinition, AttributeChangedEventDefinition, AttributeDefinition, AttributeLookup, AttributeRemovedEventDefinition, AttributeTokenRef, AttributeType, AttributeValue, BaseAsset, BatchOptions, Budget, BuildProject, BuildRun, BuildSource, BuildStatus, Bundle, BusinessRule, Bytes, CalendarEvent, CalendarEventCategory, Capacity, ChangeSet, ClassOfService, ClientConfig, ClientOptions, Command, CommandBody, CommandFailureDetail, CommandValue, Community, Concept, ConcreteAssetTypeName, ConditionalOptions, Conversation, CreateBody, CreateType, CredentialsAttributeDefinition, CustomAttributes, CustomList, DateString, DefaultRule, Defect, DefectResolution, DefectType, Delivery, DeliveryCategory, Described, EmbeddedImage$1 as EmbeddedImage, Environment, Epic, EpicCategory, EpicPriority, EpicStatus, ErrorKind, ExecuteBody, ExecuteSecurityCheckAttributeDefinition, Expression, ExpressionImage, ExpressionSpace, ExternalAction, ExternalActionInvocation, FetchTransportConfig, FieldsOf, From, Goal, GoalCategory, GoalPriority, Grant, GuardedResult, HierarchyRelationDefinition, Idea, IdeaCategory, IdeaChangeSet, IdeaComment, IdeaForum, IdeaStatus, IdeaUser, IdeasUserCache, Image, Issue, IssueCategory, IssuePriority, IssueResolution, IssueStatus, KeyResult, KeyResultCommitment, KeyResultUnit, Label, Link, LinkCategory, List, Localizations, LocalizeOptions, ManyToManyRelationDefinition, Me, Member, MemberLabel, Message, MessageReceipt, Metamodel, Milestone, MilestoneCategory, MultivalueAttributeDefinition, NameLookupFailure, Named, NoteCategory, Oid, OidParts, Okr, OkrObjective, OkrSession, OneToManyRelationDefinition, OperationExecutedEventDefinition, OperationResult, Outcome, Page, PagesOptions, PasswordAttributeDefinition, Phase, PlanningLevel, PlanningRoom, PrimaryWorkitem, PrincipalAttributeDefinition, Publication, QueryBody, QueryRejection, ReadOptions, ReadParams, ReadResult, Receipt, Ref, RegressionPlan, RegressionSuite, RegressionTest, RegressionTestStatus, Release, Request, RequestBody, RequestCategory, RequestOptions, RequestPriority, RequestResolution, RequestStatus, Resolution, ResolvedAsset, ResponseType, Retrospective, RetrospectivePrompt, RetrospectiveTopic, Reverse, Roadmap, Role, Room, RowContext, RowOf, RowsOf, SavedView, Schedule, Scheme, Scope, ScopeLabel, ScopeStatus, SelectKey, SelectPath, SelectValue, Selected, SendOptions, SimpleAttributeDefinition, State, Status, Story, StoryCategory, StorySource, StoryStatus, StrategicTheme, StrategicThemeLevel, Subscription, SubscriptionTerm, SyntheticAttributeDefinition, Tagged, Task, TaskCategory, TaskSource, TaskStatus, Team, TeamRoom, TeamWorkflow, Template, Test, TestCategory, TestRun, TestSet, TestStatus, TestSuite, Theme, ThemeCategory, ThemeLabel, ThemeSource, ThemeStatus, Timebox, Timesheet, Topic, Transport, TriggerType, TypeOf, UnauthorizedContext, UnauthorizedDecision, Unguardable, UpdateBody, UpdateSecurityCheckAttributeDefinition, ValueStream, VersionLabel, Violation, Workflow, WorkflowTransition, Workitem, WorkitemPriority, WorkitemRisk };
package/dist/index.js CHANGED
@@ -117,10 +117,22 @@ function messageOf(data, status) {
117
117
  return `HTTP ${status}`;
118
118
  }
119
119
  var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
120
+ var MAX_RETRY_AFTER_MS = 3e4;
121
+ function retryDelay(response, attempt) {
122
+ const header = response.headers.get("retry-after");
123
+ if (header) {
124
+ const seconds = Number(header);
125
+ const ms = Number.isFinite(seconds) ? seconds * 1e3 : Date.parse(header) - Date.now();
126
+ if (Number.isFinite(ms) && ms > MAX_RETRY_AFTER_MS) return null;
127
+ if (Number.isFinite(ms) && ms > 0) return ms;
128
+ }
129
+ return 150 * (attempt + 1);
130
+ }
131
+ var retriable = (status) => status === 429 || status >= 500;
120
132
  function createFetchTransport(config) {
121
133
  const fetchImpl = config.fetchImpl ?? fetch;
122
134
  const timeoutMs = config.timeoutMs ?? 6e4;
123
- const retries = Math.max(0, config.retries ?? 0);
135
+ const retries = Math.max(0, config.retries ?? 1);
124
136
  const authorization = config.authorization ?? (config.accessToken ? `Bearer ${config.accessToken}` : void 0);
125
137
  const unauthorizedStatuses = config.unauthorizedStatuses ?? [401];
126
138
  return {
@@ -163,6 +175,10 @@ function createFetchTransport(config) {
163
175
  options.onResponse?.({ status: response.status, headers: response.headers });
164
176
  return await readBody(response, responseType);
165
177
  }
178
+ if (response.status === 304 && options.acceptNotModified) {
179
+ options.onResponse?.({ status: 304, headers: response.headers });
180
+ return void 0;
181
+ }
166
182
  const data = await readBody(response, responseType === "bytes" ? "text" : responseType === "json" ? "auto" : responseType);
167
183
  lastError = new ApiError(messageOf(data, response.status), response.status, void 0, data);
168
184
  } catch (error) {
@@ -181,8 +197,10 @@ function createFetchTransport(config) {
181
197
  }
182
198
  break;
183
199
  }
184
- if (response.status >= 500 && attempt + 1 < attempts) {
185
- await delay(150 * (attempt + 1));
200
+ if (retriable(response.status) && attempt + 1 < attempts) {
201
+ const wait = retryDelay(response, attempt);
202
+ if (wait === null) break;
203
+ await delay(wait);
186
204
  continue;
187
205
  }
188
206
  break;
@@ -389,6 +407,11 @@ function toNumber(value) {
389
407
  const n = typeof value === "string" && value.trim() !== "" ? Number(value) : NaN;
390
408
  return Number.isNaN(n) ? value : n;
391
409
  }
410
+ function failureDetail(index, message, echoed) {
411
+ const detail = { index, message, violation: parseViolation(message) };
412
+ Object.defineProperty(detail, "echoed", { value: echoed, enumerable: false, writable: false });
413
+ return detail;
414
+ }
392
415
  function parseViolation(message) {
393
416
  const parts = message.trim().split("'");
394
417
  if (parts[0] !== "Violation" || !parts[1]) return null;
@@ -398,12 +421,11 @@ function parseViolation(message) {
398
421
  return { rule, attribute: token, tag: !token && rest.length > 1 ? rest.join("'") : null };
399
422
  }
400
423
  function failuresOf(response) {
401
- return (response.commandFailures?.commands ?? []).map(({ error, ...echoed }) => ({
402
- index: typeof error === "object" && error && typeof error.sourceCommandIndex === "number" ? error.sourceCommandIndex : null,
403
- message: typeof error === "string" ? error : error?.message || "command failed",
404
- echoed,
405
- violation: parseViolation(typeof error === "string" ? error : error?.message || "")
406
- }));
424
+ return (response.commandFailures?.commands ?? []).map(({ error, ...echoed }) => failureDetail(
425
+ typeof error === "object" && error && typeof error.sourceCommandIndex === "number" ? error.sourceCommandIndex : null,
426
+ typeof error === "string" ? error : error?.message || "command failed",
427
+ echoed
428
+ ));
407
429
  }
408
430
  function outcomeOf(response) {
409
431
  return {
@@ -421,7 +443,7 @@ function mergeOutcome(into, done, offset) {
421
443
  into.created.push(...done.created);
422
444
  into.modified.push(...done.modified);
423
445
  into.operated.push(...done.operated);
424
- into.failures.push(...done.failures.map((f) => ({ ...f, index: f.index === null ? null : f.index + offset })));
446
+ into.failures.push(...done.failures.map((f) => failureDetail(f.index === null ? null : f.index + offset, f.message, f.echoed)));
425
447
  into.queryResults.push(...done.queryResults);
426
448
  into.requestId = null;
427
449
  if (done.durationSeconds !== null) into.durationSeconds = (into.durationSeconds ?? 0) + done.durationSeconds;
@@ -1255,10 +1277,13 @@ async function resolveRef(transport, spec, options = {}) {
1255
1277
  // src/wire/meta.ts
1256
1278
  var meta_exports = {};
1257
1279
  __export(meta_exports, {
1280
+ etag: () => etag,
1258
1281
  getAssetType: () => getAssetType,
1259
1282
  getAttributeDefinition: () => getAttributeDefinition,
1260
1283
  getMetamodel: () => getMetamodel,
1261
1284
  isMetamodelDocument: () => isMetamodelDocument,
1285
+ readAssetType: () => readAssetType,
1286
+ readMetamodel: () => readMetamodel,
1262
1287
  toAssetType: () => toAssetType,
1263
1288
  toAttributeDefinition: () => toAttributeDefinition,
1264
1289
  toMetamodel: () => toMetamodel
@@ -1303,12 +1328,36 @@ function toMetamodel(doc) {
1303
1328
  function isMetamodelDocument(value) {
1304
1329
  return !!value && typeof value === "object" && typeof value.Version === "string" && !!value.AssetTypes && typeof value.AssetTypes === "object";
1305
1330
  }
1331
+ var PROBE_PATH = "/meta.v1/BaseAsset/Name";
1332
+ async function conditional(transport, path, options) {
1333
+ let etag2 = null, status = 200;
1334
+ const onResponse = (r) => {
1335
+ status = r.status;
1336
+ etag2 = r.headers.get("etag");
1337
+ };
1338
+ const headers = options.ifNoneMatch ? { "if-none-match": options.ifNoneMatch } : void 0;
1339
+ const doc = await transport.request({ method: "GET", path, headers, responseType: "json", onResponse, acceptNotModified: !!options.ifNoneMatch });
1340
+ if (status === 304 || doc === void 0) return null;
1341
+ return { value: doc, etag: etag2 };
1342
+ }
1306
1343
  async function getMetamodel(transport) {
1307
1344
  return toMetamodel(await transport.request({ method: "GET", path: "/meta.v1", responseType: "json" }));
1308
1345
  }
1346
+ async function readMetamodel(transport, options = {}) {
1347
+ const read2 = await conditional(transport, "/meta.v1", options);
1348
+ return read2 && { value: toMetamodel(read2.value), etag: read2.etag };
1349
+ }
1309
1350
  async function getAssetType(transport, token) {
1310
1351
  return toAssetType(await transport.request({ method: "GET", path: `/meta.v1/${encodeURIComponent(token)}`, responseType: "json" }));
1311
1352
  }
1353
+ async function readAssetType(transport, token, options = {}) {
1354
+ const read2 = await conditional(transport, `/meta.v1/${encodeURIComponent(token)}`, options);
1355
+ return read2 && { value: toAssetType(read2.value), etag: read2.etag };
1356
+ }
1357
+ async function etag(transport, options = {}) {
1358
+ const read2 = await conditional(transport, PROBE_PATH, options);
1359
+ return read2 ? read2.etag : options.ifNoneMatch ?? null;
1360
+ }
1312
1361
  async function getAttributeDefinition(transport, tokenref) {
1313
1362
  const [type, attribute] = tokenref.split(".");
1314
1363
  if (!type || !attribute) throw new UsageError(`"${tokenref}" is not an attribute token ("Story.Name")`);
@@ -1320,6 +1369,13 @@ var TypeCache = class _TypeCache {
1320
1369
  /** Loaded types; null records that the instance has no such type, so it is not asked for again. */
1321
1370
  types = /* @__PURE__ */ new Map();
1322
1371
  loading = /* @__PURE__ */ new Map();
1372
+ /** The schema's tag the loaded types came with (meta.v1's ETag, one for the whole schema); null until a load, or for a seeded cache. */
1373
+ etag = null;
1374
+ /** Forget every loaded type (and the tag): the next query loads afresh. In-flight loads finish into the cache as before. */
1375
+ clear() {
1376
+ this.types.clear();
1377
+ this.etag = null;
1378
+ }
1323
1379
  /** A cache seeded from a metamodel document. */
1324
1380
  static fromMetamodel(metamodel) {
1325
1381
  const cache = new _TypeCache();
@@ -1384,7 +1440,10 @@ var Schema = class _Schema {
1384
1440
  this.types.set(token, null);
1385
1441
  return null;
1386
1442
  }
1387
- const request = getAssetType(this.transport, token).catch((error) => {
1443
+ const request = readAssetType(this.transport, token).then((read2) => {
1444
+ if (read2?.etag) this.cache.etag = read2.etag;
1445
+ return read2 ? read2.value : null;
1446
+ }).catch((error) => {
1388
1447
  if (error instanceof ApiError && error.status === 404) return null;
1389
1448
  throw error;
1390
1449
  }).then((type) => {
@@ -1394,6 +1453,23 @@ var Schema = class _Schema {
1394
1453
  this.loading.set(token, request);
1395
1454
  return request;
1396
1455
  }
1456
+ /**
1457
+ * Has the instance's schema changed since these types were loaded? One
1458
+ * small conditional read of meta.v1. Unchanged: nothing happens.
1459
+ * Changed (or never tagged, or the transport cannot see headers): the
1460
+ * loaded types are dropped, so the next query loads them again, and the
1461
+ * new tag is kept. Answers whether it changed and the current tag. A
1462
+ * schema without a transport (seeded) never changes.
1463
+ */
1464
+ async revalidate() {
1465
+ if (!this.transport) return { changed: false, etag: this.cache.etag };
1466
+ const was = this.cache.etag;
1467
+ const now2 = await etag(this.transport, { ifNoneMatch: was });
1468
+ if (was && now2 === was) return { changed: false, etag: was };
1469
+ this.cache.clear();
1470
+ this.cache.etag = now2;
1471
+ return { changed: true, etag: now2 };
1472
+ }
1397
1473
  /**
1398
1474
  * Load whatever the queries in a batch need to have their rows translated:
1399
1475
  * the `from` types, then the types behind every relation hop and sub-select,
@@ -1454,8 +1530,8 @@ __export(queryV1_exports, {
1454
1530
  });
1455
1531
  function rejected(error) {
1456
1532
  const details = error.details;
1457
- const messages = (details?.exceptions ?? []).map((e) => e?.message).filter((m) => typeof m === "string" && !!m);
1458
- return messages.length ? new ApiError(messages.join("; "), error.status, "QUERY_REJECTED", error.details) : error;
1533
+ const rejections = (details?.exceptions ?? []).filter((e) => e && typeof e.message === "string" && e.message).map((e) => ({ type: typeof e.type === "string" ? e.type : null, message: e.message }));
1534
+ return rejections.length ? new ApiError(rejections.map((r) => r.message).join("; "), error.status, "QUERY_REJECTED", rejections) : error;
1459
1535
  }
1460
1536
  async function queries2(transport, qs, options = {}) {
1461
1537
  for (const q of qs) if (!q.page) throw new UsageError(`query of ${q.from} without a page: every query.v1 read must be bounded`);
@@ -1639,11 +1715,21 @@ var AgilityApiClient = class _AgilityApiClient {
1639
1715
  return row;
1640
1716
  }
1641
1717
  // ---- the endpoints behind the reads -----------------------------------------------
1642
- /** meta.v1: the instance's schema, raw. (The translated, cached view is `schema`.) */
1718
+ /**
1719
+ * meta.v1: the instance's schema, raw. (The translated, cached view is
1720
+ * `schema`; `schema.revalidate()` drops it when the schema's tag moves.)
1721
+ * `read` and `readType` are conditional: pass the tag a previous answer
1722
+ * carried and an unchanged schema answers null without a download —
1723
+ * meta.v1 sends one strong ETag for the whole schema. `etag()` reads that
1724
+ * tag alone, from the smallest resource.
1725
+ */
1643
1726
  meta = {
1644
1727
  metamodel: () => getMetamodel(this.transport),
1728
+ read: (options) => readMetamodel(this.transport, options),
1645
1729
  assetType: (token) => getAssetType(this.transport, token),
1646
- attributeDefinition: (tokenref) => getAttributeDefinition(this.transport, tokenref)
1730
+ readType: (token, options) => readAssetType(this.transport, token, options),
1731
+ attributeDefinition: (tokenref) => getAttributeDefinition(this.transport, tokenref),
1732
+ etag: (options) => etag(this.transport, options)
1647
1733
  };
1648
1734
  /**
1649
1735
  * loc-2.v1: labels for tags, in one request (meta's DisplayName values are