@sealant/api-contracts-next 0.40.0-next.770 → 0.40.0-next.772

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.
@@ -26,9 +26,82 @@
26
26
  * keeps the member's row, with its version. One member reads at `GET …/members/:userId`, and the
27
27
  * list pages by user id: `limit` (100 unless named, at most 500) and the previous page's
28
28
  * `nextCursor` as `cursor`.
29
+ *
30
+ * A budget caps CPU (millicores), memory and storage (bytes): ceilings, not measurements, and
31
+ * zero allows nothing. Storage is one allowance for volumes, archived ones included, and the
32
+ * backups and snapshots that protect them. `ensureResourceDomain` may name a new domain's budget
33
+ * and never changes one: naming another gets `409`, carrying the domain and its revision.
34
+ * `setResourceDomainBudget` takes the `revision` it read: a change made since gets `409`, so a
35
+ * delayed update never overwrites a newer one, and it answers the budget it wrote. Changing a
36
+ * budget changes nothing the domain holds. The read is one snapshot. It reports, per dimension,
37
+ * `used` (confirmed to exist), `reserved` (admitted, not yet confirmed), the `limit` and a `warning`
38
+ * once both reach 80%, 95% or all of it; storage also reports its `archived` and `protection` bytes.
29
39
  */
30
40
  import { Schema } from "effect";
31
41
  import { HttpApiEndpoint, HttpApiGroup } from "effect/http-api";
42
+ /**
43
+ * The dimensions a budget caps, defined once: the contract's schemas, the database's budget, the
44
+ * admission check and the SDK all take theirs from these keys. `freedBy` is what makes room in a
45
+ * dimension besides raising its limit.
46
+ */
47
+ export declare const RESOURCE_DOMAIN_BUDGET_DIMENSIONS: {
48
+ readonly cpuMillicores: {
49
+ readonly name: "CPU";
50
+ readonly freedBy: "stop-objects";
51
+ };
52
+ readonly memoryBytes: {
53
+ readonly name: "memory";
54
+ readonly freedBy: "stop-objects";
55
+ };
56
+ readonly storageBytes: {
57
+ readonly name: "storage";
58
+ readonly freedBy: "purge-objects";
59
+ };
60
+ };
61
+ export type ResourceDomainBudgetDimension = keyof typeof RESOURCE_DOMAIN_BUDGET_DIMENSIONS;
62
+ export declare const resourceDomainBudgetLimitsSchema: Schema.Struct<Record<"cpuMillicores" | "memoryBytes" | "storageBytes", Schema.Int>>;
63
+ export type ResourceDomainBudgetLimits = typeof resourceDomainBudgetLimitsSchema.Type;
64
+ /** The share of a limit at which the budget read starts to warn. */
65
+ export declare const RESOURCE_DOMAIN_BUDGET_WARNINGS: {
66
+ readonly "80-percent": 80;
67
+ readonly "95-percent": 95;
68
+ };
69
+ declare const BudgetWarning: Schema.Literals<readonly ["none", "80-percent", "95-percent", "full"]>;
70
+ export type ResourceDomainBudgetWarning = typeof BudgetWarning.Type;
71
+ export declare const resourceDomainBudgetSchema: Schema.Struct<{
72
+ readonly revision: Schema.Int;
73
+ readonly storageBytes: Schema.Struct<{
74
+ readonly limit: Schema.Int;
75
+ readonly used: Schema.Int;
76
+ readonly reserved: Schema.Int;
77
+ readonly warning: Schema.Literals<readonly ["none", "80-percent", "95-percent", "full"]>;
78
+ /** Archived volumes' data bytes, part of `used`: kept until a person purges them. */
79
+ readonly archived: Schema.Int;
80
+ /** Bytes that backups and snapshots hold, not a volume's unused reserve for them. */
81
+ readonly protection: Schema.Int;
82
+ }>;
83
+ readonly cpuMillicores: Schema.Struct<{
84
+ readonly limit: Schema.Int;
85
+ readonly used: Schema.Int;
86
+ readonly reserved: Schema.Int;
87
+ readonly warning: Schema.Literals<readonly ["none", "80-percent", "95-percent", "full"]>;
88
+ }>;
89
+ readonly memoryBytes: Schema.Struct<{
90
+ readonly limit: Schema.Int;
91
+ readonly used: Schema.Int;
92
+ readonly reserved: Schema.Int;
93
+ readonly warning: Schema.Literals<readonly ["none", "80-percent", "95-percent", "full"]>;
94
+ }>;
95
+ }>;
96
+ export type ResourceDomainBudgetWire = typeof resourceDomainBudgetSchema.Type;
97
+ export declare const setResourceDomainBudgetRequestSchema: Schema.Struct<{
98
+ /** The revision this change replaces: the one the caller last read. */
99
+ readonly revision: Schema.Int;
100
+ readonly cpuMillicores: Schema.Int;
101
+ readonly memoryBytes: Schema.Int;
102
+ readonly storageBytes: Schema.Int;
103
+ }>;
104
+ export type SetResourceDomainBudgetRequest = typeof setResourceDomainBudgetRequestSchema.Type;
32
105
  export declare const resourceDomainSchema: Schema.Struct<{
33
106
  readonly resourceDomainId: Schema.String;
34
107
  readonly key: Schema.String;
@@ -37,6 +110,8 @@ export declare const resourceDomainSchema: Schema.Struct<{
37
110
  export type ResourceDomainWire = typeof resourceDomainSchema.Type;
38
111
  export declare const ensureResourceDomainRequestSchema: Schema.Struct<{
39
112
  readonly key: Schema.String;
113
+ /** The budget a new domain starts with; none starts it at zero. */
114
+ readonly budget: Schema.optional<Schema.Struct<Record<"cpuMillicores" | "memoryBytes" | "storageBytes", Schema.Int>>>;
40
115
  }>;
41
116
  export type EnsureResourceDomainRequest = typeof ensureResourceDomainRequestSchema.Type;
42
117
  export declare const ensureResourceDomainResponseSchema: Schema.Struct<{
@@ -116,6 +191,21 @@ declare const ResourceDomainMemberVersionConflictError_base: Schema.Class<Resour
116
191
  */
117
192
  export declare class ResourceDomainMemberVersionConflictError extends ResourceDomainMemberVersionConflictError_base {
118
193
  }
194
+ declare const ResourceDomainBudgetDiffersError_base: Schema.Class<ResourceDomainBudgetDiffersError, Schema.TaggedStruct<"ResourceDomainBudgetDiffersError", {
195
+ readonly message: Schema.String;
196
+ readonly resourceDomainId: Schema.String;
197
+ readonly revision: Schema.Int;
198
+ }>, import("effect/Cause").YieldableError>;
199
+ /** The domain exists with another budget, at `revision`; nothing changed. */
200
+ export declare class ResourceDomainBudgetDiffersError extends ResourceDomainBudgetDiffersError_base {
201
+ }
202
+ declare const ResourceDomainBudgetRevisionConflictError_base: Schema.Class<ResourceDomainBudgetRevisionConflictError, Schema.TaggedStruct<"ResourceDomainBudgetRevisionConflictError", {
203
+ readonly message: Schema.String;
204
+ readonly revision: Schema.Int;
205
+ }>, import("effect/Cause").YieldableError>;
206
+ /** `revision` is the current one. */
207
+ export declare class ResourceDomainBudgetRevisionConflictError extends ResourceDomainBudgetRevisionConflictError_base {
208
+ }
119
209
  declare const ResourceDomainInternalServerError_base: Schema.Class<ResourceDomainInternalServerError, Schema.TaggedStruct<"ResourceDomainInternalServerError", {
120
210
  readonly message: Schema.String;
121
211
  }>, import("effect/Cause").YieldableError>;
@@ -123,18 +213,46 @@ export declare class ResourceDomainInternalServerError extends ResourceDomainInt
123
213
  }
124
214
  export declare const ResourceDomainsGroup: HttpApiGroup.HttpApiGroup<"resourceDomains", HttpApiEndpoint.HttpApiEndpoint<"ensureResourceDomain", "POST", "/", never, never, Schema.toCodecJson<Schema.Struct<{
125
215
  readonly key: Schema.String;
216
+ /** The budget a new domain starts with; none starts it at zero. */
217
+ readonly budget: Schema.optional<Schema.Struct<Record<"cpuMillicores" | "memoryBytes" | "storageBytes", Schema.Int>>>;
126
218
  }>>, never, Schema.toCodecJson<Schema.Struct<{
127
219
  readonly resourceDomainId: Schema.String;
128
220
  readonly key: Schema.String;
129
221
  readonly createdAt: Schema.String;
130
222
  /** True when this call created the domain. */
131
223
  readonly created: Schema.Boolean;
132
- }>>, Schema.toCodecJson<typeof ResourceDomainForbiddenError | typeof ResourceDomainInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"getResourceDomain", "GET", "/:resourceDomainId", Schema.toCodecStringTree<Schema.Struct<{
224
+ }>>, Schema.toCodecJson<typeof ResourceDomainForbiddenError | typeof ResourceDomainBudgetDiffersError | typeof ResourceDomainInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"getResourceDomain", "GET", "/:resourceDomainId", Schema.toCodecStringTree<Schema.Struct<{
133
225
  readonly resourceDomainId: Schema.String;
134
226
  }>>, never, never, never, Schema.toCodecJson<Schema.Struct<{
135
227
  readonly resourceDomainId: Schema.String;
136
228
  readonly key: Schema.String;
137
229
  readonly createdAt: Schema.String;
230
+ }>>, Schema.toCodecJson<typeof ResourceDomainForbiddenError | typeof ResourceDomainNotFoundError | typeof ResourceDomainInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"getResourceDomainBudget", "GET", "/:resourceDomainId/budget", Schema.toCodecStringTree<Schema.Struct<{
231
+ readonly resourceDomainId: Schema.String;
232
+ }>>, never, never, never, Schema.toCodecJson<Schema.Struct<{
233
+ readonly revision: Schema.Int;
234
+ readonly storageBytes: Schema.Struct<{
235
+ readonly limit: Schema.Int;
236
+ readonly used: Schema.Int;
237
+ readonly reserved: Schema.Int;
238
+ readonly warning: Schema.Literals<readonly ["none", "80-percent", "95-percent", "full"]>;
239
+ /** Archived volumes' data bytes, part of `used`: kept until a person purges them. */
240
+ readonly archived: Schema.Int;
241
+ /** Bytes that backups and snapshots hold, not a volume's unused reserve for them. */
242
+ readonly protection: Schema.Int;
243
+ }>;
244
+ readonly cpuMillicores: Schema.Struct<{
245
+ readonly limit: Schema.Int;
246
+ readonly used: Schema.Int;
247
+ readonly reserved: Schema.Int;
248
+ readonly warning: Schema.Literals<readonly ["none", "80-percent", "95-percent", "full"]>;
249
+ }>;
250
+ readonly memoryBytes: Schema.Struct<{
251
+ readonly limit: Schema.Int;
252
+ readonly used: Schema.Int;
253
+ readonly reserved: Schema.Int;
254
+ readonly warning: Schema.Literals<readonly ["none", "80-percent", "95-percent", "full"]>;
255
+ }>;
138
256
  }>>, Schema.toCodecJson<typeof ResourceDomainForbiddenError | typeof ResourceDomainNotFoundError | typeof ResourceDomainInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"getResourceDomainMember", "GET", "/:resourceDomainId/members/:userId", Schema.toCodecStringTree<Schema.Struct<{
139
257
  readonly resourceDomainId: Schema.String;
140
258
  readonly userId: Schema.String;
@@ -180,5 +298,37 @@ export declare const ResourceDomainsGroup: HttpApiGroup.HttpApiGroup<"resourceDo
180
298
  readonly state: Schema.Literals<readonly ["member", "revoked"]>;
181
299
  readonly version: Schema.Int;
182
300
  readonly updatedAt: Schema.String;
183
- }>>, Schema.toCodecJson<typeof ResourceDomainForbiddenError | typeof ResourceDomainNotFoundError | typeof ResourceDomainPersonNotFoundError | typeof ResourceDomainMemberVersionConflictError | typeof ResourceDomainInternalServerError>, never, never>, false>;
301
+ }>>, Schema.toCodecJson<typeof ResourceDomainForbiddenError | typeof ResourceDomainNotFoundError | typeof ResourceDomainPersonNotFoundError | typeof ResourceDomainMemberVersionConflictError | typeof ResourceDomainInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"setResourceDomainBudget", "PUT", "/:resourceDomainId/budget", Schema.toCodecStringTree<Schema.Struct<{
302
+ readonly resourceDomainId: Schema.String;
303
+ }>>, never, Schema.toCodecJson<Schema.Struct<{
304
+ /** The revision this change replaces: the one the caller last read. */
305
+ readonly revision: Schema.Int;
306
+ readonly cpuMillicores: Schema.Int;
307
+ readonly memoryBytes: Schema.Int;
308
+ readonly storageBytes: Schema.Int;
309
+ }>>, never, Schema.toCodecJson<Schema.Struct<{
310
+ readonly revision: Schema.Int;
311
+ readonly storageBytes: Schema.Struct<{
312
+ readonly limit: Schema.Int;
313
+ readonly used: Schema.Int;
314
+ readonly reserved: Schema.Int;
315
+ readonly warning: Schema.Literals<readonly ["none", "80-percent", "95-percent", "full"]>;
316
+ /** Archived volumes' data bytes, part of `used`: kept until a person purges them. */
317
+ readonly archived: Schema.Int;
318
+ /** Bytes that backups and snapshots hold, not a volume's unused reserve for them. */
319
+ readonly protection: Schema.Int;
320
+ }>;
321
+ readonly cpuMillicores: Schema.Struct<{
322
+ readonly limit: Schema.Int;
323
+ readonly used: Schema.Int;
324
+ readonly reserved: Schema.Int;
325
+ readonly warning: Schema.Literals<readonly ["none", "80-percent", "95-percent", "full"]>;
326
+ }>;
327
+ readonly memoryBytes: Schema.Struct<{
328
+ readonly limit: Schema.Int;
329
+ readonly used: Schema.Int;
330
+ readonly reserved: Schema.Int;
331
+ readonly warning: Schema.Literals<readonly ["none", "80-percent", "95-percent", "full"]>;
332
+ }>;
333
+ }>>, Schema.toCodecJson<typeof ResourceDomainForbiddenError | typeof ResourceDomainNotFoundError | typeof ResourceDomainBudgetRevisionConflictError | typeof ResourceDomainInternalServerError>, never, never>, false>;
184
334
  export {};
@@ -26,9 +26,20 @@
26
26
  * keeps the member's row, with its version. One member reads at `GET …/members/:userId`, and the
27
27
  * list pages by user id: `limit` (100 unless named, at most 500) and the previous page's
28
28
  * `nextCursor` as `cursor`.
29
+ *
30
+ * A budget caps CPU (millicores), memory and storage (bytes): ceilings, not measurements, and
31
+ * zero allows nothing. Storage is one allowance for volumes, archived ones included, and the
32
+ * backups and snapshots that protect them. `ensureResourceDomain` may name a new domain's budget
33
+ * and never changes one: naming another gets `409`, carrying the domain and its revision.
34
+ * `setResourceDomainBudget` takes the `revision` it read: a change made since gets `409`, so a
35
+ * delayed update never overwrites a newer one, and it answers the budget it wrote. Changing a
36
+ * budget changes nothing the domain holds. The read is one snapshot. It reports, per dimension,
37
+ * `used` (confirmed to exist), `reserved` (admitted, not yet confirmed), the `limit` and a `warning`
38
+ * once both reach 80%, 95% or all of it; storage also reports its `archived` and `protection` bytes.
29
39
  */
30
40
  import { Schema } from "effect";
31
41
  import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api";
42
+ import * as Rec from "effect/Record";
32
43
  import { PersonId, RESOURCE_DOMAIN_ID_PREFIX } from "./person-id.js";
33
44
  const RESOURCE_DOMAIN_ID_PATTERN = new RegExp(`^${RESOURCE_DOMAIN_ID_PREFIX}[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`);
34
45
  const RESOURCE_DOMAIN_KEY_PATTERN = /^[a-z][a-z0-9-]{0,31}:[A-Za-z0-9._:/@+=-]{1,223}$/;
@@ -36,12 +47,54 @@ const ResourceDomainId = Schema.String.check(Schema.makeFilter((id) => RESOURCE_
36
47
  const ResourceDomainKey = Schema.String.check(Schema.makeFilter((key) => RESOURCE_DOMAIN_KEY_PATTERN.test(key)
37
48
  ? undefined
38
49
  : "must be <application>:<name>: a lower-case application name (letters, digits, '-'), a colon, then at most 223 letters, digits, '.', '_', ':', '/', '@', '+', '=' or '-'"));
50
+ const Amount = Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }));
51
+ const BudgetRevision = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }));
52
+ /**
53
+ * The dimensions a budget caps, defined once: the contract's schemas, the database's budget, the
54
+ * admission check and the SDK all take theirs from these keys. `freedBy` is what makes room in a
55
+ * dimension besides raising its limit.
56
+ */
57
+ export const RESOURCE_DOMAIN_BUDGET_DIMENSIONS = {
58
+ cpuMillicores: { name: "CPU", freedBy: "stop-objects" },
59
+ memoryBytes: { name: "memory", freedBy: "stop-objects" },
60
+ storageBytes: { name: "storage", freedBy: "purge-objects" },
61
+ };
62
+ export const resourceDomainBudgetLimitsSchema = Schema.Struct(Rec.map(RESOURCE_DOMAIN_BUDGET_DIMENSIONS, () => Amount));
63
+ /** The share of a limit at which the budget read starts to warn. */
64
+ export const RESOURCE_DOMAIN_BUDGET_WARNINGS = { "80-percent": 80, "95-percent": 95 };
65
+ const BudgetWarning = Schema.Literals(["none", "80-percent", "95-percent", "full"]);
66
+ const budgetDimensionSchema = Schema.Struct({
67
+ limit: Amount,
68
+ used: Amount,
69
+ reserved: Amount,
70
+ warning: BudgetWarning,
71
+ });
72
+ export const resourceDomainBudgetSchema = Schema.Struct({
73
+ revision: BudgetRevision,
74
+ ...Rec.map(RESOURCE_DOMAIN_BUDGET_DIMENSIONS, () => budgetDimensionSchema),
75
+ storageBytes: Schema.Struct({
76
+ ...budgetDimensionSchema.fields,
77
+ /** Archived volumes' data bytes, part of `used`: kept until a person purges them. */
78
+ archived: Amount,
79
+ /** Bytes that backups and snapshots hold, not a volume's unused reserve for them. */
80
+ protection: Amount,
81
+ }),
82
+ });
83
+ export const setResourceDomainBudgetRequestSchema = Schema.Struct({
84
+ ...resourceDomainBudgetLimitsSchema.fields,
85
+ /** The revision this change replaces: the one the caller last read. */
86
+ revision: BudgetRevision,
87
+ });
39
88
  export const resourceDomainSchema = Schema.Struct({
40
89
  resourceDomainId: ResourceDomainId,
41
90
  key: ResourceDomainKey,
42
91
  createdAt: Schema.String,
43
92
  });
44
- export const ensureResourceDomainRequestSchema = Schema.Struct({ key: ResourceDomainKey });
93
+ export const ensureResourceDomainRequestSchema = Schema.Struct({
94
+ key: ResourceDomainKey,
95
+ /** The budget a new domain starts with; none starts it at zero. */
96
+ budget: Schema.optional(resourceDomainBudgetLimitsSchema),
97
+ });
45
98
  export const ensureResourceDomainResponseSchema = Schema.Struct({
46
99
  ...resourceDomainSchema.fields,
47
100
  /** True when this call created the domain. */
@@ -90,6 +143,12 @@ export class ResourceDomainMemberVersionConflictError extends Schema.TaggedError
90
143
  current: Schema.Struct({ state: MemberState, version: MemberVersion }),
91
144
  }, { httpApiStatus: 409 }) {
92
145
  }
146
+ /** The domain exists with another budget, at `revision`; nothing changed. */
147
+ export class ResourceDomainBudgetDiffersError extends Schema.TaggedError()("ResourceDomainBudgetDiffersError", { message: Schema.String, resourceDomainId: ResourceDomainId, revision: BudgetRevision }, { httpApiStatus: 409 }) {
148
+ }
149
+ /** `revision` is the current one. */
150
+ export class ResourceDomainBudgetRevisionConflictError extends Schema.TaggedError()("ResourceDomainBudgetRevisionConflictError", { message: Schema.String, revision: BudgetRevision }, { httpApiStatus: 409 }) {
151
+ }
93
152
  export class ResourceDomainInternalServerError extends Schema.TaggedError()("ResourceDomainInternalServerError", { message: Schema.String }, { httpApiStatus: 500 }) {
94
153
  }
95
154
  const memberParams = Schema.Struct({ resourceDomainId: ResourceDomainId, userId: PersonId });
@@ -104,7 +163,11 @@ export const ResourceDomainsGroup = HttpApiGroup.make("resourceDomains")
104
163
  .add(HttpApiEndpoint.post("ensureResourceDomain", "/", {
105
164
  payload: ensureResourceDomainRequestSchema,
106
165
  success: ensureResourceDomainResponseSchema,
107
- error: [ResourceDomainForbiddenError, ResourceDomainInternalServerError],
166
+ error: [
167
+ ResourceDomainForbiddenError,
168
+ ResourceDomainBudgetDiffersError,
169
+ ResourceDomainInternalServerError,
170
+ ],
108
171
  }))
109
172
  .add(HttpApiEndpoint.get("getResourceDomain", "/:resourceDomainId", {
110
173
  params: Schema.Struct({ resourceDomainId: ResourceDomainId }),
@@ -147,6 +210,26 @@ export const ResourceDomainsGroup = HttpApiGroup.make("resourceDomains")
147
210
  ResourceDomainNotFoundError,
148
211
  ResourceDomainInternalServerError,
149
212
  ],
213
+ }))
214
+ .add(HttpApiEndpoint.get("getResourceDomainBudget", "/:resourceDomainId/budget", {
215
+ params: Schema.Struct({ resourceDomainId: ResourceDomainId }),
216
+ success: resourceDomainBudgetSchema,
217
+ error: [
218
+ ResourceDomainForbiddenError,
219
+ ResourceDomainNotFoundError,
220
+ ResourceDomainInternalServerError,
221
+ ],
222
+ }))
223
+ .add(HttpApiEndpoint.put("setResourceDomainBudget", "/:resourceDomainId/budget", {
224
+ params: Schema.Struct({ resourceDomainId: ResourceDomainId }),
225
+ payload: setResourceDomainBudgetRequestSchema,
226
+ success: resourceDomainBudgetSchema,
227
+ error: [
228
+ ResourceDomainForbiddenError,
229
+ ResourceDomainNotFoundError,
230
+ ResourceDomainBudgetRevisionConflictError,
231
+ ResourceDomainInternalServerError,
232
+ ],
150
233
  }))
151
234
  .annotate(HttpApi.PayloadParseOptions, { onExcessProperty: "error" })
152
235
  .annotate(OpenApi.Description, "Owners that are projects, not people: they never hold a login or a provider credential.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealant/api-contracts-next",
3
- "version": "0.40.0-next.770",
3
+ "version": "0.40.0-next.772",
4
4
  "description": "Contract-first HTTP API definitions for Sealant services",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -36,7 +36,7 @@
36
36
  "peerDependencies": {
37
37
  "effect": "^4.0.5"
38
38
  },
39
- "gitHead": "1e8b4f1153f5107504649c863c305cd31f424d58",
39
+ "gitHead": "722ef7c7d08581db5c9c4de770d5630329063a97",
40
40
  "publishConfig": {
41
41
  "access": "public"
42
42
  }