@org-quicko/silo-client 1.0.0 → 1.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.
Files changed (46) hide show
  1. package/README.md +166 -124
  2. package/dist/collections/collection-handle.d.cts +15 -23
  3. package/dist/collections/collection-handle.d.ts +15 -23
  4. package/dist/collections/collections.d.cts +3 -5
  5. package/dist/collections/collections.d.ts +3 -5
  6. package/dist/entries/entry-envelope.d.cts +18 -0
  7. package/dist/entries/entry-envelope.d.ts +18 -0
  8. package/dist/entries/entry-read-options.d.cts +16 -0
  9. package/dist/entries/entry-read-options.d.ts +16 -0
  10. package/dist/entries/entry-reader.d.cts +16 -16
  11. package/dist/entries/entry-reader.d.ts +16 -16
  12. package/dist/entries/entry.d.cts +11 -23
  13. package/dist/entries/entry.d.ts +11 -23
  14. package/dist/errors/network-error.d.cts +5 -0
  15. package/dist/errors/network-error.d.ts +5 -0
  16. package/dist/index.cjs +87 -170
  17. package/dist/index.d.cts +4 -5
  18. package/dist/index.d.ts +4 -5
  19. package/dist/index.js +87 -170
  20. package/dist/media/media-asset.d.cts +17 -0
  21. package/dist/media/media-asset.d.ts +17 -0
  22. package/dist/media/media-file.d.cts +16 -0
  23. package/dist/media/media-file.d.ts +16 -0
  24. package/dist/media/media-replace.d.cts +17 -0
  25. package/dist/media/media-replace.d.ts +17 -0
  26. package/dist/media/media.d.cts +0 -4
  27. package/dist/media/media.d.ts +0 -4
  28. package/dist/search/search-hit.d.cts +5 -5
  29. package/dist/search/search-hit.d.ts +5 -5
  30. package/dist/search/search.d.cts +4 -1
  31. package/dist/search/search.d.ts +4 -1
  32. package/dist/transport/api-path.d.cts +1 -0
  33. package/dist/transport/api-path.d.ts +1 -0
  34. package/dist/transport/transport.d.cts +4 -0
  35. package/dist/transport/transport.d.ts +4 -0
  36. package/package.json +58 -58
  37. package/dist/collections/reserved-field-names.d.cts +0 -9
  38. package/dist/collections/reserved-field-names.d.ts +0 -9
  39. package/dist/entries/entry-base.d.cts +0 -31
  40. package/dist/entries/entry-base.d.ts +0 -31
  41. package/dist/entries/entry-mapper.d.cts +0 -13
  42. package/dist/entries/entry-mapper.d.ts +0 -13
  43. package/dist/entries/entry-payload.d.cts +0 -12
  44. package/dist/entries/entry-payload.d.ts +0 -12
  45. package/dist/entries/resolved-entry.d.cts +0 -11
  46. package/dist/entries/resolved-entry.d.ts +0 -11
package/dist/index.js CHANGED
@@ -84,6 +84,9 @@ class ApiPath {
84
84
  static mediaAssetUsages(id) {
85
85
  return `${ApiPath.mediaAsset(id)}/usages`;
86
86
  }
87
+ static mediaAssetContent(id) {
88
+ return `${ApiPath.mediaAsset(id)}/content`;
89
+ }
87
90
  static mediaBulkDelete() {
88
91
  return "/api/media/delete";
89
92
  }
@@ -130,6 +133,22 @@ class MediaAssetMapper {
130
133
  }
131
134
  }
132
135
 
136
+ // src/media/media-file.ts
137
+ class MediaFile {
138
+ static toBlob(bytes, contentType) {
139
+ if (bytes instanceof Blob)
140
+ return bytes;
141
+ return new Blob([bytes], contentType ? { type: contentType } : undefined);
142
+ }
143
+ static nameOf(input, explicit) {
144
+ if (explicit)
145
+ return explicit;
146
+ if (input instanceof File && input.name)
147
+ return input.name;
148
+ throw new Error("media: a Blob has no filename — pass { filename } explicitly");
149
+ }
150
+ }
151
+
133
152
  // src/media/media-reference.ts
134
153
  class MediaReference {
135
154
  static Scheme = "silo://media/";
@@ -291,6 +310,22 @@ class MediaAsset {
291
310
  async setTags(tags, options) {
292
311
  return this.patch({ tags: [...tags] }, options);
293
312
  }
313
+ async replace(input, options) {
314
+ const form = new FormData;
315
+ if (input instanceof Blob) {
316
+ form.set("file", input, MediaFile.nameOf(input, options?.filename));
317
+ } else {
318
+ form.set("file", MediaFile.toBlob(input.bytes, input.contentType), input.filename);
319
+ }
320
+ const payload = await this.transport.upload({
321
+ method: "POST",
322
+ path: ApiPath.mediaAssetContent(this.id),
323
+ signal: options?.signal,
324
+ timeoutMilliseconds: options?.timeoutMilliseconds
325
+ }, form);
326
+ this.record = MediaAssetMapper.toRecord(payload);
327
+ return this;
328
+ }
294
329
  async delete(options) {
295
330
  await this.transport.empty({
296
331
  method: "DELETE",
@@ -524,11 +559,11 @@ class Media {
524
559
  let options;
525
560
  if (input instanceof Blob) {
526
561
  const fileOptions = second ?? {};
527
- form.set("file", input, Media.filenameOf(input, fileOptions.filename));
562
+ form.set("file", input, MediaFile.nameOf(input, fileOptions.filename));
528
563
  folder = fileOptions.folder;
529
564
  options = third ?? fileOptions;
530
565
  } else {
531
- form.set("file", Media.toBlob(input.bytes, input.contentType), input.filename);
566
+ form.set("file", MediaFile.toBlob(input.bytes, input.contentType), input.filename);
532
567
  folder = input.folder;
533
568
  options = second;
534
569
  }
@@ -596,18 +631,6 @@ class Media {
596
631
  sort: query.sort
597
632
  };
598
633
  }
599
- static toBlob(bytes, contentType) {
600
- if (bytes instanceof Blob)
601
- return bytes;
602
- return new Blob([bytes], contentType ? { type: contentType } : undefined);
603
- }
604
- static filenameOf(input, explicit) {
605
- if (explicit)
606
- return explicit;
607
- if (input instanceof File && input.name)
608
- return input.name;
609
- throw new Error("media upload: a Blob has no filename — pass { filename } explicitly");
610
- }
611
634
  }
612
635
 
613
636
  // src/variables/variable.ts
@@ -678,96 +701,6 @@ class ProjectVariables {
678
701
  }
679
702
  }
680
703
 
681
- // src/entries/entry-mapper.ts
682
- class EntryMapper {
683
- static fieldsOf(payload) {
684
- const { id: _id, rev: _rev, created_at: _createdAt, updated_at: _updatedAt, ...fields } = payload;
685
- return fields;
686
- }
687
- static toPayload(id, rev, fields, createdAt, updatedAt) {
688
- return {
689
- id,
690
- rev,
691
- ...fields,
692
- created_at: createdAt.toISOString(),
693
- updated_at: updatedAt.toISOString()
694
- };
695
- }
696
- }
697
-
698
- // src/entries/entry-base.ts
699
- class EntryBase {
700
- context;
701
- id;
702
- rev;
703
- createdAt;
704
- updatedAt;
705
- fields;
706
- constructor(context, payload) {
707
- this.context = context;
708
- this.id = String(payload.id);
709
- this.rev = Number(payload.rev);
710
- this.createdAt = new Date(payload.created_at);
711
- this.updatedAt = new Date(payload.updated_at);
712
- this.fields = EntryMapper.fieldsOf(payload);
713
- }
714
- toJSON() {
715
- return EntryMapper.toPayload(this.id, this.rev, this.fields, this.createdAt, this.updatedAt);
716
- }
717
- async delete(options = {}) {
718
- await this.context.transport.empty({
719
- method: "DELETE",
720
- path: ApiPath.entry(this.context.project, this.context.environment, this.context.collection, this.id),
721
- query: { rev: this.rev },
722
- ...options
723
- });
724
- }
725
- }
726
-
727
- // src/entries/resolved-entry.ts
728
- class ResolvedEntry extends EntryBase {
729
- }
730
-
731
- // src/entries/entry.ts
732
- class Entry extends ResolvedEntry {
733
- saving = false;
734
- async save(options = {}) {
735
- if (this.saving) {
736
- throw new Error(`cannot save entry "${this.id}": a previous save() on this instance has not finished yet`);
737
- }
738
- this.saving = true;
739
- try {
740
- const payload = await this.context.transport.json({
741
- method: "PUT",
742
- path: ApiPath.entry(this.context.project, this.context.environment, this.context.collection, this.id),
743
- query: { rev: this.rev, variables: "raw" },
744
- body: this.fields,
745
- ...options
746
- });
747
- this.adopt(payload);
748
- return this;
749
- } finally {
750
- this.saving = false;
751
- }
752
- }
753
- async refresh(options = {}) {
754
- const payload = await this.context.transport.json({
755
- method: "GET",
756
- path: ApiPath.entry(this.context.project, this.context.environment, this.context.collection, this.id),
757
- query: { variables: "raw" },
758
- ...options
759
- });
760
- this.adopt(payload);
761
- return this;
762
- }
763
- adopt(payload) {
764
- this.rev = Number(payload.rev);
765
- this.createdAt = new Date(payload.created_at);
766
- this.updatedAt = new Date(payload.updated_at);
767
- this.fields = EntryMapper.fieldsOf(payload);
768
- }
769
- }
770
-
771
704
  // src/transport/page-payload.ts
772
705
  class PagePayload {
773
706
  static read(body) {
@@ -828,40 +761,39 @@ class EntryStream extends RowStream {
828
761
 
829
762
  // src/entries/entry-reader.ts
830
763
  class EntryReader {
831
- context;
832
- variables;
833
- wrap;
834
- constructor(context, variables, wrap) {
835
- this.context = context;
836
- this.variables = variables;
837
- this.wrap = wrap;
838
- }
839
- async get(id, options = {}) {
840
- const payload = await this.context.transport.json({
764
+ scope;
765
+ collection;
766
+ constructor(scope, collection) {
767
+ this.scope = scope;
768
+ this.collection = collection;
769
+ }
770
+ get(id, options = {}) {
771
+ const { variables, ...request } = options;
772
+ return this.scope.transport.json({
841
773
  method: "GET",
842
- path: ApiPath.entry(this.context.project, this.context.environment, this.context.collection, id),
843
- query: { variables: this.variables },
844
- ...options
774
+ path: ApiPath.entry(this.scope.project, this.scope.environment, this.collection, id),
775
+ query: { variables },
776
+ ...request
845
777
  });
846
- return this.wrap(payload);
847
778
  }
848
779
  async list(query = {}, options = {}) {
780
+ const { variables, ...request } = options;
849
781
  const loader = (window2) => this.list({ ...query, limit: window2.limit, offset: window2.offset }, options);
850
- const body = await this.context.transport.json({
782
+ const body = await this.scope.transport.json({
851
783
  method: "GET",
852
- path: ApiPath.entries(this.context.project, this.context.environment, this.context.collection),
784
+ path: ApiPath.entries(this.scope.project, this.scope.environment, this.collection),
853
785
  query: {
854
786
  limit: query.limit,
855
787
  offset: query.offset,
856
788
  filter: query.where?.toJSON(),
857
789
  sort: query.sort === undefined ? undefined : String(query.sort),
858
- variables: this.variables
790
+ variables
859
791
  },
860
- ...options
792
+ ...request
861
793
  });
862
794
  const page = PagePayload.read(body);
863
795
  const window = new PageWindow(page.limit ?? query.limit ?? 50, page.offset ?? query.offset ?? 0);
864
- return new EntryPage(page.rows.map((payload) => this.wrap(payload)), page.total, window, loader);
796
+ return new EntryPage(page.rows, page.total, window, loader);
865
797
  }
866
798
  all(query = {}, options = {}) {
867
799
  const loader = async (window) => {
@@ -1035,15 +967,15 @@ class Search {
1035
967
  const window = new PageWindow(page.limit ?? query.limit ?? 50, page.offset ?? query.offset ?? 0);
1036
968
  const truncated = body.truncated === true;
1037
969
  const engine = body.engine;
1038
- const hits = page.rows.map((hit) => this.toHit(hit));
970
+ const hits = page.rows.map((hit) => Search.toHit(hit));
1039
971
  return new SearchPage(hits, page.total, window, truncated, engine, loader);
1040
972
  }
1041
- toHit(payload) {
973
+ static toHit(payload) {
1042
974
  return {
1043
975
  project: payload.project,
1044
976
  environment: payload.env,
1045
977
  collection: payload.collection,
1046
- entry: new ResolvedEntry({ transport: this.transport, project: payload.project, environment: payload.env, collection: payload.collection }, payload.entry),
978
+ entry: payload.entry,
1047
979
  snippets: payload.snippets
1048
980
  };
1049
981
  }
@@ -1106,36 +1038,24 @@ class CollectionHandle {
1106
1038
  name;
1107
1039
  filter = new TypedFilter;
1108
1040
  schema;
1109
- editable;
1110
- context;
1111
- resolved;
1041
+ reader;
1112
1042
  constructor(scope, name) {
1113
1043
  this.scope = scope;
1114
1044
  this.name = name;
1115
1045
  this.schema = new CollectionSchema(scope, name);
1116
- this.context = {
1117
- transport: scope.transport,
1118
- project: scope.project,
1119
- environment: scope.environment,
1120
- collection: name
1121
- };
1122
- this.resolved = new EntryReader(this.context, undefined, (payload) => new ResolvedEntry(this.context, payload));
1123
- this.editable = new EntryReader(this.context, "raw", (payload) => new Entry(this.context, payload));
1046
+ this.reader = new EntryReader(scope, name);
1124
1047
  }
1125
1048
  get(id, options = {}) {
1126
- return this.resolved.get(id, options);
1127
- }
1128
- edit(id, options = {}) {
1129
- return this.editable.get(id, options);
1049
+ return this.reader.get(id, options);
1130
1050
  }
1131
1051
  list(query = {}, options = {}) {
1132
- return this.resolved.list(query, options);
1052
+ return this.reader.list(query, options);
1133
1053
  }
1134
1054
  all(query = {}, options = {}) {
1135
- return this.resolved.all(query, options);
1055
+ return this.reader.all(query, options);
1136
1056
  }
1137
1057
  pages(query = {}, options = {}) {
1138
- return this.resolved.pages(query, options);
1058
+ return this.reader.pages(query, options);
1139
1059
  }
1140
1060
  create(fields, options = {}) {
1141
1061
  return this.write("POST", ApiPath.entries(this.scope.project, this.scope.environment, this.name), fields, undefined, options);
@@ -1165,23 +1085,14 @@ class CollectionHandle {
1165
1085
  });
1166
1086
  return RenameReport.fromWire(payload);
1167
1087
  }
1168
- async write(method, path, fields, rev, options) {
1169
- const payload = await this.scope.transport.json({
1088
+ write(method, path, fields, rev, options) {
1089
+ return this.scope.transport.json({
1170
1090
  method,
1171
1091
  path,
1172
1092
  query: { rev, variables: "raw" },
1173
1093
  body: fields,
1174
1094
  ...options
1175
1095
  });
1176
- return new Entry(this.context, payload);
1177
- }
1178
- }
1179
-
1180
- // src/collections/reserved-field-names.ts
1181
- class ReservedFieldNames {
1182
- static all = ["id", "rev", "seq", "created_at", "updated_at"];
1183
- static isReserved(name) {
1184
- return ReservedFieldNames.all.includes(name);
1185
1096
  }
1186
1097
  }
1187
1098
 
@@ -1200,7 +1111,6 @@ class Collections {
1200
1111
  return body.items.map(Collections.toSummary);
1201
1112
  }
1202
1113
  async create(name, schema, options = {}) {
1203
- Collections.warnOnReservedFields(schema);
1204
1114
  return this.scope.transport.json({
1205
1115
  method: "POST",
1206
1116
  path: ApiPath.collections(this.scope.project, this.scope.environment),
@@ -1218,13 +1128,6 @@ class Collections {
1218
1128
  updatedAt: new Date(payload.updated_at)
1219
1129
  };
1220
1130
  }
1221
- static warnOnReservedFields(schema) {
1222
- for (const name of Object.keys(schema.properties ?? {})) {
1223
- if (ReservedFieldNames.isReserved(name)) {
1224
- console.warn(`@org-quicko/silo-client: collection schema declares reserved field "${name}", which the server never returns`);
1225
- }
1226
- }
1227
- }
1228
1131
  }
1229
1132
 
1230
1133
  // src/variables/environment-variables.ts
@@ -1591,12 +1494,19 @@ class NetworkError extends Error {
1591
1494
  method;
1592
1495
  path;
1593
1496
  constructor(method, path, cause) {
1594
- super(`network error on ${method} ${path}: the request never reached the server`, { cause });
1497
+ super(`network error on ${method} ${path}: the request never reached the server${NetworkError.reason(cause)}`, { cause });
1595
1498
  this.name = "NetworkError";
1596
1499
  this.method = method;
1597
1500
  this.path = path;
1598
1501
  Object.setPrototypeOf(this, NetworkError.prototype);
1599
1502
  }
1503
+ static reason(cause) {
1504
+ if (cause instanceof Error && cause.message)
1505
+ return ` (${cause.message})`;
1506
+ if (typeof cause === "string" && cause)
1507
+ return ` (${cause})`;
1508
+ return "";
1509
+ }
1600
1510
  }
1601
1511
 
1602
1512
  // src/errors/timeout-error.ts
@@ -1710,7 +1620,7 @@ class Transport {
1710
1620
  this.key = options.key;
1711
1621
  this.headers = options.headers ?? {};
1712
1622
  this.timeoutMilliseconds = options.timeoutMilliseconds;
1713
- this.fetchFunction = options.fetch ?? fetch;
1623
+ this.fetchFunction = Transport.resolveFetch(options.fetch);
1714
1624
  }
1715
1625
  async json(request) {
1716
1626
  const response = await this.execute(request);
@@ -1746,9 +1656,10 @@ class Transport {
1746
1656
  async execute(request, form) {
1747
1657
  const abortSignals = new AbortSignals(request.signal, request.timeoutMilliseconds ?? this.timeoutMilliseconds);
1748
1658
  const url = `${this.url}${request.path}${QueryString.build(request.query)}`;
1659
+ const sendRequest = this.fetchFunction;
1749
1660
  let response;
1750
1661
  try {
1751
- response = await this.fetchFunction(url, {
1662
+ response = await sendRequest(url, {
1752
1663
  method: request.method,
1753
1664
  headers: this.buildHeaders(request, Boolean(form)),
1754
1665
  body: form ?? (request.body === undefined ? undefined : JSON.stringify(request.body)),
@@ -1786,6 +1697,14 @@ class Transport {
1786
1697
  static normalizeUrl(url) {
1787
1698
  return url.replace(/\/+$/, "");
1788
1699
  }
1700
+ static resolveFetch(fetchFunction) {
1701
+ if (fetchFunction)
1702
+ return fetchFunction;
1703
+ if (typeof globalThis.fetch !== "function") {
1704
+ throw new Error("this runtime has no global fetch: pass one as SiloOptions.fetch");
1705
+ }
1706
+ return globalThis.fetch.bind(globalThis);
1707
+ }
1789
1708
  }
1790
1709
 
1791
1710
  // src/silo.ts
@@ -1870,6 +1789,7 @@ class RouteInventory {
1870
1789
  "PATCH /api/media/:id",
1871
1790
  "DELETE /api/media/:id",
1872
1791
  "GET /api/media/:id/usages",
1792
+ "POST /api/media/:id/content",
1873
1793
  "POST /api/media/delete",
1874
1794
  "GET /api/media/folders",
1875
1795
  "POST /api/media/folders",
@@ -1880,6 +1800,7 @@ class RouteInventory {
1880
1800
  "GET /api/session": "key introspection, an operator surface",
1881
1801
  "GET /api/keys": "keys and claims are an operator surface",
1882
1802
  "POST /api/keys": "keys and claims are an operator surface",
1803
+ "PATCH /api/keys/:id": "keys and claims are an operator surface",
1883
1804
  "DELETE /api/keys/:id": "keys and claims are an operator surface",
1884
1805
  "GET /api/audit": "an operator surface",
1885
1806
  "GET /api/observability": "an operator surface",
@@ -2007,8 +1928,6 @@ export {
2007
1928
  Search,
2008
1929
  RowStream,
2009
1930
  RouteInventory,
2010
- ResolvedEntry,
2011
- ReservedFieldNames,
2012
1931
  RequestAbortedError,
2013
1932
  RenameReport,
2014
1933
  Projects,
@@ -2044,8 +1963,6 @@ export {
2044
1963
  EntryReader,
2045
1964
  EntryPageStream,
2046
1965
  EntryPage,
2047
- EntryBase,
2048
- Entry,
2049
1966
  ConflictError,
2050
1967
  Collections,
2051
1968
  CollectionSchema,
@@ -1,6 +1,7 @@
1
1
  import type { RequestOptions } from "../request-options.cjs";
2
2
  import type { Transport } from "../transport/transport.cjs";
3
3
  import type { MediaDeleteOptions } from "./media-delete-options.cjs";
4
+ import type { MediaReplace, MediaReplaceOptions } from "./media-replace.cjs";
4
5
  import type { MediaUsageQuery } from "./media-usage-page.cjs";
5
6
  import { MediaUsagePage } from "./media-usage-page.cjs";
6
7
  /** `MediaAsset`'s mapped state — `MediaAssetMapper.toRecord`'s output. */
@@ -53,6 +54,22 @@ export declare class MediaAsset {
53
54
  moveTo(folder: string, options?: RequestOptions): Promise<this>;
54
55
  /** REPLACES the tag list — `PATCH` replaces, it does not append. */
55
56
  setTags(tags: readonly string[], options?: RequestOptions): Promise<this>;
57
+ /**
58
+ * Swaps the bytes behind this asset (D67). `id`, `reference`, `url`,
59
+ * `filename` and `folder` all survive; `hash`, `sizeInBytes` and
60
+ * `contentType` are re-read from the server's answer.
61
+ *
62
+ * Every entry referencing it now resolves to the new file, without any of
63
+ * them being rewritten — which is the point, and the reason the server asks
64
+ * for `media:replace` **and** `entries:update` at every scope that refers
65
+ * to it.
66
+ *
67
+ * The new file must keep the current extension: the blob key's suffix is
68
+ * derived from the filename at upload and is the visible tail of the URL on
69
+ * a bucket-backed instance, so a `.png` asset takes a `.png` replacement.
70
+ * Converting a file is a new upload, not a replacement of this one.
71
+ */
72
+ replace(input: MediaReplace, options?: MediaReplaceOptions): Promise<this>;
56
73
  /** Refused while an entry references it; `{ force: true }` also needs
57
74
  * `entries:update` on every scope this asset reaches. */
58
75
  delete(options?: MediaDeleteOptions): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  import type { RequestOptions } from "../request-options.js";
2
2
  import type { Transport } from "../transport/transport.js";
3
3
  import type { MediaDeleteOptions } from "./media-delete-options.js";
4
+ import type { MediaReplace, MediaReplaceOptions } from "./media-replace.js";
4
5
  import type { MediaUsageQuery } from "./media-usage-page.js";
5
6
  import { MediaUsagePage } from "./media-usage-page.js";
6
7
  /** `MediaAsset`'s mapped state — `MediaAssetMapper.toRecord`'s output. */
@@ -53,6 +54,22 @@ export declare class MediaAsset {
53
54
  moveTo(folder: string, options?: RequestOptions): Promise<this>;
54
55
  /** REPLACES the tag list — `PATCH` replaces, it does not append. */
55
56
  setTags(tags: readonly string[], options?: RequestOptions): Promise<this>;
57
+ /**
58
+ * Swaps the bytes behind this asset (D67). `id`, `reference`, `url`,
59
+ * `filename` and `folder` all survive; `hash`, `sizeInBytes` and
60
+ * `contentType` are re-read from the server's answer.
61
+ *
62
+ * Every entry referencing it now resolves to the new file, without any of
63
+ * them being rewritten — which is the point, and the reason the server asks
64
+ * for `media:replace` **and** `entries:update` at every scope that refers
65
+ * to it.
66
+ *
67
+ * The new file must keep the current extension: the blob key's suffix is
68
+ * derived from the filename at upload and is the visible tail of the URL on
69
+ * a bucket-backed instance, so a `.png` asset takes a `.png` replacement.
70
+ * Converting a file is a new upload, not a replacement of this one.
71
+ */
72
+ replace(input: MediaReplace, options?: MediaReplaceOptions): Promise<this>;
56
73
  /** Refused while an entry references it; `{ force: true }` also needs
57
74
  * `entries:update` on every scope this asset reaches. */
58
75
  delete(options?: MediaDeleteOptions): Promise<void>;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Turning what a caller passed into the two things a multipart part needs: a
3
+ * `Blob` and a filename.
4
+ *
5
+ * Shared by `Media.upload` and `MediaAsset.replace` (D67) rather than copied
6
+ * into the second. The filename is not cosmetic on either path — the server
7
+ * reads the extension off it to decide what the library accepts, and on a
8
+ * replace to decide whether the file's type is changing at all — so the two
9
+ * entry points must derive it the same way.
10
+ */
11
+ export declare class MediaFile {
12
+ static toBlob(bytes: Uint8Array | ArrayBuffer | Blob, contentType?: string): Blob;
13
+ /** A `File` carries its own name; a bare `Blob` does not, and sending it
14
+ * as "blob" is worse than refusing outright. */
15
+ static nameOf(input: Blob, explicit?: string): string;
16
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Turning what a caller passed into the two things a multipart part needs: a
3
+ * `Blob` and a filename.
4
+ *
5
+ * Shared by `Media.upload` and `MediaAsset.replace` (D67) rather than copied
6
+ * into the second. The filename is not cosmetic on either path — the server
7
+ * reads the extension off it to decide what the library accepts, and on a
8
+ * replace to decide whether the file's type is changing at all — so the two
9
+ * entry points must derive it the same way.
10
+ */
11
+ export declare class MediaFile {
12
+ static toBlob(bytes: Uint8Array | ArrayBuffer | Blob, contentType?: string): Blob;
13
+ /** A `File` carries its own name; a bare `Blob` does not, and sending it
14
+ * as "blob" is worse than refusing outright. */
15
+ static nameOf(input: Blob, explicit?: string): string;
16
+ }
@@ -0,0 +1,17 @@
1
+ import type { RequestOptions } from "../request-options.cjs";
2
+ /** The fully-described replacement: raw bytes plus the name whose extension
3
+ * the server checks against the one the asset already has. No `folder` — a
4
+ * replace changes bytes and nothing else about where the asset sits. */
5
+ export interface MediaReplaceBytes {
6
+ bytes: Uint8Array | ArrayBuffer | Blob;
7
+ filename: string;
8
+ contentType?: string;
9
+ }
10
+ /** The filename to read when the input carries none — a `File` has `.name`,
11
+ * a bare `Blob` does not, and the extension is what the server checks. */
12
+ export interface MediaReplaceOptions extends RequestOptions {
13
+ filename?: string;
14
+ }
15
+ /** `MediaAsset.replace()`'s input: a fully-described replacement, or a
16
+ * runtime `File`/`Blob`. */
17
+ export type MediaReplace = MediaReplaceBytes | File | Blob;
@@ -0,0 +1,17 @@
1
+ import type { RequestOptions } from "../request-options.js";
2
+ /** The fully-described replacement: raw bytes plus the name whose extension
3
+ * the server checks against the one the asset already has. No `folder` — a
4
+ * replace changes bytes and nothing else about where the asset sits. */
5
+ export interface MediaReplaceBytes {
6
+ bytes: Uint8Array | ArrayBuffer | Blob;
7
+ filename: string;
8
+ contentType?: string;
9
+ }
10
+ /** The filename to read when the input carries none — a `File` has `.name`,
11
+ * a bare `Blob` does not, and the extension is what the server checks. */
12
+ export interface MediaReplaceOptions extends RequestOptions {
13
+ filename?: string;
14
+ }
15
+ /** `MediaAsset.replace()`'s input: a fully-described replacement, or a
16
+ * runtime `File`/`Blob`. */
17
+ export type MediaReplace = MediaReplaceBytes | File | Blob;
@@ -30,8 +30,4 @@ export declare class Media {
30
30
  * per-id outcomes live in the report, not in a thrown error. */
31
31
  deleteMany(ids: readonly string[], options?: MediaDeleteOptions): Promise<MediaDeleteReport>;
32
32
  private static toWireQuery;
33
- private static toBlob;
34
- /** A `File` carries its own name; a bare `Blob` does not, and uploading it
35
- * as "blob" is worse than refusing outright. */
36
- private static filenameOf;
37
33
  }
@@ -30,8 +30,4 @@ export declare class Media {
30
30
  * per-id outcomes live in the report, not in a thrown error. */
31
31
  deleteMany(ids: readonly string[], options?: MediaDeleteOptions): Promise<MediaDeleteReport>;
32
32
  private static toWireQuery;
33
- private static toBlob;
34
- /** A `File` carries its own name; a bare `Blob` does not, and uploading it
35
- * as "blob" is worse than refusing outright. */
36
- private static filenameOf;
37
33
  }
@@ -1,18 +1,18 @@
1
- import type { ResolvedEntry } from "../entries/resolved-entry.cjs";
1
+ import type { Entry } from "../entries/entry.cjs";
2
2
  import type { SearchSnippet } from "./search-snippet.cjs";
3
3
  /**
4
4
  * One result. The location sits on the hit rather than on the entry, which is
5
5
  * what lets a caller link to a result found outside the scope on screen, and
6
6
  * `environment` is mapped from the wire's `env`.
7
7
  *
8
- * The entry is always a `ResolvedEntry`: a result is not addressed to one
9
- * typed collection, so there is no `Fields` to give it, and it is not meant to
10
- * be edited from here — re-read through `collection.edit()` for that.
8
+ * The entry is untyped: a result is not addressed to one collection, so there
9
+ * is no `Fields` to give it. Its `{{NAME}}` templates are resolved, so read it
10
+ * again through `collection.get(id, { variables: "raw" })` before editing.
11
11
  */
12
12
  export interface SearchHit {
13
13
  readonly project: string;
14
14
  readonly environment: string;
15
15
  readonly collection: string;
16
- readonly entry: ResolvedEntry<unknown>;
16
+ readonly entry: Entry;
17
17
  readonly snippets: readonly SearchSnippet[];
18
18
  }
@@ -1,18 +1,18 @@
1
- import type { ResolvedEntry } from "../entries/resolved-entry.js";
1
+ import type { Entry } from "../entries/entry.js";
2
2
  import type { SearchSnippet } from "./search-snippet.js";
3
3
  /**
4
4
  * One result. The location sits on the hit rather than on the entry, which is
5
5
  * what lets a caller link to a result found outside the scope on screen, and
6
6
  * `environment` is mapped from the wire's `env`.
7
7
  *
8
- * The entry is always a `ResolvedEntry`: a result is not addressed to one
9
- * typed collection, so there is no `Fields` to give it, and it is not meant to
10
- * be edited from here — re-read through `collection.edit()` for that.
8
+ * The entry is untyped: a result is not addressed to one collection, so there
9
+ * is no `Fields` to give it. Its `{{NAME}}` templates are resolved, so read it
10
+ * again through `collection.get(id, { variables: "raw" })` before editing.
11
11
  */
12
12
  export interface SearchHit {
13
13
  readonly project: string;
14
14
  readonly environment: string;
15
15
  readonly collection: string;
16
- readonly entry: ResolvedEntry<unknown>;
16
+ readonly entry: Entry;
17
17
  readonly snippets: readonly SearchSnippet[];
18
18
  }
@@ -14,5 +14,8 @@ export declare class Search {
14
14
  private readonly reach;
15
15
  constructor(transport: Transport, reach: SearchReach);
16
16
  run(query: SearchQuery, options?: RequestOptions): Promise<SearchPage>;
17
- private toHit;
17
+ /** `env` is the only rename: the hit's own location, which sits here rather
18
+ * than on the entry so a result found outside the scope on screen can still
19
+ * be linked to. */
20
+ private static toHit;
18
21
  }
@@ -14,5 +14,8 @@ export declare class Search {
14
14
  private readonly reach;
15
15
  constructor(transport: Transport, reach: SearchReach);
16
16
  run(query: SearchQuery, options?: RequestOptions): Promise<SearchPage>;
17
- private toHit;
17
+ /** `env` is the only rename: the hit's own location, which sits here rather
18
+ * than on the entry so a result found outside the scope on screen can still
19
+ * be linked to. */
20
+ private static toHit;
18
21
  }
@@ -31,6 +31,7 @@ export declare class ApiPath {
31
31
  static mediaExtensions(): string;
32
32
  static mediaAsset(id: string): string;
33
33
  static mediaAssetUsages(id: string): string;
34
+ static mediaAssetContent(id: string): string;
34
35
  static mediaBulkDelete(): string;
35
36
  static mediaFolders(): string;
36
37
  }