@backstage/catalog-client 1.7.0-next.1 → 1.7.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @backstage/catalog-client
2
2
 
3
+ ## 1.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 78475c3: Allow offset mode paging in entity list provider
8
+ - 29e57c7: Add catalog service mocks under the `/testUtils` subpath export.
9
+
10
+ ### Patch Changes
11
+
12
+ - 1882cfe: Moved `getEntities` ordering to utilize database instead of having it inside catalog client
13
+
14
+ Please note that the latest version of `@backstage/catalog-client` will not order the entities in the same way as before. This is because the ordering is now done in the database query instead of in the client. If you rely on the ordering of the entities, you may need to update your backend plugin or code to handle this change.
15
+
16
+ - Updated dependencies
17
+ - @backstage/catalog-model@1.7.0
18
+ - @backstage/errors@1.2.4
19
+
3
20
  ## 1.7.0-next.1
4
21
 
5
22
  ### Minor Changes
@@ -0,0 +1,44 @@
1
+ import '@backstage/catalog-model';
2
+ import '@backstage/errors';
3
+
4
+ const SPECIAL_KEYS = [
5
+ "attachments",
6
+ "relations",
7
+ "status",
8
+ "metadata.name",
9
+ "metadata.namespace",
10
+ "metadata.uid",
11
+ "metadata.etag"
12
+ ];
13
+ function traverse(root) {
14
+ const output = [];
15
+ function visit(path, current) {
16
+ if (SPECIAL_KEYS.includes(path)) {
17
+ return;
18
+ }
19
+ if (current === void 0 || current === null || ["string", "number", "boolean"].includes(typeof current)) {
20
+ output.push({ key: path, value: current });
21
+ return;
22
+ }
23
+ if (typeof current !== "object") {
24
+ return;
25
+ }
26
+ if (Array.isArray(current)) {
27
+ for (const item of current) {
28
+ visit(path, item);
29
+ if (typeof item === "string") {
30
+ output.push({ key: `${path}.${item}`, value: true });
31
+ }
32
+ }
33
+ return;
34
+ }
35
+ for (const [key, value] of Object.entries(current)) {
36
+ visit(path ? `${path}.${key}` : key, value);
37
+ }
38
+ }
39
+ visit("", root);
40
+ return output;
41
+ }
42
+
43
+ export { traverse };
44
+ //# sourceMappingURL=buildEntitySearch.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildEntitySearch.esm.js","sources":["../../../../../../../../../plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { DEFAULT_NAMESPACE, Entity } from '@backstage/catalog-model';\nimport { InputError } from '@backstage/errors';\nimport { DbSearchRow } from '../../tables';\n\n// These are excluded in the generic loop, either because they do not make sense\n// to index, or because they are special-case always inserted whether they are\n// null or not\nconst SPECIAL_KEYS = [\n 'attachments',\n 'relations',\n 'status',\n 'metadata.name',\n 'metadata.namespace',\n 'metadata.uid',\n 'metadata.etag',\n];\n\n// The maximum length allowed for search values. These columns are indexed, and\n// database engines do not like to index on massive values. For example,\n// postgres will balk after 8191 byte line sizes.\nconst MAX_KEY_LENGTH = 200;\nconst MAX_VALUE_LENGTH = 200;\n\ntype Kv = {\n key: string;\n value: unknown;\n};\n\n// Helper for traversing through a nested structure and outputting a list of\n// path->value entries of the leaves.\n//\n// For example, this yaml structure\n//\n// a: 1\n// b:\n// c: null\n// e: [f, g]\n// h:\n// - i: 1\n// j: k\n// - i: 2\n// j: l\n//\n// will result in\n//\n// \"a\", 1\n// \"b.c\", null\n// \"b.e\": \"f\"\n// \"b.e.f\": true\n// \"b.e\": \"g\"\n// \"b.e.g\": true\n// \"h.i\": 1\n// \"h.j\": \"k\"\n// \"h.i\": 2\n// \"h.j\": \"l\"\nexport function traverse(root: unknown): Kv[] {\n const output: Kv[] = [];\n\n function visit(path: string, current: unknown) {\n if (SPECIAL_KEYS.includes(path)) {\n return;\n }\n\n // empty or scalar\n if (\n current === undefined ||\n current === null ||\n ['string', 'number', 'boolean'].includes(typeof current)\n ) {\n output.push({ key: path, value: current });\n return;\n }\n\n // unknown\n if (typeof current !== 'object') {\n return;\n }\n\n // array\n if (Array.isArray(current)) {\n for (const item of current) {\n // NOTE(freben): The reason that these are output in two different ways,\n // is to support use cases where you want to express that MORE than one\n // tag is present in a list. Since the EntityFilters structure is a\n // record, you can't have several entries of the same key. Therefore\n // you will have to match on\n //\n // { \"a.b\": [\"true\"], \"a.c\": [\"true\"] }\n //\n // rather than\n //\n // { \"a\": [\"b\", \"c\"] }\n //\n // because the latter means EITHER b or c has to be present.\n visit(path, item);\n if (typeof item === 'string') {\n output.push({ key: `${path}.${item}`, value: true });\n }\n }\n return;\n }\n\n // object\n for (const [key, value] of Object.entries(current!)) {\n visit(path ? `${path}.${key}` : key, value);\n }\n }\n\n visit('', root);\n\n return output;\n}\n\n// Translates a number of raw data rows to search table rows\nexport function mapToRows(input: Kv[], entityId: string): DbSearchRow[] {\n const result: DbSearchRow[] = [];\n\n for (const { key: rawKey, value: rawValue } of input) {\n const key = rawKey.toLocaleLowerCase('en-US');\n if (key.length > MAX_KEY_LENGTH) {\n continue;\n }\n if (rawValue === undefined || rawValue === null) {\n result.push({\n entity_id: entityId,\n key,\n original_value: null,\n value: null,\n });\n } else {\n const value = String(rawValue).toLocaleLowerCase('en-US');\n if (value.length <= MAX_VALUE_LENGTH) {\n result.push({\n entity_id: entityId,\n key,\n original_value: String(rawValue),\n value: value,\n });\n } else {\n result.push({\n entity_id: entityId,\n key,\n original_value: null,\n value: null,\n });\n }\n }\n }\n\n return result;\n}\n\n/**\n * Generates all of the search rows that are relevant for this entity.\n *\n * @param entityId - The uid of the entity\n * @param entity - The entity\n * @returns A list of entity search rows\n */\nexport function buildEntitySearch(\n entityId: string,\n entity: Entity,\n): DbSearchRow[] {\n // Visit the base structure recursively\n const raw = traverse(entity);\n\n // Start with some special keys that are always present because you want to\n // be able to easily search for null specifically\n raw.push({ key: 'metadata.name', value: entity.metadata.name });\n raw.push({ key: 'metadata.namespace', value: entity.metadata.namespace });\n raw.push({ key: 'metadata.uid', value: entity.metadata.uid });\n\n // Namespace not specified has the default value \"default\", so we want to\n // match on that as well\n if (!entity.metadata.namespace) {\n raw.push({ key: 'metadata.namespace', value: DEFAULT_NAMESPACE });\n }\n\n // Visit relations\n for (const relation of entity.relations ?? []) {\n raw.push({\n key: `relations.${relation.type}`,\n value: relation.targetRef,\n });\n }\n\n // This validates that there are no keys that vary only in casing, such\n // as `spec.foo` and `spec.Foo`.\n const keys = new Set(raw.map(r => r.key));\n const lowerKeys = new Set(raw.map(r => r.key.toLocaleLowerCase('en-US')));\n if (keys.size !== lowerKeys.size) {\n const difference = [];\n for (const key of keys) {\n const lower = key.toLocaleLowerCase('en-US');\n if (!lowerKeys.delete(lower)) {\n difference.push(lower);\n }\n }\n const badKeys = `'${difference.join(\"', '\")}'`;\n throw new InputError(\n `Entity has duplicate keys that vary only in casing, ${badKeys}`,\n );\n }\n\n return mapToRows(raw, entityId);\n}\n"],"names":[],"mappings":";;;AAuBA,MAAM,YAAe,GAAA;AAAA,EACnB,aAAA;AAAA,EACA,WAAA;AAAA,EACA,QAAA;AAAA,EACA,eAAA;AAAA,EACA,oBAAA;AAAA,EACA,cAAA;AAAA,EACA,eAAA;AACF,CAAA,CAAA;AAwCO,SAAS,SAAS,IAAqB,EAAA;AAC5C,EAAA,MAAM,SAAe,EAAC,CAAA;AAEtB,EAAS,SAAA,KAAA,CAAM,MAAc,OAAkB,EAAA;AAC7C,IAAI,IAAA,YAAA,CAAa,QAAS,CAAA,IAAI,CAAG,EAAA;AAC/B,MAAA,OAAA;AAAA,KACF;AAGA,IAAA,IACE,OAAY,KAAA,KAAA,CAAA,IACZ,OAAY,KAAA,IAAA,IACZ,CAAC,QAAA,EAAU,QAAU,EAAA,SAAS,CAAE,CAAA,QAAA,CAAS,OAAO,OAAO,CACvD,EAAA;AACA,MAAA,MAAA,CAAO,KAAK,EAAE,GAAA,EAAK,IAAM,EAAA,KAAA,EAAO,SAAS,CAAA,CAAA;AACzC,MAAA,OAAA;AAAA,KACF;AAGA,IAAI,IAAA,OAAO,YAAY,QAAU,EAAA;AAC/B,MAAA,OAAA;AAAA,KACF;AAGA,IAAI,IAAA,KAAA,CAAM,OAAQ,CAAA,OAAO,CAAG,EAAA;AAC1B,MAAA,KAAA,MAAW,QAAQ,OAAS,EAAA;AAc1B,QAAA,KAAA,CAAM,MAAM,IAAI,CAAA,CAAA;AAChB,QAAI,IAAA,OAAO,SAAS,QAAU,EAAA;AAC5B,UAAO,MAAA,CAAA,IAAA,CAAK,EAAE,GAAA,EAAK,CAAG,EAAA,IAAI,IAAI,IAAI,CAAA,CAAA,EAAI,KAAO,EAAA,IAAA,EAAM,CAAA,CAAA;AAAA,SACrD;AAAA,OACF;AACA,MAAA,OAAA;AAAA,KACF;AAGA,IAAA,KAAA,MAAW,CAAC,GAAK,EAAA,KAAK,KAAK,MAAO,CAAA,OAAA,CAAQ,OAAQ,CAAG,EAAA;AACnD,MAAA,KAAA,CAAM,OAAO,CAAG,EAAA,IAAI,IAAI,GAAG,CAAA,CAAA,GAAK,KAAK,KAAK,CAAA,CAAA;AAAA,KAC5C;AAAA,GACF;AAEA,EAAA,KAAA,CAAM,IAAI,IAAI,CAAA,CAAA;AAEd,EAAO,OAAA,MAAA,CAAA;AACT;;;;"}
@@ -0,0 +1,157 @@
1
+ import { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client';
2
+ import { stringifyEntityRef, parseEntityRef, DEFAULT_NAMESPACE } from '@backstage/catalog-model';
3
+ import { NotFoundError, NotImplementedError } from '@backstage/errors';
4
+ import { traverse } from '../plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.esm.js';
5
+
6
+ function buildEntitySearch(entity) {
7
+ const rows = traverse(entity);
8
+ if (entity.metadata?.name) {
9
+ rows.push({ key: "metadata.name", value: entity.metadata.name });
10
+ }
11
+ if (entity.metadata?.namespace) {
12
+ rows.push({ key: "metadata.namespace", value: entity.metadata.namespace });
13
+ }
14
+ if (entity.metadata?.uid) {
15
+ rows.push({ key: "metadata.uid", value: entity.metadata.uid });
16
+ }
17
+ if (!entity.metadata.namespace) {
18
+ rows.push({ key: "metadata.namespace", value: DEFAULT_NAMESPACE });
19
+ }
20
+ for (const relation of entity.relations ?? []) {
21
+ rows.push({
22
+ key: `relations.${relation.type}`,
23
+ value: relation.targetRef
24
+ });
25
+ }
26
+ return rows;
27
+ }
28
+ function createFilter(filterOrFilters) {
29
+ if (!filterOrFilters) {
30
+ return () => true;
31
+ }
32
+ const filters = [filterOrFilters].flat();
33
+ return (entity) => {
34
+ const rows = buildEntitySearch(entity);
35
+ return filters.some((filter) => {
36
+ for (const [key, expectedValue] of Object.entries(filter)) {
37
+ const searchValues = rows.filter((row) => row.key === key.toLocaleLowerCase("en-US")).map((row) => row.value?.toString().toLocaleLowerCase("en-US"));
38
+ if (searchValues.length === 0) {
39
+ return false;
40
+ }
41
+ if (expectedValue === CATALOG_FILTER_EXISTS) {
42
+ continue;
43
+ }
44
+ if (!searchValues?.includes(
45
+ String(expectedValue).toLocaleLowerCase("en-US")
46
+ )) {
47
+ return false;
48
+ }
49
+ }
50
+ return true;
51
+ });
52
+ };
53
+ }
54
+ class InMemoryCatalogClient {
55
+ #entities;
56
+ constructor(options) {
57
+ this.#entities = options?.entities?.slice() ?? [];
58
+ }
59
+ async getEntities(request) {
60
+ const filter = createFilter(request?.filter);
61
+ return { items: this.#entities.filter(filter) };
62
+ }
63
+ async getEntitiesByRefs(request) {
64
+ const filter = createFilter(request.filter);
65
+ const refMap = this.#createEntityRefMap();
66
+ return {
67
+ items: request.entityRefs.map((ref) => refMap.get(ref)).map((e) => e && filter(e) ? e : void 0)
68
+ };
69
+ }
70
+ async queryEntities(request) {
71
+ if (request && "cursor" in request) {
72
+ return { items: [], pageInfo: {}, totalItems: 0 };
73
+ }
74
+ const filter = createFilter(request?.filter);
75
+ const items = this.#entities.filter(filter);
76
+ return {
77
+ items,
78
+ pageInfo: {},
79
+ totalItems: items.length
80
+ };
81
+ }
82
+ async getEntityAncestors(request) {
83
+ const entity = this.#createEntityRefMap().get(request.entityRef);
84
+ if (!entity) {
85
+ throw new NotFoundError(`Entity with ref ${request.entityRef} not found`);
86
+ }
87
+ return {
88
+ items: [{ entity, parentEntityRefs: [] }],
89
+ rootEntityRef: request.entityRef
90
+ };
91
+ }
92
+ async getEntityByRef(entityRef) {
93
+ return this.#createEntityRefMap().get(
94
+ stringifyEntityRef(parseEntityRef(entityRef))
95
+ );
96
+ }
97
+ async removeEntityByUid(uid) {
98
+ const index = this.#entities.findIndex((e) => e.metadata.uid === uid);
99
+ if (index !== -1) {
100
+ this.#entities.splice(index, 1);
101
+ }
102
+ }
103
+ async refreshEntity(_entityRef) {
104
+ }
105
+ async getEntityFacets(request) {
106
+ const filter = createFilter(request.filter);
107
+ const filteredEntities = this.#entities.filter(filter);
108
+ const facets = Object.fromEntries(
109
+ request.facets.map((facet) => {
110
+ const facetValues = /* @__PURE__ */ new Map();
111
+ for (const entity of filteredEntities) {
112
+ const rows = buildEntitySearch(entity);
113
+ const value = rows.find(
114
+ (row) => row.key === facet.toLocaleLowerCase("en-US")
115
+ )?.value;
116
+ if (value) {
117
+ facetValues.set(
118
+ String(value),
119
+ (facetValues.get(String(value)) ?? 0) + 1
120
+ );
121
+ }
122
+ }
123
+ const counts = Array.from(facetValues.entries()).map(
124
+ ([value, count]) => ({ value, count })
125
+ );
126
+ return [facet, counts];
127
+ })
128
+ );
129
+ return {
130
+ facets
131
+ };
132
+ }
133
+ async getLocationById(_id) {
134
+ throw new NotImplementedError("Method not implemented.");
135
+ }
136
+ async getLocationByRef(_locationRef) {
137
+ throw new NotImplementedError("Method not implemented.");
138
+ }
139
+ async addLocation(_location) {
140
+ throw new NotImplementedError("Method not implemented.");
141
+ }
142
+ async removeLocationById(_id) {
143
+ throw new NotImplementedError("Method not implemented.");
144
+ }
145
+ async getLocationByEntity(_entityRef) {
146
+ throw new NotImplementedError("Method not implemented.");
147
+ }
148
+ async validateEntity(_entity, _locationRef) {
149
+ throw new NotImplementedError("Method not implemented.");
150
+ }
151
+ #createEntityRefMap() {
152
+ return new Map(this.#entities.map((e) => [stringifyEntityRef(e), e]));
153
+ }
154
+ }
155
+
156
+ export { InMemoryCatalogClient };
157
+ //# sourceMappingURL=InMemoryCatalogClient.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InMemoryCatalogClient.esm.js","sources":["../../src/testUtils/InMemoryCatalogClient.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AddLocationRequest,\n AddLocationResponse,\n CATALOG_FILTER_EXISTS,\n CatalogApi,\n EntityFilterQuery,\n GetEntitiesByRefsRequest,\n GetEntitiesByRefsResponse,\n GetEntitiesRequest,\n GetEntitiesResponse,\n GetEntityAncestorsRequest,\n GetEntityAncestorsResponse,\n GetEntityFacetsRequest,\n GetEntityFacetsResponse,\n Location,\n QueryEntitiesRequest,\n QueryEntitiesResponse,\n ValidateEntityResponse,\n} from '@backstage/catalog-client';\nimport {\n CompoundEntityRef,\n DEFAULT_NAMESPACE,\n Entity,\n parseEntityRef,\n stringifyEntityRef,\n} from '@backstage/catalog-model';\nimport { NotFoundError, NotImplementedError } from '@backstage/errors';\n// eslint-disable-next-line @backstage/no-relative-monorepo-imports\nimport { traverse } from '../../../../plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch';\n\nfunction buildEntitySearch(entity: Entity) {\n const rows = traverse(entity);\n\n if (entity.metadata?.name) {\n rows.push({ key: 'metadata.name', value: entity.metadata.name });\n }\n if (entity.metadata?.namespace) {\n rows.push({ key: 'metadata.namespace', value: entity.metadata.namespace });\n }\n if (entity.metadata?.uid) {\n rows.push({ key: 'metadata.uid', value: entity.metadata.uid });\n }\n\n if (!entity.metadata.namespace) {\n rows.push({ key: 'metadata.namespace', value: DEFAULT_NAMESPACE });\n }\n\n // Visit relations\n for (const relation of entity.relations ?? []) {\n rows.push({\n key: `relations.${relation.type}`,\n value: relation.targetRef,\n });\n }\n\n return rows;\n}\n\nfunction createFilter(\n filterOrFilters?: EntityFilterQuery,\n): (entity: Entity) => boolean {\n if (!filterOrFilters) {\n return () => true;\n }\n\n const filters = [filterOrFilters].flat();\n\n return entity => {\n const rows = buildEntitySearch(entity);\n\n return filters.some(filter => {\n for (const [key, expectedValue] of Object.entries(filter)) {\n const searchValues = rows\n .filter(row => row.key === key.toLocaleLowerCase('en-US'))\n .map(row => row.value?.toString().toLocaleLowerCase('en-US'));\n\n if (searchValues.length === 0) {\n return false;\n }\n if (expectedValue === CATALOG_FILTER_EXISTS) {\n continue;\n }\n if (\n !searchValues?.includes(\n String(expectedValue).toLocaleLowerCase('en-US'),\n )\n ) {\n return false;\n }\n }\n return true;\n });\n };\n}\n\n/**\n * Implements a VERY basic fake catalog client that stores entities in memory.\n * It has severely limited functionality, and is only useful under certain\n * circumstances in tests.\n *\n * @public\n */\nexport class InMemoryCatalogClient implements CatalogApi {\n #entities: Entity[];\n\n constructor(options?: { entities?: Entity[] }) {\n this.#entities = options?.entities?.slice() ?? [];\n }\n\n async getEntities(\n request?: GetEntitiesRequest,\n ): Promise<GetEntitiesResponse> {\n const filter = createFilter(request?.filter);\n return { items: this.#entities.filter(filter) };\n }\n\n async getEntitiesByRefs(\n request: GetEntitiesByRefsRequest,\n ): Promise<GetEntitiesByRefsResponse> {\n const filter = createFilter(request.filter);\n const refMap = this.#createEntityRefMap();\n return {\n items: request.entityRefs\n .map(ref => refMap.get(ref))\n .map(e => (e && filter(e) ? e : undefined)),\n };\n }\n\n async queryEntities(\n request?: QueryEntitiesRequest,\n ): Promise<QueryEntitiesResponse> {\n if (request && 'cursor' in request) {\n return { items: [], pageInfo: {}, totalItems: 0 };\n }\n const filter = createFilter(request?.filter);\n const items = this.#entities.filter(filter);\n // TODO(Rugvip): Pagination\n return {\n items,\n pageInfo: {},\n totalItems: items.length,\n };\n }\n\n async getEntityAncestors(\n request: GetEntityAncestorsRequest,\n ): Promise<GetEntityAncestorsResponse> {\n const entity = this.#createEntityRefMap().get(request.entityRef);\n if (!entity) {\n throw new NotFoundError(`Entity with ref ${request.entityRef} not found`);\n }\n return {\n items: [{ entity, parentEntityRefs: [] }],\n rootEntityRef: request.entityRef,\n };\n }\n\n async getEntityByRef(\n entityRef: string | CompoundEntityRef,\n ): Promise<Entity | undefined> {\n return this.#createEntityRefMap().get(\n stringifyEntityRef(parseEntityRef(entityRef)),\n );\n }\n\n async removeEntityByUid(uid: string): Promise<void> {\n const index = this.#entities.findIndex(e => e.metadata.uid === uid);\n if (index !== -1) {\n this.#entities.splice(index, 1);\n }\n }\n\n async refreshEntity(_entityRef: string): Promise<void> {}\n\n async getEntityFacets(\n request: GetEntityFacetsRequest,\n ): Promise<GetEntityFacetsResponse> {\n const filter = createFilter(request.filter);\n const filteredEntities = this.#entities.filter(filter);\n const facets = Object.fromEntries(\n request.facets.map(facet => {\n const facetValues = new Map<string, number>();\n for (const entity of filteredEntities) {\n const rows = buildEntitySearch(entity);\n const value = rows.find(\n row => row.key === facet.toLocaleLowerCase('en-US'),\n )?.value;\n if (value) {\n facetValues.set(\n String(value),\n (facetValues.get(String(value)) ?? 0) + 1,\n );\n }\n }\n const counts = Array.from(facetValues.entries()).map(\n ([value, count]) => ({ value, count }),\n );\n return [facet, counts];\n }),\n );\n return {\n facets,\n };\n }\n\n async getLocationById(_id: string): Promise<Location | undefined> {\n throw new NotImplementedError('Method not implemented.');\n }\n\n async getLocationByRef(_locationRef: string): Promise<Location | undefined> {\n throw new NotImplementedError('Method not implemented.');\n }\n\n async addLocation(\n _location: AddLocationRequest,\n ): Promise<AddLocationResponse> {\n throw new NotImplementedError('Method not implemented.');\n }\n\n async removeLocationById(_id: string): Promise<void> {\n throw new NotImplementedError('Method not implemented.');\n }\n\n async getLocationByEntity(\n _entityRef: string | CompoundEntityRef,\n ): Promise<Location | undefined> {\n throw new NotImplementedError('Method not implemented.');\n }\n\n async validateEntity(\n _entity: Entity,\n _locationRef: string,\n ): Promise<ValidateEntityResponse> {\n throw new NotImplementedError('Method not implemented.');\n }\n\n #createEntityRefMap() {\n return new Map(this.#entities.map(e => [stringifyEntityRef(e), e]));\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,197 @@
1
+ 'use strict';
2
+
3
+ var catalogClient = require('@backstage/catalog-client');
4
+ var catalogModel = require('@backstage/catalog-model');
5
+ var errors = require('@backstage/errors');
6
+
7
+ const SPECIAL_KEYS = [
8
+ "attachments",
9
+ "relations",
10
+ "status",
11
+ "metadata.name",
12
+ "metadata.namespace",
13
+ "metadata.uid",
14
+ "metadata.etag"
15
+ ];
16
+ function traverse(root) {
17
+ const output = [];
18
+ function visit(path, current) {
19
+ if (SPECIAL_KEYS.includes(path)) {
20
+ return;
21
+ }
22
+ if (current === void 0 || current === null || ["string", "number", "boolean"].includes(typeof current)) {
23
+ output.push({ key: path, value: current });
24
+ return;
25
+ }
26
+ if (typeof current !== "object") {
27
+ return;
28
+ }
29
+ if (Array.isArray(current)) {
30
+ for (const item of current) {
31
+ visit(path, item);
32
+ if (typeof item === "string") {
33
+ output.push({ key: `${path}.${item}`, value: true });
34
+ }
35
+ }
36
+ return;
37
+ }
38
+ for (const [key, value] of Object.entries(current)) {
39
+ visit(path ? `${path}.${key}` : key, value);
40
+ }
41
+ }
42
+ visit("", root);
43
+ return output;
44
+ }
45
+
46
+ function buildEntitySearch(entity) {
47
+ const rows = traverse(entity);
48
+ if (entity.metadata?.name) {
49
+ rows.push({ key: "metadata.name", value: entity.metadata.name });
50
+ }
51
+ if (entity.metadata?.namespace) {
52
+ rows.push({ key: "metadata.namespace", value: entity.metadata.namespace });
53
+ }
54
+ if (entity.metadata?.uid) {
55
+ rows.push({ key: "metadata.uid", value: entity.metadata.uid });
56
+ }
57
+ if (!entity.metadata.namespace) {
58
+ rows.push({ key: "metadata.namespace", value: catalogModel.DEFAULT_NAMESPACE });
59
+ }
60
+ for (const relation of entity.relations ?? []) {
61
+ rows.push({
62
+ key: `relations.${relation.type}`,
63
+ value: relation.targetRef
64
+ });
65
+ }
66
+ return rows;
67
+ }
68
+ function createFilter(filterOrFilters) {
69
+ if (!filterOrFilters) {
70
+ return () => true;
71
+ }
72
+ const filters = [filterOrFilters].flat();
73
+ return (entity) => {
74
+ const rows = buildEntitySearch(entity);
75
+ return filters.some((filter) => {
76
+ for (const [key, expectedValue] of Object.entries(filter)) {
77
+ const searchValues = rows.filter((row) => row.key === key.toLocaleLowerCase("en-US")).map((row) => row.value?.toString().toLocaleLowerCase("en-US"));
78
+ if (searchValues.length === 0) {
79
+ return false;
80
+ }
81
+ if (expectedValue === catalogClient.CATALOG_FILTER_EXISTS) {
82
+ continue;
83
+ }
84
+ if (!searchValues?.includes(
85
+ String(expectedValue).toLocaleLowerCase("en-US")
86
+ )) {
87
+ return false;
88
+ }
89
+ }
90
+ return true;
91
+ });
92
+ };
93
+ }
94
+ class InMemoryCatalogClient {
95
+ #entities;
96
+ constructor(options) {
97
+ this.#entities = options?.entities?.slice() ?? [];
98
+ }
99
+ async getEntities(request) {
100
+ const filter = createFilter(request?.filter);
101
+ return { items: this.#entities.filter(filter) };
102
+ }
103
+ async getEntitiesByRefs(request) {
104
+ const filter = createFilter(request.filter);
105
+ const refMap = this.#createEntityRefMap();
106
+ return {
107
+ items: request.entityRefs.map((ref) => refMap.get(ref)).map((e) => e && filter(e) ? e : void 0)
108
+ };
109
+ }
110
+ async queryEntities(request) {
111
+ if (request && "cursor" in request) {
112
+ return { items: [], pageInfo: {}, totalItems: 0 };
113
+ }
114
+ const filter = createFilter(request?.filter);
115
+ const items = this.#entities.filter(filter);
116
+ return {
117
+ items,
118
+ pageInfo: {},
119
+ totalItems: items.length
120
+ };
121
+ }
122
+ async getEntityAncestors(request) {
123
+ const entity = this.#createEntityRefMap().get(request.entityRef);
124
+ if (!entity) {
125
+ throw new errors.NotFoundError(`Entity with ref ${request.entityRef} not found`);
126
+ }
127
+ return {
128
+ items: [{ entity, parentEntityRefs: [] }],
129
+ rootEntityRef: request.entityRef
130
+ };
131
+ }
132
+ async getEntityByRef(entityRef) {
133
+ return this.#createEntityRefMap().get(
134
+ catalogModel.stringifyEntityRef(catalogModel.parseEntityRef(entityRef))
135
+ );
136
+ }
137
+ async removeEntityByUid(uid) {
138
+ const index = this.#entities.findIndex((e) => e.metadata.uid === uid);
139
+ if (index !== -1) {
140
+ this.#entities.splice(index, 1);
141
+ }
142
+ }
143
+ async refreshEntity(_entityRef) {
144
+ }
145
+ async getEntityFacets(request) {
146
+ const filter = createFilter(request.filter);
147
+ const filteredEntities = this.#entities.filter(filter);
148
+ const facets = Object.fromEntries(
149
+ request.facets.map((facet) => {
150
+ const facetValues = /* @__PURE__ */ new Map();
151
+ for (const entity of filteredEntities) {
152
+ const rows = buildEntitySearch(entity);
153
+ const value = rows.find(
154
+ (row) => row.key === facet.toLocaleLowerCase("en-US")
155
+ )?.value;
156
+ if (value) {
157
+ facetValues.set(
158
+ String(value),
159
+ (facetValues.get(String(value)) ?? 0) + 1
160
+ );
161
+ }
162
+ }
163
+ const counts = Array.from(facetValues.entries()).map(
164
+ ([value, count]) => ({ value, count })
165
+ );
166
+ return [facet, counts];
167
+ })
168
+ );
169
+ return {
170
+ facets
171
+ };
172
+ }
173
+ async getLocationById(_id) {
174
+ throw new errors.NotImplementedError("Method not implemented.");
175
+ }
176
+ async getLocationByRef(_locationRef) {
177
+ throw new errors.NotImplementedError("Method not implemented.");
178
+ }
179
+ async addLocation(_location) {
180
+ throw new errors.NotImplementedError("Method not implemented.");
181
+ }
182
+ async removeLocationById(_id) {
183
+ throw new errors.NotImplementedError("Method not implemented.");
184
+ }
185
+ async getLocationByEntity(_entityRef) {
186
+ throw new errors.NotImplementedError("Method not implemented.");
187
+ }
188
+ async validateEntity(_entity, _locationRef) {
189
+ throw new errors.NotImplementedError("Method not implemented.");
190
+ }
191
+ #createEntityRefMap() {
192
+ return new Map(this.#entities.map((e) => [catalogModel.stringifyEntityRef(e), e]));
193
+ }
194
+ }
195
+
196
+ exports.InMemoryCatalogClient = InMemoryCatalogClient;
197
+ //# sourceMappingURL=testUtils.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testUtils.cjs.js","sources":["../../../plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.ts","../src/testUtils/InMemoryCatalogClient.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { DEFAULT_NAMESPACE, Entity } from '@backstage/catalog-model';\nimport { InputError } from '@backstage/errors';\nimport { DbSearchRow } from '../../tables';\n\n// These are excluded in the generic loop, either because they do not make sense\n// to index, or because they are special-case always inserted whether they are\n// null or not\nconst SPECIAL_KEYS = [\n 'attachments',\n 'relations',\n 'status',\n 'metadata.name',\n 'metadata.namespace',\n 'metadata.uid',\n 'metadata.etag',\n];\n\n// The maximum length allowed for search values. These columns are indexed, and\n// database engines do not like to index on massive values. For example,\n// postgres will balk after 8191 byte line sizes.\nconst MAX_KEY_LENGTH = 200;\nconst MAX_VALUE_LENGTH = 200;\n\ntype Kv = {\n key: string;\n value: unknown;\n};\n\n// Helper for traversing through a nested structure and outputting a list of\n// path->value entries of the leaves.\n//\n// For example, this yaml structure\n//\n// a: 1\n// b:\n// c: null\n// e: [f, g]\n// h:\n// - i: 1\n// j: k\n// - i: 2\n// j: l\n//\n// will result in\n//\n// \"a\", 1\n// \"b.c\", null\n// \"b.e\": \"f\"\n// \"b.e.f\": true\n// \"b.e\": \"g\"\n// \"b.e.g\": true\n// \"h.i\": 1\n// \"h.j\": \"k\"\n// \"h.i\": 2\n// \"h.j\": \"l\"\nexport function traverse(root: unknown): Kv[] {\n const output: Kv[] = [];\n\n function visit(path: string, current: unknown) {\n if (SPECIAL_KEYS.includes(path)) {\n return;\n }\n\n // empty or scalar\n if (\n current === undefined ||\n current === null ||\n ['string', 'number', 'boolean'].includes(typeof current)\n ) {\n output.push({ key: path, value: current });\n return;\n }\n\n // unknown\n if (typeof current !== 'object') {\n return;\n }\n\n // array\n if (Array.isArray(current)) {\n for (const item of current) {\n // NOTE(freben): The reason that these are output in two different ways,\n // is to support use cases where you want to express that MORE than one\n // tag is present in a list. Since the EntityFilters structure is a\n // record, you can't have several entries of the same key. Therefore\n // you will have to match on\n //\n // { \"a.b\": [\"true\"], \"a.c\": [\"true\"] }\n //\n // rather than\n //\n // { \"a\": [\"b\", \"c\"] }\n //\n // because the latter means EITHER b or c has to be present.\n visit(path, item);\n if (typeof item === 'string') {\n output.push({ key: `${path}.${item}`, value: true });\n }\n }\n return;\n }\n\n // object\n for (const [key, value] of Object.entries(current!)) {\n visit(path ? `${path}.${key}` : key, value);\n }\n }\n\n visit('', root);\n\n return output;\n}\n\n// Translates a number of raw data rows to search table rows\nexport function mapToRows(input: Kv[], entityId: string): DbSearchRow[] {\n const result: DbSearchRow[] = [];\n\n for (const { key: rawKey, value: rawValue } of input) {\n const key = rawKey.toLocaleLowerCase('en-US');\n if (key.length > MAX_KEY_LENGTH) {\n continue;\n }\n if (rawValue === undefined || rawValue === null) {\n result.push({\n entity_id: entityId,\n key,\n original_value: null,\n value: null,\n });\n } else {\n const value = String(rawValue).toLocaleLowerCase('en-US');\n if (value.length <= MAX_VALUE_LENGTH) {\n result.push({\n entity_id: entityId,\n key,\n original_value: String(rawValue),\n value: value,\n });\n } else {\n result.push({\n entity_id: entityId,\n key,\n original_value: null,\n value: null,\n });\n }\n }\n }\n\n return result;\n}\n\n/**\n * Generates all of the search rows that are relevant for this entity.\n *\n * @param entityId - The uid of the entity\n * @param entity - The entity\n * @returns A list of entity search rows\n */\nexport function buildEntitySearch(\n entityId: string,\n entity: Entity,\n): DbSearchRow[] {\n // Visit the base structure recursively\n const raw = traverse(entity);\n\n // Start with some special keys that are always present because you want to\n // be able to easily search for null specifically\n raw.push({ key: 'metadata.name', value: entity.metadata.name });\n raw.push({ key: 'metadata.namespace', value: entity.metadata.namespace });\n raw.push({ key: 'metadata.uid', value: entity.metadata.uid });\n\n // Namespace not specified has the default value \"default\", so we want to\n // match on that as well\n if (!entity.metadata.namespace) {\n raw.push({ key: 'metadata.namespace', value: DEFAULT_NAMESPACE });\n }\n\n // Visit relations\n for (const relation of entity.relations ?? []) {\n raw.push({\n key: `relations.${relation.type}`,\n value: relation.targetRef,\n });\n }\n\n // This validates that there are no keys that vary only in casing, such\n // as `spec.foo` and `spec.Foo`.\n const keys = new Set(raw.map(r => r.key));\n const lowerKeys = new Set(raw.map(r => r.key.toLocaleLowerCase('en-US')));\n if (keys.size !== lowerKeys.size) {\n const difference = [];\n for (const key of keys) {\n const lower = key.toLocaleLowerCase('en-US');\n if (!lowerKeys.delete(lower)) {\n difference.push(lower);\n }\n }\n const badKeys = `'${difference.join(\"', '\")}'`;\n throw new InputError(\n `Entity has duplicate keys that vary only in casing, ${badKeys}`,\n );\n }\n\n return mapToRows(raw, entityId);\n}\n","/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AddLocationRequest,\n AddLocationResponse,\n CATALOG_FILTER_EXISTS,\n CatalogApi,\n EntityFilterQuery,\n GetEntitiesByRefsRequest,\n GetEntitiesByRefsResponse,\n GetEntitiesRequest,\n GetEntitiesResponse,\n GetEntityAncestorsRequest,\n GetEntityAncestorsResponse,\n GetEntityFacetsRequest,\n GetEntityFacetsResponse,\n Location,\n QueryEntitiesRequest,\n QueryEntitiesResponse,\n ValidateEntityResponse,\n} from '@backstage/catalog-client';\nimport {\n CompoundEntityRef,\n DEFAULT_NAMESPACE,\n Entity,\n parseEntityRef,\n stringifyEntityRef,\n} from '@backstage/catalog-model';\nimport { NotFoundError, NotImplementedError } from '@backstage/errors';\n// eslint-disable-next-line @backstage/no-relative-monorepo-imports\nimport { traverse } from '../../../../plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch';\n\nfunction buildEntitySearch(entity: Entity) {\n const rows = traverse(entity);\n\n if (entity.metadata?.name) {\n rows.push({ key: 'metadata.name', value: entity.metadata.name });\n }\n if (entity.metadata?.namespace) {\n rows.push({ key: 'metadata.namespace', value: entity.metadata.namespace });\n }\n if (entity.metadata?.uid) {\n rows.push({ key: 'metadata.uid', value: entity.metadata.uid });\n }\n\n if (!entity.metadata.namespace) {\n rows.push({ key: 'metadata.namespace', value: DEFAULT_NAMESPACE });\n }\n\n // Visit relations\n for (const relation of entity.relations ?? []) {\n rows.push({\n key: `relations.${relation.type}`,\n value: relation.targetRef,\n });\n }\n\n return rows;\n}\n\nfunction createFilter(\n filterOrFilters?: EntityFilterQuery,\n): (entity: Entity) => boolean {\n if (!filterOrFilters) {\n return () => true;\n }\n\n const filters = [filterOrFilters].flat();\n\n return entity => {\n const rows = buildEntitySearch(entity);\n\n return filters.some(filter => {\n for (const [key, expectedValue] of Object.entries(filter)) {\n const searchValues = rows\n .filter(row => row.key === key.toLocaleLowerCase('en-US'))\n .map(row => row.value?.toString().toLocaleLowerCase('en-US'));\n\n if (searchValues.length === 0) {\n return false;\n }\n if (expectedValue === CATALOG_FILTER_EXISTS) {\n continue;\n }\n if (\n !searchValues?.includes(\n String(expectedValue).toLocaleLowerCase('en-US'),\n )\n ) {\n return false;\n }\n }\n return true;\n });\n };\n}\n\n/**\n * Implements a VERY basic fake catalog client that stores entities in memory.\n * It has severely limited functionality, and is only useful under certain\n * circumstances in tests.\n *\n * @public\n */\nexport class InMemoryCatalogClient implements CatalogApi {\n #entities: Entity[];\n\n constructor(options?: { entities?: Entity[] }) {\n this.#entities = options?.entities?.slice() ?? [];\n }\n\n async getEntities(\n request?: GetEntitiesRequest,\n ): Promise<GetEntitiesResponse> {\n const filter = createFilter(request?.filter);\n return { items: this.#entities.filter(filter) };\n }\n\n async getEntitiesByRefs(\n request: GetEntitiesByRefsRequest,\n ): Promise<GetEntitiesByRefsResponse> {\n const filter = createFilter(request.filter);\n const refMap = this.#createEntityRefMap();\n return {\n items: request.entityRefs\n .map(ref => refMap.get(ref))\n .map(e => (e && filter(e) ? e : undefined)),\n };\n }\n\n async queryEntities(\n request?: QueryEntitiesRequest,\n ): Promise<QueryEntitiesResponse> {\n if (request && 'cursor' in request) {\n return { items: [], pageInfo: {}, totalItems: 0 };\n }\n const filter = createFilter(request?.filter);\n const items = this.#entities.filter(filter);\n // TODO(Rugvip): Pagination\n return {\n items,\n pageInfo: {},\n totalItems: items.length,\n };\n }\n\n async getEntityAncestors(\n request: GetEntityAncestorsRequest,\n ): Promise<GetEntityAncestorsResponse> {\n const entity = this.#createEntityRefMap().get(request.entityRef);\n if (!entity) {\n throw new NotFoundError(`Entity with ref ${request.entityRef} not found`);\n }\n return {\n items: [{ entity, parentEntityRefs: [] }],\n rootEntityRef: request.entityRef,\n };\n }\n\n async getEntityByRef(\n entityRef: string | CompoundEntityRef,\n ): Promise<Entity | undefined> {\n return this.#createEntityRefMap().get(\n stringifyEntityRef(parseEntityRef(entityRef)),\n );\n }\n\n async removeEntityByUid(uid: string): Promise<void> {\n const index = this.#entities.findIndex(e => e.metadata.uid === uid);\n if (index !== -1) {\n this.#entities.splice(index, 1);\n }\n }\n\n async refreshEntity(_entityRef: string): Promise<void> {}\n\n async getEntityFacets(\n request: GetEntityFacetsRequest,\n ): Promise<GetEntityFacetsResponse> {\n const filter = createFilter(request.filter);\n const filteredEntities = this.#entities.filter(filter);\n const facets = Object.fromEntries(\n request.facets.map(facet => {\n const facetValues = new Map<string, number>();\n for (const entity of filteredEntities) {\n const rows = buildEntitySearch(entity);\n const value = rows.find(\n row => row.key === facet.toLocaleLowerCase('en-US'),\n )?.value;\n if (value) {\n facetValues.set(\n String(value),\n (facetValues.get(String(value)) ?? 0) + 1,\n );\n }\n }\n const counts = Array.from(facetValues.entries()).map(\n ([value, count]) => ({ value, count }),\n );\n return [facet, counts];\n }),\n );\n return {\n facets,\n };\n }\n\n async getLocationById(_id: string): Promise<Location | undefined> {\n throw new NotImplementedError('Method not implemented.');\n }\n\n async getLocationByRef(_locationRef: string): Promise<Location | undefined> {\n throw new NotImplementedError('Method not implemented.');\n }\n\n async addLocation(\n _location: AddLocationRequest,\n ): Promise<AddLocationResponse> {\n throw new NotImplementedError('Method not implemented.');\n }\n\n async removeLocationById(_id: string): Promise<void> {\n throw new NotImplementedError('Method not implemented.');\n }\n\n async getLocationByEntity(\n _entityRef: string | CompoundEntityRef,\n ): Promise<Location | undefined> {\n throw new NotImplementedError('Method not implemented.');\n }\n\n async validateEntity(\n _entity: Entity,\n _locationRef: string,\n ): Promise<ValidateEntityResponse> {\n throw new NotImplementedError('Method not implemented.');\n }\n\n #createEntityRefMap() {\n return new Map(this.#entities.map(e => [stringifyEntityRef(e), e]));\n }\n}\n"],"names":["DEFAULT_NAMESPACE","CATALOG_FILTER_EXISTS","NotFoundError","stringifyEntityRef","parseEntityRef","NotImplementedError"],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import { CatalogApi, GetEntitiesRequest, GetEntitiesResponse, GetEntitiesByRefsRequest, GetEntitiesByRefsResponse, QueryEntitiesRequest, QueryEntitiesResponse, GetEntityAncestorsRequest, GetEntityAncestorsResponse, GetEntityFacetsRequest, GetEntityFacetsResponse, Location, AddLocationRequest, AddLocationResponse, ValidateEntityResponse } from '@backstage/catalog-client';
2
+ import { Entity, CompoundEntityRef } from '@backstage/catalog-model';
3
+
4
+ /**
5
+ * Implements a VERY basic fake catalog client that stores entities in memory.
6
+ * It has severely limited functionality, and is only useful under certain
7
+ * circumstances in tests.
8
+ *
9
+ * @public
10
+ */
11
+ declare class InMemoryCatalogClient implements CatalogApi {
12
+ #private;
13
+ constructor(options?: {
14
+ entities?: Entity[];
15
+ });
16
+ getEntities(request?: GetEntitiesRequest): Promise<GetEntitiesResponse>;
17
+ getEntitiesByRefs(request: GetEntitiesByRefsRequest): Promise<GetEntitiesByRefsResponse>;
18
+ queryEntities(request?: QueryEntitiesRequest): Promise<QueryEntitiesResponse>;
19
+ getEntityAncestors(request: GetEntityAncestorsRequest): Promise<GetEntityAncestorsResponse>;
20
+ getEntityByRef(entityRef: string | CompoundEntityRef): Promise<Entity | undefined>;
21
+ removeEntityByUid(uid: string): Promise<void>;
22
+ refreshEntity(_entityRef: string): Promise<void>;
23
+ getEntityFacets(request: GetEntityFacetsRequest): Promise<GetEntityFacetsResponse>;
24
+ getLocationById(_id: string): Promise<Location | undefined>;
25
+ getLocationByRef(_locationRef: string): Promise<Location | undefined>;
26
+ addLocation(_location: AddLocationRequest): Promise<AddLocationResponse>;
27
+ removeLocationById(_id: string): Promise<void>;
28
+ getLocationByEntity(_entityRef: string | CompoundEntityRef): Promise<Location | undefined>;
29
+ validateEntity(_entity: Entity, _locationRef: string): Promise<ValidateEntityResponse>;
30
+ }
31
+
32
+ export { InMemoryCatalogClient };
@@ -0,0 +1,2 @@
1
+ export { InMemoryCatalogClient } from './testUtils/InMemoryCatalogClient.esm.js';
2
+ //# sourceMappingURL=testUtils.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testUtils.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,15 +1,12 @@
1
1
  {
2
2
  "name": "@backstage/catalog-client",
3
- "version": "1.7.0-next.1",
3
+ "version": "1.7.0",
4
4
  "description": "An isomorphic client for the catalog backend",
5
5
  "backstage": {
6
6
  "role": "common-library"
7
7
  },
8
8
  "publishConfig": {
9
- "access": "public",
10
- "main": "dist/index.cjs.js",
11
- "module": "dist/index.esm.js",
12
- "types": "dist/index.d.ts"
9
+ "access": "public"
13
10
  },
14
11
  "keywords": [
15
12
  "backstage"
@@ -22,10 +19,26 @@
22
19
  },
23
20
  "license": "Apache-2.0",
24
21
  "sideEffects": false,
25
- "main": "dist/index.cjs.js",
26
- "types": "dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "import": "./dist/index.esm.js",
25
+ "require": "./dist/index.cjs.js",
26
+ "types": "./dist/index.d.ts",
27
+ "default": "./dist/index.cjs.js"
28
+ },
29
+ "./testUtils": {
30
+ "import": "./dist/testUtils.esm.js",
31
+ "require": "./dist/testUtils.cjs.js",
32
+ "types": "./dist/testUtils.d.ts",
33
+ "default": "./dist/testUtils.cjs.js"
34
+ },
35
+ "./package.json": "./package.json"
36
+ },
37
+ "main": "./dist/index.cjs.js",
38
+ "types": "./dist/index.d.ts",
27
39
  "files": [
28
- "dist"
40
+ "dist",
41
+ "testUtils"
29
42
  ],
30
43
  "scripts": {
31
44
  "build": "backstage-cli package build",
@@ -36,14 +49,14 @@
36
49
  "test": "backstage-cli package test"
37
50
  },
38
51
  "dependencies": {
39
- "@backstage/catalog-model": "^1.6.0",
52
+ "@backstage/catalog-model": "^1.7.0",
40
53
  "@backstage/errors": "^1.2.4",
41
54
  "cross-fetch": "^4.0.0",
42
55
  "uri-template": "^2.0.0"
43
56
  },
44
57
  "devDependencies": {
45
- "@backstage/cli": "^0.27.1-next.2",
58
+ "@backstage/cli": "^0.27.1",
46
59
  "msw": "^1.0.0"
47
60
  },
48
- "module": "dist/index.esm.js"
61
+ "module": "./dist/index.esm.js"
49
62
  }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "@backstage/catalog-client__testutils",
3
+ "version": "1.7.0",
4
+ "main": "../dist/testUtils.cjs.js",
5
+ "module": "../dist/testUtils.esm.js",
6
+ "types": "../dist/testUtils.d.ts"
7
+ }