@backstage/plugin-catalog-backend 3.5.0-next.1 → 3.5.0-next.2
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 +18 -0
- package/config.d.ts +9 -0
- package/dist/database/operations/stitcher/getDeferredStitchableEntities.cjs.js +6 -6
- package/dist/database/operations/stitcher/getDeferredStitchableEntities.cjs.js.map +1 -1
- package/dist/database/operations/stitcher/markDeferredStitchCompleted.cjs.js +1 -4
- package/dist/database/operations/stitcher/markDeferredStitchCompleted.cjs.js.map +1 -1
- package/dist/database/operations/stitcher/markForStitching.cjs.js +19 -8
- package/dist/database/operations/stitcher/markForStitching.cjs.js.map +1 -1
- package/dist/database/operations/stitcher/performStitching.cjs.js +12 -8
- package/dist/database/operations/stitcher/performStitching.cjs.js.map +1 -1
- package/dist/providers/DefaultLocationStore.cjs.js +14 -2
- package/dist/providers/DefaultLocationStore.cjs.js.map +1 -1
- package/dist/schema/openapi/generated/router.cjs.js +19 -0
- package/dist/schema/openapi/generated/router.cjs.js.map +1 -1
- package/dist/service/AuthorizedLocationService.cjs.js.map +1 -1
- package/dist/service/CatalogBuilder.cjs.js +4 -1
- package/dist/service/CatalogBuilder.cjs.js.map +1 -1
- package/dist/service/DefaultLocationService.cjs.js +6 -3
- package/dist/service/DefaultLocationService.cjs.js.map +1 -1
- package/dist/service/createRouter.cjs.js +2 -0
- package/dist/service/createRouter.cjs.js.map +1 -1
- package/dist/stitching/progressTracker.cjs.js +3 -1
- package/dist/stitching/progressTracker.cjs.js.map +1 -1
- package/migrations/20260215000000_move_stitch_queue.js +174 -0
- package/package.json +12 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @backstage/plugin-catalog-backend
|
|
2
2
|
|
|
3
|
+
## 3.5.0-next.2
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 5d95e8e: Add an `onConflict` option to location creation that can refresh an existing location instead of throwing a conflict error.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 7416e8b: Moved stitch queue concerns out of `refresh_state` and `final_entities` into a dedicated `stitch_queue` table with `entity_ref` as the primary key. The `stitch_ticket` is used for optimistic concurrency control. When a stitch completes successfully and the ticket hasn't changed, the corresponding row is deleted from the queue. The migration handles existing data and is fully reversible.
|
|
12
|
+
- Updated dependencies
|
|
13
|
+
- @backstage/backend-plugin-api@1.8.0-next.1
|
|
14
|
+
- @backstage/catalog-client@1.14.0-next.2
|
|
15
|
+
- @backstage/integration@2.0.0-next.2
|
|
16
|
+
- @backstage/backend-openapi-utils@0.6.7-next.1
|
|
17
|
+
- @backstage/plugin-catalog-node@2.1.0-next.2
|
|
18
|
+
- @backstage/plugin-events-node@0.4.20-next.1
|
|
19
|
+
- @backstage/plugin-permission-node@0.10.11-next.1
|
|
20
|
+
|
|
3
21
|
## 3.5.0-next.1
|
|
4
22
|
|
|
5
23
|
### Minor Changes
|
package/config.d.ts
CHANGED
|
@@ -191,6 +191,15 @@ export interface Config {
|
|
|
191
191
|
stitchTimeout?: HumanDuration | string;
|
|
192
192
|
};
|
|
193
193
|
|
|
194
|
+
/**
|
|
195
|
+
* The strategy to use when there is a conflict with a location being registered.
|
|
196
|
+
*
|
|
197
|
+
* The default value is "reject".
|
|
198
|
+
*
|
|
199
|
+
* The "refresh" strategy will refresh the existing location instead of throwing a conflict error.
|
|
200
|
+
*/
|
|
201
|
+
defaultLocationConflictStrategy?: 'refresh' | 'reject';
|
|
202
|
+
|
|
194
203
|
/**
|
|
195
204
|
* The interval at which the catalog should process its entities.
|
|
196
205
|
* @remarks
|
|
@@ -5,27 +5,27 @@ var conversion = require('../../conversion.cjs.js');
|
|
|
5
5
|
|
|
6
6
|
async function getDeferredStitchableEntities(options) {
|
|
7
7
|
const { knex, batchSize, stitchTimeout } = options;
|
|
8
|
-
let itemsQuery = knex("
|
|
8
|
+
let itemsQuery = knex("stitch_queue").select(
|
|
9
9
|
"entity_ref",
|
|
10
10
|
"next_stitch_at",
|
|
11
|
-
"
|
|
11
|
+
"stitch_ticket"
|
|
12
12
|
);
|
|
13
13
|
if (["mysql", "mysql2", "pg"].includes(knex.client.config.client)) {
|
|
14
14
|
itemsQuery = itemsQuery.forUpdate().skipLocked();
|
|
15
15
|
}
|
|
16
|
-
const items = await itemsQuery.
|
|
16
|
+
const items = await itemsQuery.where("next_stitch_at", "<=", knex.fn.now()).orderBy("next_stitch_at", "asc").limit(batchSize);
|
|
17
17
|
if (!items.length) {
|
|
18
18
|
return [];
|
|
19
19
|
}
|
|
20
|
-
await knex("
|
|
20
|
+
await knex("stitch_queue").whereIn(
|
|
21
21
|
"entity_ref",
|
|
22
22
|
items.map((i) => i.entity_ref)
|
|
23
|
-
).
|
|
23
|
+
).update({
|
|
24
24
|
next_stitch_at: nowPlus(knex, stitchTimeout)
|
|
25
25
|
});
|
|
26
26
|
return items.map((i) => ({
|
|
27
27
|
entityRef: i.entity_ref,
|
|
28
|
-
stitchTicket: i.
|
|
28
|
+
stitchTicket: i.stitch_ticket,
|
|
29
29
|
stitchRequestedAt: conversion.timestampToDateTime(i.next_stitch_at)
|
|
30
30
|
}));
|
|
31
31
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"getDeferredStitchableEntities.cjs.js","sources":["../../../../src/database/operations/stitcher/getDeferredStitchableEntities.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { durationToMilliseconds, HumanDuration } from '@backstage/types';\nimport { Knex } from 'knex';\nimport { DateTime } from 'luxon';\nimport { timestampToDateTime } from '../../conversion';\nimport {
|
|
1
|
+
{"version":3,"file":"getDeferredStitchableEntities.cjs.js","sources":["../../../../src/database/operations/stitcher/getDeferredStitchableEntities.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { durationToMilliseconds, HumanDuration } from '@backstage/types';\nimport { Knex } from 'knex';\nimport { DateTime } from 'luxon';\nimport { timestampToDateTime } from '../../conversion';\nimport { DbStitchQueueRow } from '../../tables';\n\n// TODO(freben): There is no retry counter or similar. If items start\n// perpetually crashing during stitching, they'll just get silently retried over\n// and over again, for better or worse. This will be visible in metrics though.\n\n/**\n * Finds entities that are marked for deferred stitching.\n *\n * @remarks\n *\n * This assumes that the stitching strategy is set to deferred.\n *\n * They are expected to already have the stitch_ticket set (by\n * markForStitching) so that their tickets can be returned with each item.\n *\n * All returned items have their next_stitch_at updated to be moved forward by\n * the given timeout duration. This has the effect that they will be picked up\n * for stitching again in the future, if it hasn't completed by that point for\n * some reason (restarts, crashes, etc).\n */\nexport async function getDeferredStitchableEntities(options: {\n knex: Knex | Knex.Transaction;\n batchSize: number;\n stitchTimeout: HumanDuration;\n}): Promise<\n Array<{\n entityRef: string;\n stitchTicket: string;\n stitchRequestedAt: DateTime; // the time BEFORE moving it forward by the timeout\n }>\n> {\n const { knex, batchSize, stitchTimeout } = options;\n\n let itemsQuery = knex<DbStitchQueueRow>('stitch_queue').select(\n 'entity_ref',\n 'next_stitch_at',\n 'stitch_ticket',\n );\n\n // This avoids duplication of work because of race conditions and is\n // also fast because locked rows are ignored rather than blocking.\n // It's only available in MySQL and PostgreSQL\n if (['mysql', 'mysql2', 'pg'].includes(knex.client.config.client)) {\n itemsQuery = itemsQuery.forUpdate().skipLocked();\n }\n\n const items = await itemsQuery\n .where('next_stitch_at', '<=', knex.fn.now())\n .orderBy('next_stitch_at', 'asc')\n .limit(batchSize);\n\n if (!items.length) {\n return [];\n }\n\n await knex<DbStitchQueueRow>('stitch_queue')\n .whereIn(\n 'entity_ref',\n items.map(i => i.entity_ref),\n )\n .update({\n next_stitch_at: nowPlus(knex, stitchTimeout),\n });\n\n return items.map(i => ({\n entityRef: i.entity_ref,\n stitchTicket: i.stitch_ticket,\n stitchRequestedAt: timestampToDateTime(i.next_stitch_at),\n }));\n}\n\nfunction nowPlus(knex: Knex, duration: HumanDuration): Knex.Raw {\n const seconds = durationToMilliseconds(duration) / 1000;\n if (knex.client.config.client.includes('sqlite3')) {\n return knex.raw(`datetime('now', ?)`, [`${seconds} seconds`]);\n } else if (knex.client.config.client.includes('mysql')) {\n return knex.raw(`now() + interval ${seconds} second`);\n }\n return knex.raw(`now() + interval '${seconds} seconds'`);\n}\n"],"names":["timestampToDateTime","durationToMilliseconds"],"mappings":";;;;;AAyCA,eAAsB,8BAA8B,OAAA,EAUlD;AACA,EAAA,MAAM,EAAE,IAAA,EAAM,SAAA,EAAW,aAAA,EAAc,GAAI,OAAA;AAE3C,EAAA,IAAI,UAAA,GAAa,IAAA,CAAuB,cAAc,CAAA,CAAE,MAAA;AAAA,IACtD,YAAA;AAAA,IACA,gBAAA;AAAA,IACA;AAAA,GACF;AAKA,EAAA,IAAI,CAAC,OAAA,EAAS,QAAA,EAAU,IAAI,CAAA,CAAE,SAAS,IAAA,CAAK,MAAA,CAAO,MAAA,CAAO,MAAM,CAAA,EAAG;AACjE,IAAA,UAAA,GAAa,UAAA,CAAW,SAAA,EAAU,CAAE,UAAA,EAAW;AAAA,EACjD;AAEA,EAAA,MAAM,QAAQ,MAAM,UAAA,CACjB,KAAA,CAAM,gBAAA,EAAkB,MAAM,IAAA,CAAK,EAAA,CAAG,GAAA,EAAK,EAC3C,OAAA,CAAQ,gBAAA,EAAkB,KAAK,CAAA,CAC/B,MAAM,SAAS,CAAA;AAElB,EAAA,IAAI,CAAC,MAAM,MAAA,EAAQ;AACjB,IAAA,OAAO,EAAC;AAAA,EACV;AAEA,EAAA,MAAM,IAAA,CAAuB,cAAc,CAAA,CACxC,OAAA;AAAA,IACC,YAAA;AAAA,IACA,KAAA,CAAM,GAAA,CAAI,CAAA,CAAA,KAAK,CAAA,CAAE,UAAU;AAAA,IAE5B,MAAA,CAAO;AAAA,IACN,cAAA,EAAgB,OAAA,CAAQ,IAAA,EAAM,aAAa;AAAA,GAC5C,CAAA;AAEH,EAAA,OAAO,KAAA,CAAM,IAAI,CAAA,CAAA,MAAM;AAAA,IACrB,WAAW,CAAA,CAAE,UAAA;AAAA,IACb,cAAc,CAAA,CAAE,aAAA;AAAA,IAChB,iBAAA,EAAmBA,8BAAA,CAAoB,CAAA,CAAE,cAAc;AAAA,GACzD,CAAE,CAAA;AACJ;AAEA,SAAS,OAAA,CAAQ,MAAY,QAAA,EAAmC;AAC9D,EAAA,MAAM,OAAA,GAAUC,4BAAA,CAAuB,QAAQ,CAAA,GAAI,GAAA;AACnD,EAAA,IAAI,KAAK,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,QAAA,CAAS,SAAS,CAAA,EAAG;AACjD,IAAA,OAAO,KAAK,GAAA,CAAI,CAAA,kBAAA,CAAA,EAAsB,CAAC,CAAA,EAAG,OAAO,UAAU,CAAC,CAAA;AAAA,EAC9D,WAAW,IAAA,CAAK,MAAA,CAAO,OAAO,MAAA,CAAO,QAAA,CAAS,OAAO,CAAA,EAAG;AACtD,IAAA,OAAO,IAAA,CAAK,GAAA,CAAI,CAAA,iBAAA,EAAoB,OAAO,CAAA,OAAA,CAAS,CAAA;AAAA,EACtD;AACA,EAAA,OAAO,IAAA,CAAK,GAAA,CAAI,CAAA,kBAAA,EAAqB,OAAO,CAAA,SAAA,CAAW,CAAA;AACzD;;;;"}
|
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
async function markDeferredStitchCompleted(option) {
|
|
4
4
|
const { knex, entityRef, stitchTicket } = option;
|
|
5
|
-
await knex("
|
|
6
|
-
next_stitch_at: null,
|
|
7
|
-
next_stitch_ticket: null
|
|
8
|
-
}).where("entity_ref", "=", entityRef).andWhere("next_stitch_ticket", "=", stitchTicket);
|
|
5
|
+
await knex("stitch_queue").where("entity_ref", "=", entityRef).andWhere("stitch_ticket", "=", stitchTicket).delete();
|
|
9
6
|
}
|
|
10
7
|
|
|
11
8
|
exports.markDeferredStitchCompleted = markDeferredStitchCompleted;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markDeferredStitchCompleted.cjs.js","sources":["../../../../src/database/operations/stitcher/markDeferredStitchCompleted.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { Knex } from 'knex';\nimport {
|
|
1
|
+
{"version":3,"file":"markDeferredStitchCompleted.cjs.js","sources":["../../../../src/database/operations/stitcher/markDeferredStitchCompleted.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { Knex } from 'knex';\nimport { DbStitchQueueRow } from '../../tables';\n\n/**\n * Marks a single entity as having been stitched.\n *\n * @remarks\n *\n * This assumes that the stitching strategy is set to deferred.\n *\n * The row is only deleted from stitch_queue if the ticket hasn't changed. If\n * it has, it means that a new stitch request has been made, and the entity\n * should be stitched once more some time in the future - or is indeed already\n * being stitched concurrently with ourselves.\n */\nexport async function markDeferredStitchCompleted(option: {\n knex: Knex | Knex.Transaction;\n entityRef: string;\n stitchTicket: string;\n}): Promise<void> {\n const { knex, entityRef, stitchTicket } = option;\n\n await knex<DbStitchQueueRow>('stitch_queue')\n .where('entity_ref', '=', entityRef)\n .andWhere('stitch_ticket', '=', stitchTicket)\n .delete();\n}\n"],"names":[],"mappings":";;AA+BA,eAAsB,4BAA4B,MAAA,EAIhC;AAChB,EAAA,MAAM,EAAE,IAAA,EAAM,SAAA,EAAW,YAAA,EAAa,GAAI,MAAA;AAE1C,EAAA,MAAM,IAAA,CAAuB,cAAc,CAAA,CACxC,KAAA,CAAM,YAAA,EAAc,GAAA,EAAK,SAAS,CAAA,CAClC,QAAA,CAAS,eAAA,EAAiB,GAAA,EAAK,YAAY,EAC3C,MAAA,EAAO;AACZ;;;;"}
|
|
@@ -41,18 +41,29 @@ async function markForStitching(options) {
|
|
|
41
41
|
const ticket = uuid.v4();
|
|
42
42
|
for (const chunk of entityRefs) {
|
|
43
43
|
await util.retryOnDeadlock(async () => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
if (chunk.length > 0) {
|
|
45
|
+
await knex("stitch_queue").insert(
|
|
46
|
+
chunk.map((ref) => ({
|
|
47
|
+
entity_ref: ref,
|
|
48
|
+
stitch_ticket: ticket,
|
|
49
|
+
next_stitch_at: knex.fn.now()
|
|
50
|
+
}))
|
|
51
|
+
).onConflict("entity_ref").merge(["next_stitch_at", "stitch_ticket"]);
|
|
52
|
+
}
|
|
48
53
|
}, knex);
|
|
49
54
|
}
|
|
50
55
|
for (const chunk of entityIds) {
|
|
51
56
|
await util.retryOnDeadlock(async () => {
|
|
52
|
-
await knex("refresh_state").
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
const refreshStateRows = await knex("refresh_state").select("entity_ref").whereIn("entity_id", chunk);
|
|
58
|
+
if (refreshStateRows.length > 0) {
|
|
59
|
+
await knex("stitch_queue").insert(
|
|
60
|
+
refreshStateRows.map((row) => ({
|
|
61
|
+
entity_ref: row.entity_ref,
|
|
62
|
+
stitch_ticket: ticket,
|
|
63
|
+
next_stitch_at: knex.fn.now()
|
|
64
|
+
}))
|
|
65
|
+
).onConflict("entity_ref").merge(["next_stitch_at", "stitch_ticket"]);
|
|
66
|
+
}
|
|
56
67
|
}, knex);
|
|
57
68
|
}
|
|
58
69
|
} else {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markForStitching.cjs.js","sources":["../../../../src/database/operations/stitcher/markForStitching.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { Knex } from 'knex';\nimport splitToChunks from 'lodash/chunk';\nimport { v4 as uuid } from 'uuid';\nimport { StitchingStrategy } from '../../../stitching/types';\nimport {
|
|
1
|
+
{"version":3,"file":"markForStitching.cjs.js","sources":["../../../../src/database/operations/stitcher/markForStitching.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { Knex } from 'knex';\nimport splitToChunks from 'lodash/chunk';\nimport { v4 as uuid } from 'uuid';\nimport { StitchingStrategy } from '../../../stitching/types';\nimport {\n DbFinalEntitiesRow,\n DbRefreshStateRow,\n DbStitchQueueRow,\n} from '../../tables';\nimport { retryOnDeadlock } from '../../util';\n\nconst UPDATE_CHUNK_SIZE = 100; // Smaller chunks reduce contention\n\n/**\n * Marks a number of entities for stitching some time in the near\n * future.\n *\n * @remarks\n */\nexport async function markForStitching(options: {\n knex: Knex | Knex.Transaction;\n strategy: StitchingStrategy;\n entityRefs?: Iterable<string>;\n entityIds?: Iterable<string>;\n}): Promise<void> {\n const entityRefs = sortSplit(options.entityRefs);\n const entityIds = sortSplit(options.entityIds);\n const knex = options.knex;\n const mode = options.strategy.mode;\n\n if (mode === 'immediate') {\n for (const chunk of entityRefs) {\n await knex\n .table<DbFinalEntitiesRow>('final_entities')\n .update({\n hash: 'force-stitching',\n })\n .whereIn('entity_ref', chunk);\n await retryOnDeadlock(async () => {\n await knex\n .table<DbRefreshStateRow>('refresh_state')\n .update({\n result_hash: 'force-stitching',\n next_update_at: knex.fn.now(),\n })\n .whereIn('entity_ref', chunk);\n }, knex);\n }\n\n for (const chunk of entityIds) {\n await knex\n .table<DbFinalEntitiesRow>('final_entities')\n .update({\n hash: 'force-stitching',\n })\n .whereIn('entity_id', chunk);\n await retryOnDeadlock(async () => {\n await knex\n .table<DbRefreshStateRow>('refresh_state')\n .update({\n result_hash: 'force-stitching',\n next_update_at: knex.fn.now(),\n })\n .whereIn('entity_id', chunk);\n }, knex);\n }\n } else if (mode === 'deferred') {\n // It's OK that this is shared across stitch_queue rows; it just needs to\n // be uniquely generated for every new stitch request.\n const ticket = uuid();\n\n for (const chunk of entityRefs) {\n await retryOnDeadlock(async () => {\n if (chunk.length > 0) {\n await knex<DbStitchQueueRow>('stitch_queue')\n .insert(\n chunk.map(ref => ({\n entity_ref: ref,\n stitch_ticket: ticket,\n next_stitch_at: knex.fn.now(),\n })),\n )\n .onConflict('entity_ref')\n .merge(['next_stitch_at', 'stitch_ticket']);\n }\n }, knex);\n }\n\n for (const chunk of entityIds) {\n await retryOnDeadlock(async () => {\n // Look up entity_refs from refresh_state by entity_id\n const refreshStateRows = await knex<DbRefreshStateRow>('refresh_state')\n .select('entity_ref')\n .whereIn('entity_id', chunk);\n\n if (refreshStateRows.length > 0) {\n await knex<DbStitchQueueRow>('stitch_queue')\n .insert(\n refreshStateRows.map(row => ({\n entity_ref: row.entity_ref,\n stitch_ticket: ticket,\n next_stitch_at: knex.fn.now(),\n })),\n )\n .onConflict('entity_ref')\n .merge(['next_stitch_at', 'stitch_ticket']);\n }\n }, knex);\n }\n } else {\n throw new Error(`Unknown stitching strategy mode ${mode}`);\n }\n}\n\nfunction sortSplit(input: Iterable<string> | undefined): string[][] {\n if (!input) {\n return [];\n }\n const array = Array.isArray(input) ? input.slice() : [...input];\n array.sort();\n return splitToChunks(array, UPDATE_CHUNK_SIZE);\n}\n"],"names":["retryOnDeadlock","uuid","splitToChunks"],"mappings":";;;;;;;;;;AA2BA,MAAM,iBAAA,GAAoB,GAAA;AAQ1B,eAAsB,iBAAiB,OAAA,EAKrB;AAChB,EAAA,MAAM,UAAA,GAAa,SAAA,CAAU,OAAA,CAAQ,UAAU,CAAA;AAC/C,EAAA,MAAM,SAAA,GAAY,SAAA,CAAU,OAAA,CAAQ,SAAS,CAAA;AAC7C,EAAA,MAAM,OAAO,OAAA,CAAQ,IAAA;AACrB,EAAA,MAAM,IAAA,GAAO,QAAQ,QAAA,CAAS,IAAA;AAE9B,EAAA,IAAI,SAAS,WAAA,EAAa;AACxB,IAAA,KAAA,MAAW,SAAS,UAAA,EAAY;AAC9B,MAAA,MAAM,IAAA,CACH,KAAA,CAA0B,gBAAgB,CAAA,CAC1C,MAAA,CAAO;AAAA,QACN,IAAA,EAAM;AAAA,OACP,CAAA,CACA,OAAA,CAAQ,YAAA,EAAc,KAAK,CAAA;AAC9B,MAAA,MAAMA,qBAAgB,YAAY;AAChC,QAAA,MAAM,IAAA,CACH,KAAA,CAAyB,eAAe,CAAA,CACxC,MAAA,CAAO;AAAA,UACN,WAAA,EAAa,iBAAA;AAAA,UACb,cAAA,EAAgB,IAAA,CAAK,EAAA,CAAG,GAAA;AAAI,SAC7B,CAAA,CACA,OAAA,CAAQ,YAAA,EAAc,KAAK,CAAA;AAAA,MAChC,GAAG,IAAI,CAAA;AAAA,IACT;AAEA,IAAA,KAAA,MAAW,SAAS,SAAA,EAAW;AAC7B,MAAA,MAAM,IAAA,CACH,KAAA,CAA0B,gBAAgB,CAAA,CAC1C,MAAA,CAAO;AAAA,QACN,IAAA,EAAM;AAAA,OACP,CAAA,CACA,OAAA,CAAQ,WAAA,EAAa,KAAK,CAAA;AAC7B,MAAA,MAAMA,qBAAgB,YAAY;AAChC,QAAA,MAAM,IAAA,CACH,KAAA,CAAyB,eAAe,CAAA,CACxC,MAAA,CAAO;AAAA,UACN,WAAA,EAAa,iBAAA;AAAA,UACb,cAAA,EAAgB,IAAA,CAAK,EAAA,CAAG,GAAA;AAAI,SAC7B,CAAA,CACA,OAAA,CAAQ,WAAA,EAAa,KAAK,CAAA;AAAA,MAC/B,GAAG,IAAI,CAAA;AAAA,IACT;AAAA,EACF,CAAA,MAAA,IAAW,SAAS,UAAA,EAAY;AAG9B,IAAA,MAAM,SAASC,OAAA,EAAK;AAEpB,IAAA,KAAA,MAAW,SAAS,UAAA,EAAY;AAC9B,MAAA,MAAMD,qBAAgB,YAAY;AAChC,QAAA,IAAI,KAAA,CAAM,SAAS,CAAA,EAAG;AACpB,UAAA,MAAM,IAAA,CAAuB,cAAc,CAAA,CACxC,MAAA;AAAA,YACC,KAAA,CAAM,IAAI,CAAA,GAAA,MAAQ;AAAA,cAChB,UAAA,EAAY,GAAA;AAAA,cACZ,aAAA,EAAe,MAAA;AAAA,cACf,cAAA,EAAgB,IAAA,CAAK,EAAA,CAAG,GAAA;AAAI,aAC9B,CAAE;AAAA,WACJ,CACC,WAAW,YAAY,CAAA,CACvB,MAAM,CAAC,gBAAA,EAAkB,eAAe,CAAC,CAAA;AAAA,QAC9C;AAAA,MACF,GAAG,IAAI,CAAA;AAAA,IACT;AAEA,IAAA,KAAA,MAAW,SAAS,SAAA,EAAW;AAC7B,MAAA,MAAMA,qBAAgB,YAAY;AAEhC,QAAA,MAAM,gBAAA,GAAmB,MAAM,IAAA,CAAwB,eAAe,CAAA,CACnE,OAAO,YAAY,CAAA,CACnB,OAAA,CAAQ,WAAA,EAAa,KAAK,CAAA;AAE7B,QAAA,IAAI,gBAAA,CAAiB,SAAS,CAAA,EAAG;AAC/B,UAAA,MAAM,IAAA,CAAuB,cAAc,CAAA,CACxC,MAAA;AAAA,YACC,gBAAA,CAAiB,IAAI,CAAA,GAAA,MAAQ;AAAA,cAC3B,YAAY,GAAA,CAAI,UAAA;AAAA,cAChB,aAAA,EAAe,MAAA;AAAA,cACf,cAAA,EAAgB,IAAA,CAAK,EAAA,CAAG,GAAA;AAAI,aAC9B,CAAE;AAAA,WACJ,CACC,WAAW,YAAY,CAAA,CACvB,MAAM,CAAC,gBAAA,EAAkB,eAAe,CAAC,CAAA;AAAA,QAC9C;AAAA,MACF,GAAG,IAAI,CAAA;AAAA,IACT;AAAA,EACF,CAAA,MAAO;AACL,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,gCAAA,EAAmC,IAAI,CAAA,CAAE,CAAA;AAAA,EAC3D;AACF;AAEA,SAAS,UAAU,KAAA,EAAiD;AAClE,EAAA,IAAI,CAAC,KAAA,EAAO;AACV,IAAA,OAAO,EAAC;AAAA,EACV;AACA,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,GAAI,MAAM,KAAA,EAAM,GAAI,CAAC,GAAG,KAAK,CAAA;AAC9D,EAAA,KAAA,CAAM,IAAA,EAAK;AACX,EAAA,OAAOE,8BAAA,CAAc,OAAO,iBAAiB,CAAA;AAC/C;;;;"}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
var catalogClient = require('@backstage/catalog-client');
|
|
4
4
|
var catalogModel = require('@backstage/catalog-model');
|
|
5
|
-
var uuid = require('uuid');
|
|
6
5
|
var buildEntitySearch = require('./buildEntitySearch.cjs.js');
|
|
7
6
|
var markDeferredStitchCompleted = require('./markDeferredStitchCompleted.cjs.js');
|
|
8
7
|
var util = require('./util.cjs.js');
|
|
@@ -14,7 +13,7 @@ const scriptProtocolPattern = (
|
|
|
14
13
|
);
|
|
15
14
|
async function performStitching(options) {
|
|
16
15
|
const { knex, logger, entityRef } = options;
|
|
17
|
-
const stitchTicket = options.stitchTicket
|
|
16
|
+
const stitchTicket = options.stitchTicket;
|
|
18
17
|
let removeFromStitchQueueOnCompletion = options.strategy.mode === "deferred";
|
|
19
18
|
try {
|
|
20
19
|
const entityResult = await knex("refresh_state").where({ entity_ref: entityRef }).limit(1).select("entity_id");
|
|
@@ -25,9 +24,8 @@ async function performStitching(options) {
|
|
|
25
24
|
await knex("final_entities").insert({
|
|
26
25
|
entity_id: entityResult[0].entity_id,
|
|
27
26
|
hash: "",
|
|
28
|
-
entity_ref: entityRef
|
|
29
|
-
|
|
30
|
-
}).onConflict("entity_id").merge(["stitch_ticket"]);
|
|
27
|
+
entity_ref: entityRef
|
|
28
|
+
}).onConflict("entity_id").ignore();
|
|
31
29
|
} catch (error) {
|
|
32
30
|
if (backendPluginApi.isDatabaseConflictError(error)) {
|
|
33
31
|
logger.debug(`Skipping stitching of ${entityRef}, conflict`, error);
|
|
@@ -121,11 +119,17 @@ async function performStitching(options) {
|
|
|
121
119
|
entity.metadata.etag = hash;
|
|
122
120
|
}
|
|
123
121
|
const searchEntries = buildEntitySearch.buildEntitySearch(entityId, entity);
|
|
124
|
-
|
|
122
|
+
let updateQuery = knex("final_entities").update({
|
|
125
123
|
final_entity: JSON.stringify(entity),
|
|
126
124
|
hash,
|
|
127
125
|
last_updated_at: knex.fn.now()
|
|
128
|
-
}).where("entity_id", entityId)
|
|
126
|
+
}).where("entity_id", entityId);
|
|
127
|
+
if (options.strategy.mode === "deferred" && stitchTicket) {
|
|
128
|
+
updateQuery = updateQuery.whereExists(
|
|
129
|
+
knex("stitch_queue").where("stitch_queue.entity_ref", entityRef).where("stitch_queue.stitch_ticket", stitchTicket).select(knex.raw("1"))
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
const amountOfRowsChanged = await updateQuery;
|
|
129
133
|
if (amountOfRowsChanged === 0) {
|
|
130
134
|
logger.debug(`Entity ${entityRef} is already stitched, skipping write.`);
|
|
131
135
|
return "abandoned";
|
|
@@ -139,7 +143,7 @@ async function performStitching(options) {
|
|
|
139
143
|
removeFromStitchQueueOnCompletion = false;
|
|
140
144
|
throw error;
|
|
141
145
|
} finally {
|
|
142
|
-
if (removeFromStitchQueueOnCompletion) {
|
|
146
|
+
if (removeFromStitchQueueOnCompletion && stitchTicket) {
|
|
143
147
|
await markDeferredStitchCompleted.markDeferredStitchCompleted({
|
|
144
148
|
knex,
|
|
145
149
|
entityRef,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"performStitching.cjs.js","sources":["../../../../src/database/operations/stitcher/performStitching.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from '@backstage/catalog-client';\nimport {\n ANNOTATION_EDIT_URL,\n ANNOTATION_VIEW_URL,\n EntityRelation,\n} from '@backstage/catalog-model';\nimport { AlphaEntity, EntityStatusItem } from '@backstage/catalog-model/alpha';\nimport { SerializedError } from '@backstage/errors';\nimport { Knex } from 'knex';\nimport { v4 as uuid } from 'uuid';\nimport { StitchingStrategy } from '../../../stitching/types';\nimport {\n DbFinalEntitiesRow,\n DbRefreshStateRow,\n DbSearchRow,\n} from '../../tables';\nimport { buildEntitySearch } from './buildEntitySearch';\nimport { markDeferredStitchCompleted } from './markDeferredStitchCompleted';\nimport { BATCH_SIZE, generateStableHash } from './util';\nimport {\n LoggerService,\n isDatabaseConflictError,\n} from '@backstage/backend-plugin-api';\n\n// See https://github.com/facebook/react/blob/f0cf832e1d0c8544c36aa8b310960885a11a847c/packages/react-dom-bindings/src/shared/sanitizeURL.js\nconst scriptProtocolPattern =\n // eslint-disable-next-line no-control-regex\n /^[\\u0000-\\u001F ]*j[\\r\\n\\t]*a[\\r\\n\\t]*v[\\r\\n\\t]*a[\\r\\n\\t]*s[\\r\\n\\t]*c[\\r\\n\\t]*r[\\r\\n\\t]*i[\\r\\n\\t]*p[\\r\\n\\t]*t[\\r\\n\\t]*\\:/i;\n\n/**\n * Performs the act of stitching - to take all of the various outputs from the\n * ingestion process, and stitching them together into the final entity JSON\n * shape.\n */\nexport async function performStitching(options: {\n knex: Knex | Knex.Transaction;\n logger: LoggerService;\n strategy: StitchingStrategy;\n entityRef: string;\n stitchTicket?: string;\n}): Promise<'changed' | 'unchanged' | 'abandoned'> {\n const { knex, logger, entityRef } = options;\n const stitchTicket = options.stitchTicket ?? uuid();\n\n // In deferred mode, the entity is removed from the stitch queue on ANY\n // completion, except when an exception is thrown. In the latter case, the\n // entity will be retried at a later time.\n let removeFromStitchQueueOnCompletion = options.strategy.mode === 'deferred';\n\n try {\n const entityResult = await knex<DbRefreshStateRow>('refresh_state')\n .where({ entity_ref: entityRef })\n .limit(1)\n .select('entity_id');\n if (!entityResult.length) {\n // Entity does no exist in refresh state table, no stitching required.\n return 'abandoned';\n }\n\n // Insert stitching ticket that will be compared before inserting the final entity.\n try {\n await knex<DbFinalEntitiesRow>('final_entities')\n .insert({\n entity_id: entityResult[0].entity_id,\n hash: '',\n entity_ref: entityRef,\n stitch_ticket: stitchTicket,\n })\n .onConflict('entity_id')\n .merge(['stitch_ticket']);\n } catch (error) {\n // It's possible to hit a race where a refresh_state table delete + insert\n // is done just after we read the entity_id from it. This conflict is safe\n // to ignore because the current stitching operation will be triggered by\n // the old entry, and the new entry will trigger it's own stitching that\n // will update the entity.\n if (isDatabaseConflictError(error)) {\n logger.debug(`Skipping stitching of ${entityRef}, conflict`, error);\n return 'abandoned';\n }\n\n throw error;\n }\n\n // Selecting from refresh_state and final_entities should yield exactly\n // one row (except in abnormal cases where the stitch was invoked for\n // something that didn't exist at all, in which case it's zero rows).\n // The join with the temporary incoming_references still gives one row.\n const [processedResult, relationsResult] = await Promise.all([\n knex\n .with('incoming_references', function incomingReferences(builder) {\n return builder\n .from('refresh_state_references')\n .where({ target_entity_ref: entityRef })\n .count({ count: '*' });\n })\n .select({\n entityId: 'refresh_state.entity_id',\n processedEntity: 'refresh_state.processed_entity',\n errors: 'refresh_state.errors',\n incomingReferenceCount: 'incoming_references.count',\n previousHash: 'final_entities.hash',\n })\n .from('refresh_state')\n .where({ 'refresh_state.entity_ref': entityRef })\n .crossJoin(knex.raw('incoming_references'))\n .leftOuterJoin('final_entities', {\n 'final_entities.entity_id': 'refresh_state.entity_id',\n }),\n knex\n .distinct({\n relationType: 'type',\n relationTarget: 'target_entity_ref',\n })\n .from('relations')\n .where({ source_entity_ref: entityRef })\n .orderBy('relationType', 'asc')\n .orderBy('relationTarget', 'asc'),\n ]);\n\n // If there were no rows returned, it would mean that there was no\n // matching row even in the refresh_state. This can happen for example\n // if we emit a relation to something that hasn't been ingested yet.\n // It's safe to ignore this stitch attempt in that case.\n if (!processedResult.length) {\n logger.debug(\n `Unable to stitch ${entityRef}, item does not exist in refresh state table`,\n );\n return 'abandoned';\n }\n\n const {\n entityId,\n processedEntity,\n errors,\n incomingReferenceCount,\n previousHash,\n } = processedResult[0];\n\n // If there was no processed entity in place, the target hasn't been\n // through the processing steps yet. It's safe to ignore this stitch\n // attempt in that case, since another stitch will be triggered when\n // that processing has finished.\n if (!processedEntity) {\n logger.debug(\n `Unable to stitch ${entityRef}, the entity has not yet been processed`,\n );\n return 'abandoned';\n }\n\n // Grab the processed entity and stitch all of the relevant data into\n // it\n const entity = JSON.parse(processedEntity) as AlphaEntity;\n const isOrphan = Number(incomingReferenceCount) === 0;\n let statusItems: EntityStatusItem[] = [];\n\n if (isOrphan) {\n logger.debug(`${entityRef} is an orphan`);\n entity.metadata.annotations = {\n ...entity.metadata.annotations,\n ['backstage.io/orphan']: 'true',\n };\n }\n if (errors) {\n const parsedErrors = JSON.parse(errors) as SerializedError[];\n if (Array.isArray(parsedErrors) && parsedErrors.length) {\n statusItems = parsedErrors.map(e => ({\n type: ENTITY_STATUS_CATALOG_PROCESSING_TYPE,\n level: 'error',\n message: `${e.name}: ${e.message}`,\n error: e,\n }));\n }\n }\n // We opt to do this check here as we otherwise can't guarantee that it will be run after all processors\n for (const annotation of [ANNOTATION_VIEW_URL, ANNOTATION_EDIT_URL]) {\n const value = entity.metadata.annotations?.[annotation];\n if (typeof value === 'string' && scriptProtocolPattern.test(value)) {\n entity.metadata.annotations![annotation] =\n 'https://backstage.io/annotation-rejected-for-security-reasons';\n }\n }\n\n // TODO: entityRef is lower case and should be uppercase in the final\n // result\n entity.relations = relationsResult\n .filter(row => row.relationType /* exclude null row, if relevant */)\n .map<EntityRelation>(row => ({\n type: row.relationType!,\n targetRef: row.relationTarget!,\n }));\n if (statusItems.length) {\n entity.status = {\n ...entity.status,\n items: [...(entity.status?.items ?? []), ...statusItems],\n };\n }\n\n // If the output entity was actually not changed, just abort\n const hash = generateStableHash(entity);\n if (hash === previousHash) {\n logger.debug(`Skipped stitching of ${entityRef}, no changes`);\n return 'unchanged';\n }\n\n entity.metadata.uid = entityId;\n if (!entity.metadata.etag) {\n // If the original data source did not have its own etag handling,\n // use the hash as a good-quality etag\n entity.metadata.etag = hash;\n }\n\n // This may throw if the entity is invalid, so we call it before\n // the final_entities write, even though we may end up not needing\n // to write the search index.\n const searchEntries = buildEntitySearch(entityId, entity);\n\n const amountOfRowsChanged = await knex<DbFinalEntitiesRow>('final_entities')\n .update({\n final_entity: JSON.stringify(entity),\n hash,\n last_updated_at: knex.fn.now(),\n })\n .where('entity_id', entityId)\n .where('stitch_ticket', stitchTicket);\n\n if (amountOfRowsChanged === 0) {\n logger.debug(`Entity ${entityRef} is already stitched, skipping write.`);\n return 'abandoned';\n }\n\n await knex.transaction(async trx => {\n await trx<DbSearchRow>('search').where({ entity_id: entityId }).delete();\n await trx.batchInsert('search', searchEntries, BATCH_SIZE);\n });\n\n return 'changed';\n } catch (error) {\n removeFromStitchQueueOnCompletion = false;\n throw error;\n } finally {\n if (removeFromStitchQueueOnCompletion) {\n await markDeferredStitchCompleted({\n knex: knex,\n entityRef,\n stitchTicket,\n });\n }\n }\n}\n"],"names":["uuid","isDatabaseConflictError","ENTITY_STATUS_CATALOG_PROCESSING_TYPE","ANNOTATION_VIEW_URL","ANNOTATION_EDIT_URL","generateStableHash","buildEntitySearch","BATCH_SIZE","markDeferredStitchCompleted"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"performStitching.cjs.js","sources":["../../../../src/database/operations/stitcher/performStitching.ts"],"sourcesContent":["/*\n * Copyright 2023 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 { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from '@backstage/catalog-client';\nimport {\n ANNOTATION_EDIT_URL,\n ANNOTATION_VIEW_URL,\n EntityRelation,\n} from '@backstage/catalog-model';\nimport { AlphaEntity, EntityStatusItem } from '@backstage/catalog-model/alpha';\nimport { SerializedError } from '@backstage/errors';\nimport { Knex } from 'knex';\nimport { StitchingStrategy } from '../../../stitching/types';\nimport {\n DbFinalEntitiesRow,\n DbRefreshStateRow,\n DbSearchRow,\n DbStitchQueueRow,\n} from '../../tables';\nimport { buildEntitySearch } from './buildEntitySearch';\nimport { markDeferredStitchCompleted } from './markDeferredStitchCompleted';\nimport { BATCH_SIZE, generateStableHash } from './util';\nimport {\n LoggerService,\n isDatabaseConflictError,\n} from '@backstage/backend-plugin-api';\n\n// See https://github.com/facebook/react/blob/f0cf832e1d0c8544c36aa8b310960885a11a847c/packages/react-dom-bindings/src/shared/sanitizeURL.js\nconst scriptProtocolPattern =\n // eslint-disable-next-line no-control-regex\n /^[\\u0000-\\u001F ]*j[\\r\\n\\t]*a[\\r\\n\\t]*v[\\r\\n\\t]*a[\\r\\n\\t]*s[\\r\\n\\t]*c[\\r\\n\\t]*r[\\r\\n\\t]*i[\\r\\n\\t]*p[\\r\\n\\t]*t[\\r\\n\\t]*\\:/i;\n\n/**\n * Performs the act of stitching - to take all of the various outputs from the\n * ingestion process, and stitching them together into the final entity JSON\n * shape.\n */\nexport async function performStitching(options: {\n knex: Knex | Knex.Transaction;\n logger: LoggerService;\n strategy: StitchingStrategy;\n entityRef: string;\n stitchTicket?: string;\n}): Promise<'changed' | 'unchanged' | 'abandoned'> {\n const { knex, logger, entityRef } = options;\n const stitchTicket = options.stitchTicket;\n\n // In deferred mode, the entity is removed from the stitch queue on ANY\n // completion, except when an exception is thrown. In the latter case, the\n // entity will be retried at a later time.\n let removeFromStitchQueueOnCompletion = options.strategy.mode === 'deferred';\n\n try {\n const entityResult = await knex<DbRefreshStateRow>('refresh_state')\n .where({ entity_ref: entityRef })\n .limit(1)\n .select('entity_id');\n if (!entityResult.length) {\n // Entity does no exist in refresh state table, no stitching required.\n return 'abandoned';\n }\n\n // Ensure that a final_entities row exists for this entity.\n try {\n await knex<DbFinalEntitiesRow>('final_entities')\n .insert({\n entity_id: entityResult[0].entity_id,\n hash: '',\n entity_ref: entityRef,\n })\n .onConflict('entity_id')\n .ignore();\n } catch (error) {\n // It's possible to hit a race where a refresh_state table delete + insert\n // is done just after we read the entity_id from it. This conflict is safe\n // to ignore because the current stitching operation will be triggered by\n // the old entry, and the new entry will trigger it's own stitching that\n // will update the entity.\n if (isDatabaseConflictError(error)) {\n logger.debug(`Skipping stitching of ${entityRef}, conflict`, error);\n return 'abandoned';\n }\n\n throw error;\n }\n\n // Selecting from refresh_state and final_entities should yield exactly\n // one row (except in abnormal cases where the stitch was invoked for\n // something that didn't exist at all, in which case it's zero rows).\n // The join with the temporary incoming_references still gives one row.\n const [processedResult, relationsResult] = await Promise.all([\n knex\n .with('incoming_references', function incomingReferences(builder) {\n return builder\n .from('refresh_state_references')\n .where({ target_entity_ref: entityRef })\n .count({ count: '*' });\n })\n .select({\n entityId: 'refresh_state.entity_id',\n processedEntity: 'refresh_state.processed_entity',\n errors: 'refresh_state.errors',\n incomingReferenceCount: 'incoming_references.count',\n previousHash: 'final_entities.hash',\n })\n .from('refresh_state')\n .where({ 'refresh_state.entity_ref': entityRef })\n .crossJoin(knex.raw('incoming_references'))\n .leftOuterJoin('final_entities', {\n 'final_entities.entity_id': 'refresh_state.entity_id',\n }),\n knex\n .distinct({\n relationType: 'type',\n relationTarget: 'target_entity_ref',\n })\n .from('relations')\n .where({ source_entity_ref: entityRef })\n .orderBy('relationType', 'asc')\n .orderBy('relationTarget', 'asc'),\n ]);\n\n // If there were no rows returned, it would mean that there was no\n // matching row even in the refresh_state. This can happen for example\n // if we emit a relation to something that hasn't been ingested yet.\n // It's safe to ignore this stitch attempt in that case.\n if (!processedResult.length) {\n logger.debug(\n `Unable to stitch ${entityRef}, item does not exist in refresh state table`,\n );\n return 'abandoned';\n }\n\n const {\n entityId,\n processedEntity,\n errors,\n incomingReferenceCount,\n previousHash,\n } = processedResult[0];\n\n // If there was no processed entity in place, the target hasn't been\n // through the processing steps yet. It's safe to ignore this stitch\n // attempt in that case, since another stitch will be triggered when\n // that processing has finished.\n if (!processedEntity) {\n logger.debug(\n `Unable to stitch ${entityRef}, the entity has not yet been processed`,\n );\n return 'abandoned';\n }\n\n // Grab the processed entity and stitch all of the relevant data into\n // it\n const entity = JSON.parse(processedEntity) as AlphaEntity;\n const isOrphan = Number(incomingReferenceCount) === 0;\n let statusItems: EntityStatusItem[] = [];\n\n if (isOrphan) {\n logger.debug(`${entityRef} is an orphan`);\n entity.metadata.annotations = {\n ...entity.metadata.annotations,\n ['backstage.io/orphan']: 'true',\n };\n }\n if (errors) {\n const parsedErrors = JSON.parse(errors) as SerializedError[];\n if (Array.isArray(parsedErrors) && parsedErrors.length) {\n statusItems = parsedErrors.map(e => ({\n type: ENTITY_STATUS_CATALOG_PROCESSING_TYPE,\n level: 'error',\n message: `${e.name}: ${e.message}`,\n error: e,\n }));\n }\n }\n // We opt to do this check here as we otherwise can't guarantee that it will be run after all processors\n for (const annotation of [ANNOTATION_VIEW_URL, ANNOTATION_EDIT_URL]) {\n const value = entity.metadata.annotations?.[annotation];\n if (typeof value === 'string' && scriptProtocolPattern.test(value)) {\n entity.metadata.annotations![annotation] =\n 'https://backstage.io/annotation-rejected-for-security-reasons';\n }\n }\n\n // TODO: entityRef is lower case and should be uppercase in the final\n // result\n entity.relations = relationsResult\n .filter(row => row.relationType /* exclude null row, if relevant */)\n .map<EntityRelation>(row => ({\n type: row.relationType!,\n targetRef: row.relationTarget!,\n }));\n if (statusItems.length) {\n entity.status = {\n ...entity.status,\n items: [...(entity.status?.items ?? []), ...statusItems],\n };\n }\n\n // If the output entity was actually not changed, just abort\n const hash = generateStableHash(entity);\n if (hash === previousHash) {\n logger.debug(`Skipped stitching of ${entityRef}, no changes`);\n return 'unchanged';\n }\n\n entity.metadata.uid = entityId;\n if (!entity.metadata.etag) {\n // If the original data source did not have its own etag handling,\n // use the hash as a good-quality etag\n entity.metadata.etag = hash;\n }\n\n // This may throw if the entity is invalid, so we call it before\n // the final_entities write, even though we may end up not needing\n // to write the search index.\n const searchEntries = buildEntitySearch(entityId, entity);\n\n let updateQuery = knex<DbFinalEntitiesRow>('final_entities')\n .update({\n final_entity: JSON.stringify(entity),\n hash,\n last_updated_at: knex.fn.now(),\n })\n .where('entity_id', entityId);\n\n // In deferred mode, guard against concurrent stitchers by checking that\n // the stitch_ticket in stitch_queue still matches what we were given.\n if (options.strategy.mode === 'deferred' && stitchTicket) {\n updateQuery = updateQuery.whereExists(\n knex<DbStitchQueueRow>('stitch_queue')\n .where('stitch_queue.entity_ref', entityRef)\n .where('stitch_queue.stitch_ticket', stitchTicket)\n .select(knex.raw('1')),\n );\n }\n\n const amountOfRowsChanged = await updateQuery;\n\n if (amountOfRowsChanged === 0) {\n logger.debug(`Entity ${entityRef} is already stitched, skipping write.`);\n return 'abandoned';\n }\n\n await knex.transaction(async trx => {\n await trx<DbSearchRow>('search').where({ entity_id: entityId }).delete();\n await trx.batchInsert('search', searchEntries, BATCH_SIZE);\n });\n\n return 'changed';\n } catch (error) {\n removeFromStitchQueueOnCompletion = false;\n throw error;\n } finally {\n if (removeFromStitchQueueOnCompletion && stitchTicket) {\n await markDeferredStitchCompleted({\n knex: knex,\n entityRef,\n stitchTicket,\n });\n }\n }\n}\n"],"names":["isDatabaseConflictError","ENTITY_STATUS_CATALOG_PROCESSING_TYPE","ANNOTATION_VIEW_URL","ANNOTATION_EDIT_URL","generateStableHash","buildEntitySearch","BATCH_SIZE","markDeferredStitchCompleted"],"mappings":"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"}
|
|
@@ -25,13 +25,18 @@ class DefaultLocationStore {
|
|
|
25
25
|
getProviderName() {
|
|
26
26
|
return "DefaultLocationStore";
|
|
27
27
|
}
|
|
28
|
-
async createLocation(input) {
|
|
28
|
+
async createLocation(input, options) {
|
|
29
|
+
let existed = false;
|
|
29
30
|
const location = await this.db.transaction(async (tx) => {
|
|
30
31
|
const previousLocations = await this.locations(tx);
|
|
31
|
-
const previousLocation = previousLocations.
|
|
32
|
+
const previousLocation = previousLocations.find(
|
|
32
33
|
(l) => input.type === l.type && input.target === l.target
|
|
33
34
|
);
|
|
34
35
|
if (previousLocation) {
|
|
36
|
+
if (options?.onConflict === "refresh") {
|
|
37
|
+
existed = true;
|
|
38
|
+
return previousLocation;
|
|
39
|
+
}
|
|
35
40
|
throw new errors.ConflictError(
|
|
36
41
|
`Location ${input.type}:${input.target} already exists`
|
|
37
42
|
);
|
|
@@ -50,6 +55,13 @@ class DefaultLocationStore {
|
|
|
50
55
|
added: [{ entity, locationKey: util.getEntityLocationRef(entity) }],
|
|
51
56
|
removed: []
|
|
52
57
|
});
|
|
58
|
+
if (existed) {
|
|
59
|
+
const entityRef = catalogModel.stringifyEntityRef(entity);
|
|
60
|
+
await this.db("refresh_state").where({ entity_ref: entityRef }).update({
|
|
61
|
+
next_update_at: this.db.fn.now(),
|
|
62
|
+
result_hash: ""
|
|
63
|
+
});
|
|
64
|
+
}
|
|
53
65
|
return location;
|
|
54
66
|
}
|
|
55
67
|
async listLocations() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DefaultLocationStore.cjs.js","sources":["../../src/providers/DefaultLocationStore.ts"],"sourcesContent":["/*\n * Copyright 2021 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 { Location } from '@backstage/catalog-client';\nimport { ConflictError, InputError, NotFoundError } from '@backstage/errors';\nimport { Knex } from 'knex';\nimport { v4 as uuid } from 'uuid';\nimport {\n DbLocationsRow,\n DbRefreshStateRow,\n DbSearchRow,\n} from '../database/tables';\nimport { getEntityLocationRef } from '../processing/util';\nimport {\n EntityProvider,\n EntityProviderConnection,\n} from '@backstage/plugin-catalog-node';\nimport { locationSpecToLocationEntity } from '../util/conversion';\nimport { LocationInput, LocationStore } from '../service/types';\nimport {\n ANNOTATION_ORIGIN_LOCATION,\n CompoundEntityRef,\n parseLocationRef,\n stringifyEntityRef,\n} from '@backstage/catalog-model';\nimport {\n CatalogScmEvent,\n CatalogScmEventsService,\n} from '@backstage/plugin-catalog-node/alpha';\nimport { chunk, uniqBy } from 'lodash';\nimport parseGitUrl, { type GitUrl } from 'git-url-parse';\nimport { ScmEventHandlingConfig } from '../util/readScmEventHandlingConfig';\nimport {\n FilterPredicate,\n FilterPredicateValue,\n} from '@backstage/filter-predicates';\n\nexport class DefaultLocationStore implements LocationStore, EntityProvider {\n private _connection: EntityProviderConnection | undefined;\n private readonly db: Knex;\n private readonly scmEvents: CatalogScmEventsService;\n private readonly scmEventHandlingConfig: ScmEventHandlingConfig;\n\n constructor(\n db: Knex,\n scmEvents: CatalogScmEventsService,\n scmEventHandlingConfig: ScmEventHandlingConfig,\n ) {\n this.db = db;\n this.scmEvents = scmEvents;\n this.scmEventHandlingConfig = scmEventHandlingConfig;\n }\n\n getProviderName(): string {\n return 'DefaultLocationStore';\n }\n\n async createLocation(input: LocationInput): Promise<Location> {\n const location = await this.db.transaction(async tx => {\n // Attempt to find a previous location matching the input\n const previousLocations = await this.locations(tx);\n // TODO: when location id's are a compilation of input target we can remove this full\n // lookup of locations first and just grab the by that instead.\n const previousLocation = previousLocations.some(\n l => input.type === l.type && input.target === l.target,\n );\n if (previousLocation) {\n throw new ConflictError(\n `Location ${input.type}:${input.target} already exists`,\n );\n }\n\n const inner: DbLocationsRow = {\n id: uuid(),\n type: input.type,\n target: input.target,\n };\n\n await tx<DbLocationsRow>('locations').insert(inner);\n\n return inner;\n });\n const entity = locationSpecToLocationEntity({ location });\n await this.connection.applyMutation({\n type: 'delta',\n added: [{ entity, locationKey: getEntityLocationRef(entity) }],\n removed: [],\n });\n\n return location;\n }\n\n async listLocations(): Promise<Location[]> {\n return await this.locations();\n }\n\n async queryLocations(options: {\n limit: number;\n afterId?: string;\n query?: FilterPredicate;\n }): Promise<{ items: Location[]; totalItems: number }> {\n let itemsQuery = this.db<DbLocationsRow>('locations').whereNot(\n 'type',\n 'bootstrap',\n );\n\n if (options.query) {\n itemsQuery = applyLocationFilterToQuery(\n this.db.client.config.client,\n itemsQuery,\n options.query,\n );\n }\n\n const countQuery = itemsQuery.clone().count('*', { as: 'count' });\n\n itemsQuery = itemsQuery.orderBy('id', 'asc');\n if (options.afterId !== undefined) {\n itemsQuery = itemsQuery.where('id', '>', options.afterId);\n }\n if (options.limit !== undefined) {\n itemsQuery = itemsQuery.limit(options.limit);\n }\n\n const [items, [{ count }]] = await Promise.all([itemsQuery, countQuery]);\n\n return {\n items: items.map(item => ({\n id: item.id,\n target: item.target,\n type: item.type,\n })),\n totalItems: Number(count),\n };\n }\n\n async getLocation(id: string): Promise<Location> {\n const items = await this.db<DbLocationsRow>('locations')\n .where({ id })\n .select();\n\n if (!items.length) {\n throw new NotFoundError(`Found no location with ID ${id}`);\n }\n return items[0];\n }\n\n async deleteLocation(id: string): Promise<void> {\n if (!this.connection) {\n throw new Error('location store is not initialized');\n }\n\n const deleted = await this.db.transaction(async tx => {\n const [location] = await tx<DbLocationsRow>('locations')\n .where({ id })\n .select();\n\n if (!location) {\n throw new NotFoundError(`Found no location with ID ${id}`);\n }\n\n await tx<DbLocationsRow>('locations').where({ id }).del();\n return location;\n });\n const entity = locationSpecToLocationEntity({ location: deleted });\n await this.connection.applyMutation({\n type: 'delta',\n added: [],\n removed: [{ entity, locationKey: getEntityLocationRef(entity) }],\n });\n }\n\n async getLocationByEntity(entityRef: CompoundEntityRef): Promise<Location> {\n const entityRefString = stringifyEntityRef(entityRef);\n\n const [entityRow] = await this.db<DbRefreshStateRow>('refresh_state')\n .where({ entity_ref: entityRefString })\n .select('entity_id')\n .limit(1);\n if (!entityRow) {\n throw new NotFoundError(`found no entity for ref ${entityRefString}`);\n }\n\n const [searchRow] = await this.db<DbSearchRow>('search')\n .where({\n entity_id: entityRow.entity_id,\n key: `metadata.annotations.${ANNOTATION_ORIGIN_LOCATION}`,\n })\n .select('original_value')\n .limit(1);\n if (!searchRow?.original_value) {\n throw new NotFoundError(\n `found no origin annotation for ref ${entityRefString}`,\n );\n }\n\n const { type, target } = parseLocationRef(searchRow.original_value);\n const [locationRow] = await this.db<DbLocationsRow>('locations')\n .where({ type, target })\n .select()\n .limit(1);\n\n if (!locationRow) {\n throw new NotFoundError(\n `Found no location with type ${type} and target ${target}`,\n );\n }\n\n return locationRow;\n }\n\n private get connection(): EntityProviderConnection {\n if (!this._connection) {\n throw new Error('location store is not initialized');\n }\n\n return this._connection;\n }\n\n async connect(connection: EntityProviderConnection): Promise<void> {\n this._connection = connection;\n\n const locations = await this.locations();\n\n const entities = locations.map(location => {\n const entity = locationSpecToLocationEntity({ location });\n return { entity, locationKey: getEntityLocationRef(entity) };\n });\n\n await this.connection.applyMutation({\n type: 'full',\n entities,\n });\n\n if (\n this.scmEventHandlingConfig.unregister ||\n this.scmEventHandlingConfig.move\n ) {\n this.scmEvents.subscribe({ onEvents: this.#onScmEvents.bind(this) });\n }\n }\n\n private async locations(dbOrTx: Knex.Transaction | Knex = this.db) {\n const locations = await dbOrTx<DbLocationsRow>('locations').select();\n return (\n locations\n // TODO(blam): We should create a mutation to remove this location for everyone\n // eventually when it's all done and dusted\n .filter(({ type }) => type !== 'bootstrap')\n .map(item => ({\n id: item.id,\n target: item.target,\n type: item.type,\n }))\n );\n }\n\n // #region SCM event handling\n\n async #onScmEvents(events: CatalogScmEvent[]): Promise<void> {\n const exactLocationsToDelete = new Set<string>();\n const locationPrefixesToDelete = new Set<string>();\n const exactLocationsToCreate = new Set<string>();\n const locationPrefixesToMove = new Map<string, string>();\n\n for (const event of events) {\n if (\n event.type === 'location.deleted' &&\n this.scmEventHandlingConfig.unregister\n ) {\n exactLocationsToDelete.add(event.url);\n } else if (\n event.type === 'location.moved' &&\n this.scmEventHandlingConfig.move\n ) {\n // Since Location entities are named after their target URL, these\n // unfortunately have to be translated into deletion and creation\n exactLocationsToDelete.add(event.fromUrl);\n exactLocationsToCreate.add(event.toUrl);\n } else if (\n event.type === 'repository.deleted' &&\n this.scmEventHandlingConfig.unregister\n ) {\n locationPrefixesToDelete.add(event.url);\n } else if (\n event.type === 'repository.moved' &&\n this.scmEventHandlingConfig.move\n ) {\n // These also have to be handled with deletions and creations\n locationPrefixesToMove.set(event.fromUrl, event.toUrl);\n }\n }\n\n if (exactLocationsToDelete.size > 0) {\n const count = await this.#deleteLocationsByExactUrl(\n exactLocationsToDelete,\n );\n this.scmEvents.markEventActionTaken({\n count,\n action: 'delete',\n });\n }\n if (locationPrefixesToDelete.size > 0) {\n const count = await this.#deleteLocationsByUrlPrefix(\n locationPrefixesToDelete,\n );\n this.scmEvents.markEventActionTaken({\n count,\n action: 'delete',\n });\n }\n if (exactLocationsToCreate.size > 0) {\n const count = await this.#createLocationsByExactUrl(\n exactLocationsToCreate,\n );\n this.scmEvents.markEventActionTaken({\n count,\n action: 'create',\n });\n }\n if (locationPrefixesToMove.size > 0) {\n const count = await this.#moveLocationsByUrlPrefix(\n locationPrefixesToMove,\n );\n this.scmEvents.markEventActionTaken({\n count,\n action: 'move',\n });\n }\n }\n\n async #createLocationsByExactUrl(urls: Iterable<string>): Promise<number> {\n let count = 0;\n\n for (const batch of chunk(Array.from(urls), 100)) {\n const existingUrls = await this.db<DbLocationsRow>('locations')\n .where('type', '=', 'url')\n .whereIn('target', batch)\n .select()\n .then(rows => new Set(rows.map(row => row.target)));\n\n const newLocations = batch\n .filter(url => !existingUrls.has(url))\n .map(url => ({ id: uuid(), type: 'url', target: url }));\n\n if (newLocations.length) {\n await this.db<DbLocationsRow>('locations').insert(newLocations);\n\n await this.connection.applyMutation({\n type: 'delta',\n added: newLocations.map(location => {\n const entity = locationSpecToLocationEntity({ location });\n return { entity, locationKey: getEntityLocationRef(entity) };\n }),\n removed: [],\n });\n\n count += newLocations.length;\n }\n }\n\n return count;\n }\n\n async #deleteLocationsByExactUrl(urls: Iterable<string>): Promise<number> {\n let count = 0;\n\n for (const batch of chunk(Array.from(urls), 100)) {\n const rows = await this.db<DbLocationsRow>('locations')\n .where('type', '=', 'url')\n .whereIn('target', batch)\n .select();\n\n if (rows.length) {\n await this.db<DbLocationsRow>('locations')\n .whereIn(\n 'id',\n rows.map(row => row.id),\n )\n .delete();\n\n await this.connection.applyMutation({\n type: 'delta',\n added: [],\n removed: rows.map(row => ({\n entity: locationSpecToLocationEntity({ location: row }),\n })),\n });\n\n count += rows.length;\n }\n }\n\n return count;\n }\n\n async #deleteLocationsByUrlPrefix(urls: Iterable<string>): Promise<number> {\n const matches = await this.#findLocationsByPrefixOrExactMatch(urls);\n if (matches.length) {\n await this.#deleteLocations(matches.map(l => l.row));\n }\n\n return matches.length;\n }\n\n async #moveLocationsByUrlPrefix(\n urlPrefixes: Map<string, string>,\n ): Promise<number> {\n let count = 0;\n\n for (const [fromPrefix, toPrefix] of urlPrefixes) {\n if (fromPrefix === toPrefix) {\n continue;\n }\n\n if (fromPrefix.match(/[?#]/) || toPrefix.match(/[?#]/)) {\n // TODO(freben): We can't yet support complex URL locations where e.g.\n // the path can be anywhere in the URL including in the query or hash\n // part. The code below currently assumes that we can use simple\n // substring operations.\n continue;\n }\n\n const matches = await this.#findLocationsByPrefixOrExactMatch([\n fromPrefix,\n ]);\n if (matches.length) {\n await this.#deleteLocations(matches.map(m => m.row));\n\n await this.#createLocationsByExactUrl(\n matches.map(m => {\n const remainder = m.row.target\n .slice(fromPrefix.length)\n .replace(/^\\/+/, '');\n if (!remainder) {\n return toPrefix;\n }\n return `${toPrefix.replace(/\\/+$/, '')}/${remainder}`;\n }),\n );\n\n count += matches.length;\n }\n }\n\n return count;\n }\n\n async #deleteLocations(rows: DbLocationsRow[]): Promise<void> {\n // Delete the location table entries (in chunks so as not to overload the\n // knex query builder)\n for (const ids of chunk(\n rows.map(l => l.id),\n 100,\n )) {\n await this.db<DbLocationsRow>('locations').whereIn('id', ids).delete();\n }\n\n // Delete the corresponding Location kind entities (this is efficiently\n // chunked internally in the catalog)\n await this.connection.applyMutation({\n type: 'delta',\n added: [],\n removed: rows.map(l => ({\n entity: locationSpecToLocationEntity({ location: l }),\n })),\n });\n }\n\n /**\n * Given a \"base\" URL prefix, find all locations that are for paths at or\n * below it.\n *\n * For example, given a base URL prefix of\n * \"https://github.com/backstage/backstage/blob/master/plugins\", it will match\n * locations inside the plugins directory, and nowhere else.\n */\n async #findLocationsByPrefixOrExactMatch(\n urls: Iterable<string>,\n ): Promise<Array<{ row: DbLocationsRow; parsed: GitUrl }>> {\n const result = new Array<{ row: DbLocationsRow; parsed: GitUrl }>();\n\n for (const url of urls) {\n let base: GitUrl;\n try {\n base = parseGitUrl(url);\n } catch (error) {\n throw new Error(`Invalid URL prefix, could not parse: ${url}`);\n }\n\n if (!base.owner || !base.name) {\n throw new Error(\n `Invalid URL prefix, missing owner or repository: ${url}`,\n );\n }\n\n const pathPrefix =\n base.filepath === '' || base.filepath.endsWith('/')\n ? base.filepath\n : `${base.filepath}/`;\n\n const rows = await this.db<DbLocationsRow>('locations')\n .where('type', '=', 'url')\n // Initial rough pruning to not have to go through them all\n .where('target', 'like', `%${base.owner}%`)\n .where('target', 'like', `%${base.name}%`)\n .select();\n\n result.push(\n ...rows.flatMap(row => {\n try {\n // We do this pretty explicit set of checks because we want to support\n // providers that have a URL format where the path isn't necessarily at\n // the end of the URL string (e.g. in the query part). Some of these may\n // be empty strings etc, but that's fine as long as they parse to the\n // same thing as above.\n const candidate = parseGitUrl(row.target);\n\n if (\n candidate.protocol === base.protocol &&\n candidate.resource === base.resource &&\n candidate.port === base.port &&\n candidate.organization === base.organization &&\n candidate.owner === base.owner &&\n candidate.name === base.name &&\n // If the base has no ref (for example didn't have the \"/blob/master\"\n // part and therefore targeted an entire repository) then we match any\n // ref below that\n (!base.ref || candidate.ref === base.ref) &&\n // Match both on exact equality and any subpath with a slash between\n (candidate.filepath === base.filepath ||\n candidate.filepath.startsWith(pathPrefix))\n ) {\n return [{ row, parsed: candidate }];\n }\n return [];\n } catch {\n return [];\n }\n }),\n );\n }\n\n return uniqBy(result, entry => entry.row.id);\n }\n\n // #endregion\n}\n\n/**\n * Recursively builds up the SQL expression corresponding to the given filter\n * predicate.\n *\n * @remarks\n *\n * Design note: The code prefers to let the SQL engine achieve case\n * insensitivity. We could attempt to use `.toUpperCase` etc on the client\n * side, but that would only work for the values being passed in, not the column\n * side of the expression. If we let the database perform UPPER on both, we know\n * that they will always be locale consistent etc as well.\n *\n * This does come at a runtime cost. However, the data set is typically rather\n * small in the grand scheme of things, and we can add the proper indices in the\n * future if needed. At this point I considered it not worth the effort.\n */\nfunction applyLocationFilterToQuery(\n clientType: string,\n inputQuery: Knex.QueryBuilder,\n query: FilterPredicate,\n): Knex.QueryBuilder {\n let result = inputQuery;\n\n if (!query || typeof query !== 'object' || Array.isArray(query)) {\n throw new InputError('Invalid filter predicate, expected an object');\n }\n\n if ('$all' in query) {\n // Explicitly handle the empty case to avoid malformed SQL\n if (query.$all.length === 0) {\n return result.whereRaw('1 = 0');\n }\n\n return result.where(outer => {\n for (const subQuery of query.$all) {\n outer.andWhere(inner => {\n applyLocationFilterToQuery(clientType, inner, subQuery);\n });\n }\n });\n }\n\n if ('$any' in query) {\n // Explicitly handle the empty case to avoid malformed SQL\n if (query.$any.length === 0) {\n return result.whereRaw('1 = 0');\n }\n\n return result.where(outer => {\n for (const subQuery of query.$any) {\n outer.orWhere(inner => {\n applyLocationFilterToQuery(clientType, inner, subQuery);\n });\n }\n });\n }\n\n if ('$not' in query) {\n return result.whereNot(inner => {\n applyLocationFilterToQuery(clientType, inner, query.$not);\n });\n }\n\n const entries = Object.entries(query);\n const keys = entries.map(e => e[0]);\n if (keys.some(k => k.startsWith('$'))) {\n throw new InputError(\n `Invalid filter predicate, unknown logic operator '${keys.join(', ')}'`,\n );\n }\n\n for (const [keyAnyCase, value] of entries) {\n const key = keyAnyCase.toLocaleLowerCase('en-US');\n if (!['id', 'type', 'target'].includes(key)) {\n throw new InputError(\n `Invalid filter predicate, expected key to be 'id', 'type', or 'target', got '${keyAnyCase}'`,\n );\n }\n\n result = applyFilterValueToQuery(clientType, result, key, value);\n }\n\n return result;\n}\n\nfunction applyFilterValueToQuery(\n clientType: string,\n result: Knex.QueryBuilder,\n key: string,\n value: FilterPredicateValue,\n): Knex.QueryBuilder {\n // Is it a primitive value?\n if (['string', 'number', 'boolean'].includes(typeof value)) {\n if (clientType === 'pg') {\n return result.whereRaw(`UPPER(??::text) = UPPER(?::text)`, [key, value]);\n }\n\n if (clientType.includes('mysql')) {\n return result.whereRaw(\n `UPPER(CAST(?? AS CHAR)) = UPPER(CAST(? AS CHAR))`,\n [key, value],\n );\n }\n\n return result.whereRaw(`UPPER(??) = UPPER(?)`, [key, value]);\n }\n\n // Is it a matcher object?\n if (typeof value === 'object') {\n if (!value || Array.isArray(value)) {\n throw new InputError(\n `Invalid filter predicate, got unknown matcher object '${JSON.stringify(\n value,\n )}'`,\n );\n }\n\n // Technically existence checks do not make much sense in the context of\n // this table at the time of writing (values are always present), but\n // there's nothing gained by prohibiting it.\n if ('$exists' in value) {\n return value.$exists ? result.whereNotNull(key) : result.whereNull(key);\n }\n\n if ('$in' in value) {\n // Explicitly handle the empty case to avoid malformed SQL\n if (value.$in.length === 0) {\n return result.whereRaw('1 = 0');\n }\n\n // The id is matched with plain equality; it's of UUID type and case\n // insensitivity does not apply.\n if (key === 'id') {\n return result.whereIn(key, value.$in);\n }\n\n if (clientType === 'pg') {\n const rhs = value.$in.map(() => 'UPPER(?::text)').join(', ');\n return result.whereRaw(`UPPER(??::text) IN (${rhs})`, [\n key,\n ...value.$in,\n ]);\n }\n\n if (clientType.includes('mysql')) {\n const rhs = value.$in.map(() => 'UPPER(CAST(? AS CHAR))').join(', ');\n return result.whereRaw(`UPPER(CAST(?? AS CHAR)) IN (${rhs})`, [\n key,\n ...value.$in,\n ]);\n }\n\n const rhs = value.$in.map(() => 'UPPER(?)').join(', ');\n return result.whereRaw(`UPPER(??) IN (${rhs})`, [key, ...value.$in]);\n }\n\n if ('$hasPrefix' in value) {\n const escaped = value.$hasPrefix.replace(/([\\\\%_])/g, '\\\\$1');\n\n if (clientType === 'pg') {\n return result.whereRaw(\"?? ilike ? escape '\\\\'\", [key, `${escaped}%`]);\n }\n\n if (clientType.includes('mysql')) {\n return result.whereRaw(\"UPPER(??) like UPPER(?) escape '\\\\\\\\'\", [\n key,\n `${escaped}%`,\n ]);\n }\n\n return result.whereRaw(\"UPPER(??) like UPPER(?) escape '\\\\'\", [\n key,\n `${escaped}%`,\n ]);\n }\n\n // There are no array shaped values for location queries, so we just always\n // fail here\n if ('$contains' in value) {\n return result.whereRaw('1 = 0');\n }\n\n throw new InputError(\n `Invalid filter predicate, got unknown matcher object '${JSON.stringify(\n value,\n )}'`,\n );\n }\n\n throw new InputError(\n `Invalid filter predicate, expected value to be a primitive value or a matcher object, got '${typeof value}'`,\n );\n}\n"],"names":["ConflictError","uuid","locationSpecToLocationEntity","getEntityLocationRef","NotFoundError","stringifyEntityRef","ANNOTATION_ORIGIN_LOCATION","parseLocationRef","chunk","parseGitUrl","uniqBy","InputError","rhs"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DefaultLocationStore.cjs.js","sources":["../../src/providers/DefaultLocationStore.ts"],"sourcesContent":["/*\n * Copyright 2021 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 { Location } from '@backstage/catalog-client';\nimport { ConflictError, InputError, NotFoundError } from '@backstage/errors';\nimport { Knex } from 'knex';\nimport { v4 as uuid } from 'uuid';\nimport {\n DbLocationsRow,\n DbRefreshStateRow,\n DbSearchRow,\n} from '../database/tables';\nimport { getEntityLocationRef } from '../processing/util';\nimport {\n EntityProvider,\n EntityProviderConnection,\n} from '@backstage/plugin-catalog-node';\nimport { locationSpecToLocationEntity } from '../util/conversion';\nimport { LocationInput, LocationStore } from '../service/types';\nimport {\n ANNOTATION_ORIGIN_LOCATION,\n CompoundEntityRef,\n parseLocationRef,\n stringifyEntityRef,\n} from '@backstage/catalog-model';\nimport {\n CatalogScmEvent,\n CatalogScmEventsService,\n} from '@backstage/plugin-catalog-node/alpha';\nimport { chunk, uniqBy } from 'lodash';\nimport parseGitUrl, { type GitUrl } from 'git-url-parse';\nimport { ScmEventHandlingConfig } from '../util/readScmEventHandlingConfig';\nimport {\n FilterPredicate,\n FilterPredicateValue,\n} from '@backstage/filter-predicates';\n\nexport class DefaultLocationStore implements LocationStore, EntityProvider {\n private _connection: EntityProviderConnection | undefined;\n private readonly db: Knex;\n private readonly scmEvents: CatalogScmEventsService;\n private readonly scmEventHandlingConfig: ScmEventHandlingConfig;\n\n constructor(\n db: Knex,\n scmEvents: CatalogScmEventsService,\n scmEventHandlingConfig: ScmEventHandlingConfig,\n ) {\n this.db = db;\n this.scmEvents = scmEvents;\n this.scmEventHandlingConfig = scmEventHandlingConfig;\n }\n\n getProviderName(): string {\n return 'DefaultLocationStore';\n }\n\n async createLocation(\n input: LocationInput,\n options?: {\n onConflict?: 'refresh' | 'reject';\n },\n ): Promise<Location> {\n let existed = false;\n\n const location = await this.db.transaction(async tx => {\n // Attempt to find a previous location matching the input\n const previousLocations = await this.locations(tx);\n // TODO: when location id's are a compilation of input target we can remove this full\n // lookup of locations first and just grab the by that instead.\n const previousLocation = previousLocations.find(\n l => input.type === l.type && input.target === l.target,\n );\n if (previousLocation) {\n if (options?.onConflict === 'refresh') {\n existed = true;\n return previousLocation;\n }\n throw new ConflictError(\n `Location ${input.type}:${input.target} already exists`,\n );\n }\n\n const inner: DbLocationsRow = {\n id: uuid(),\n type: input.type,\n target: input.target,\n };\n\n await tx<DbLocationsRow>('locations').insert(inner);\n\n return inner;\n });\n\n // Always upsert the entity, even if the location already existed, to\n // recover from cases where the entity was inadvertently deleted.\n const entity = locationSpecToLocationEntity({ location });\n await this.connection.applyMutation({\n type: 'delta',\n added: [{ entity, locationKey: getEntityLocationRef(entity) }],\n removed: [],\n });\n\n if (existed) {\n // This is the \"onConflict refresh\" case, where a re-registration safely\n // tries to recover from a bad state.\n const entityRef = stringifyEntityRef(entity);\n await this.db<DbRefreshStateRow>('refresh_state')\n .where({ entity_ref: entityRef })\n .update({\n next_update_at: this.db.fn.now(),\n result_hash: '',\n });\n }\n\n return location;\n }\n\n async listLocations(): Promise<Location[]> {\n return await this.locations();\n }\n\n async queryLocations(options: {\n limit: number;\n afterId?: string;\n query?: FilterPredicate;\n }): Promise<{ items: Location[]; totalItems: number }> {\n let itemsQuery = this.db<DbLocationsRow>('locations').whereNot(\n 'type',\n 'bootstrap',\n );\n\n if (options.query) {\n itemsQuery = applyLocationFilterToQuery(\n this.db.client.config.client,\n itemsQuery,\n options.query,\n );\n }\n\n const countQuery = itemsQuery.clone().count('*', { as: 'count' });\n\n itemsQuery = itemsQuery.orderBy('id', 'asc');\n if (options.afterId !== undefined) {\n itemsQuery = itemsQuery.where('id', '>', options.afterId);\n }\n if (options.limit !== undefined) {\n itemsQuery = itemsQuery.limit(options.limit);\n }\n\n const [items, [{ count }]] = await Promise.all([itemsQuery, countQuery]);\n\n return {\n items: items.map(item => ({\n id: item.id,\n target: item.target,\n type: item.type,\n })),\n totalItems: Number(count),\n };\n }\n\n async getLocation(id: string): Promise<Location> {\n const items = await this.db<DbLocationsRow>('locations')\n .where({ id })\n .select();\n\n if (!items.length) {\n throw new NotFoundError(`Found no location with ID ${id}`);\n }\n return items[0];\n }\n\n async deleteLocation(id: string): Promise<void> {\n if (!this.connection) {\n throw new Error('location store is not initialized');\n }\n\n const deleted = await this.db.transaction(async tx => {\n const [location] = await tx<DbLocationsRow>('locations')\n .where({ id })\n .select();\n\n if (!location) {\n throw new NotFoundError(`Found no location with ID ${id}`);\n }\n\n await tx<DbLocationsRow>('locations').where({ id }).del();\n return location;\n });\n const entity = locationSpecToLocationEntity({ location: deleted });\n await this.connection.applyMutation({\n type: 'delta',\n added: [],\n removed: [{ entity, locationKey: getEntityLocationRef(entity) }],\n });\n }\n\n async getLocationByEntity(entityRef: CompoundEntityRef): Promise<Location> {\n const entityRefString = stringifyEntityRef(entityRef);\n\n const [entityRow] = await this.db<DbRefreshStateRow>('refresh_state')\n .where({ entity_ref: entityRefString })\n .select('entity_id')\n .limit(1);\n if (!entityRow) {\n throw new NotFoundError(`found no entity for ref ${entityRefString}`);\n }\n\n const [searchRow] = await this.db<DbSearchRow>('search')\n .where({\n entity_id: entityRow.entity_id,\n key: `metadata.annotations.${ANNOTATION_ORIGIN_LOCATION}`,\n })\n .select('original_value')\n .limit(1);\n if (!searchRow?.original_value) {\n throw new NotFoundError(\n `found no origin annotation for ref ${entityRefString}`,\n );\n }\n\n const { type, target } = parseLocationRef(searchRow.original_value);\n const [locationRow] = await this.db<DbLocationsRow>('locations')\n .where({ type, target })\n .select()\n .limit(1);\n\n if (!locationRow) {\n throw new NotFoundError(\n `Found no location with type ${type} and target ${target}`,\n );\n }\n\n return locationRow;\n }\n\n private get connection(): EntityProviderConnection {\n if (!this._connection) {\n throw new Error('location store is not initialized');\n }\n\n return this._connection;\n }\n\n async connect(connection: EntityProviderConnection): Promise<void> {\n this._connection = connection;\n\n const locations = await this.locations();\n\n const entities = locations.map(location => {\n const entity = locationSpecToLocationEntity({ location });\n return { entity, locationKey: getEntityLocationRef(entity) };\n });\n\n await this.connection.applyMutation({\n type: 'full',\n entities,\n });\n\n if (\n this.scmEventHandlingConfig.unregister ||\n this.scmEventHandlingConfig.move\n ) {\n this.scmEvents.subscribe({ onEvents: this.#onScmEvents.bind(this) });\n }\n }\n\n private async locations(dbOrTx: Knex.Transaction | Knex = this.db) {\n const locations = await dbOrTx<DbLocationsRow>('locations').select();\n return (\n locations\n // TODO(blam): We should create a mutation to remove this location for everyone\n // eventually when it's all done and dusted\n .filter(({ type }) => type !== 'bootstrap')\n .map(item => ({\n id: item.id,\n target: item.target,\n type: item.type,\n }))\n );\n }\n\n // #region SCM event handling\n\n async #onScmEvents(events: CatalogScmEvent[]): Promise<void> {\n const exactLocationsToDelete = new Set<string>();\n const locationPrefixesToDelete = new Set<string>();\n const exactLocationsToCreate = new Set<string>();\n const locationPrefixesToMove = new Map<string, string>();\n\n for (const event of events) {\n if (\n event.type === 'location.deleted' &&\n this.scmEventHandlingConfig.unregister\n ) {\n exactLocationsToDelete.add(event.url);\n } else if (\n event.type === 'location.moved' &&\n this.scmEventHandlingConfig.move\n ) {\n // Since Location entities are named after their target URL, these\n // unfortunately have to be translated into deletion and creation\n exactLocationsToDelete.add(event.fromUrl);\n exactLocationsToCreate.add(event.toUrl);\n } else if (\n event.type === 'repository.deleted' &&\n this.scmEventHandlingConfig.unregister\n ) {\n locationPrefixesToDelete.add(event.url);\n } else if (\n event.type === 'repository.moved' &&\n this.scmEventHandlingConfig.move\n ) {\n // These also have to be handled with deletions and creations\n locationPrefixesToMove.set(event.fromUrl, event.toUrl);\n }\n }\n\n if (exactLocationsToDelete.size > 0) {\n const count = await this.#deleteLocationsByExactUrl(\n exactLocationsToDelete,\n );\n this.scmEvents.markEventActionTaken({\n count,\n action: 'delete',\n });\n }\n if (locationPrefixesToDelete.size > 0) {\n const count = await this.#deleteLocationsByUrlPrefix(\n locationPrefixesToDelete,\n );\n this.scmEvents.markEventActionTaken({\n count,\n action: 'delete',\n });\n }\n if (exactLocationsToCreate.size > 0) {\n const count = await this.#createLocationsByExactUrl(\n exactLocationsToCreate,\n );\n this.scmEvents.markEventActionTaken({\n count,\n action: 'create',\n });\n }\n if (locationPrefixesToMove.size > 0) {\n const count = await this.#moveLocationsByUrlPrefix(\n locationPrefixesToMove,\n );\n this.scmEvents.markEventActionTaken({\n count,\n action: 'move',\n });\n }\n }\n\n async #createLocationsByExactUrl(urls: Iterable<string>): Promise<number> {\n let count = 0;\n\n for (const batch of chunk(Array.from(urls), 100)) {\n const existingUrls = await this.db<DbLocationsRow>('locations')\n .where('type', '=', 'url')\n .whereIn('target', batch)\n .select()\n .then(rows => new Set(rows.map(row => row.target)));\n\n const newLocations = batch\n .filter(url => !existingUrls.has(url))\n .map(url => ({ id: uuid(), type: 'url', target: url }));\n\n if (newLocations.length) {\n await this.db<DbLocationsRow>('locations').insert(newLocations);\n\n await this.connection.applyMutation({\n type: 'delta',\n added: newLocations.map(location => {\n const entity = locationSpecToLocationEntity({ location });\n return { entity, locationKey: getEntityLocationRef(entity) };\n }),\n removed: [],\n });\n\n count += newLocations.length;\n }\n }\n\n return count;\n }\n\n async #deleteLocationsByExactUrl(urls: Iterable<string>): Promise<number> {\n let count = 0;\n\n for (const batch of chunk(Array.from(urls), 100)) {\n const rows = await this.db<DbLocationsRow>('locations')\n .where('type', '=', 'url')\n .whereIn('target', batch)\n .select();\n\n if (rows.length) {\n await this.db<DbLocationsRow>('locations')\n .whereIn(\n 'id',\n rows.map(row => row.id),\n )\n .delete();\n\n await this.connection.applyMutation({\n type: 'delta',\n added: [],\n removed: rows.map(row => ({\n entity: locationSpecToLocationEntity({ location: row }),\n })),\n });\n\n count += rows.length;\n }\n }\n\n return count;\n }\n\n async #deleteLocationsByUrlPrefix(urls: Iterable<string>): Promise<number> {\n const matches = await this.#findLocationsByPrefixOrExactMatch(urls);\n if (matches.length) {\n await this.#deleteLocations(matches.map(l => l.row));\n }\n\n return matches.length;\n }\n\n async #moveLocationsByUrlPrefix(\n urlPrefixes: Map<string, string>,\n ): Promise<number> {\n let count = 0;\n\n for (const [fromPrefix, toPrefix] of urlPrefixes) {\n if (fromPrefix === toPrefix) {\n continue;\n }\n\n if (fromPrefix.match(/[?#]/) || toPrefix.match(/[?#]/)) {\n // TODO(freben): We can't yet support complex URL locations where e.g.\n // the path can be anywhere in the URL including in the query or hash\n // part. The code below currently assumes that we can use simple\n // substring operations.\n continue;\n }\n\n const matches = await this.#findLocationsByPrefixOrExactMatch([\n fromPrefix,\n ]);\n if (matches.length) {\n await this.#deleteLocations(matches.map(m => m.row));\n\n await this.#createLocationsByExactUrl(\n matches.map(m => {\n const remainder = m.row.target\n .slice(fromPrefix.length)\n .replace(/^\\/+/, '');\n if (!remainder) {\n return toPrefix;\n }\n return `${toPrefix.replace(/\\/+$/, '')}/${remainder}`;\n }),\n );\n\n count += matches.length;\n }\n }\n\n return count;\n }\n\n async #deleteLocations(rows: DbLocationsRow[]): Promise<void> {\n // Delete the location table entries (in chunks so as not to overload the\n // knex query builder)\n for (const ids of chunk(\n rows.map(l => l.id),\n 100,\n )) {\n await this.db<DbLocationsRow>('locations').whereIn('id', ids).delete();\n }\n\n // Delete the corresponding Location kind entities (this is efficiently\n // chunked internally in the catalog)\n await this.connection.applyMutation({\n type: 'delta',\n added: [],\n removed: rows.map(l => ({\n entity: locationSpecToLocationEntity({ location: l }),\n })),\n });\n }\n\n /**\n * Given a \"base\" URL prefix, find all locations that are for paths at or\n * below it.\n *\n * For example, given a base URL prefix of\n * \"https://github.com/backstage/backstage/blob/master/plugins\", it will match\n * locations inside the plugins directory, and nowhere else.\n */\n async #findLocationsByPrefixOrExactMatch(\n urls: Iterable<string>,\n ): Promise<Array<{ row: DbLocationsRow; parsed: GitUrl }>> {\n const result = new Array<{ row: DbLocationsRow; parsed: GitUrl }>();\n\n for (const url of urls) {\n let base: GitUrl;\n try {\n base = parseGitUrl(url);\n } catch (error) {\n throw new Error(`Invalid URL prefix, could not parse: ${url}`);\n }\n\n if (!base.owner || !base.name) {\n throw new Error(\n `Invalid URL prefix, missing owner or repository: ${url}`,\n );\n }\n\n const pathPrefix =\n base.filepath === '' || base.filepath.endsWith('/')\n ? base.filepath\n : `${base.filepath}/`;\n\n const rows = await this.db<DbLocationsRow>('locations')\n .where('type', '=', 'url')\n // Initial rough pruning to not have to go through them all\n .where('target', 'like', `%${base.owner}%`)\n .where('target', 'like', `%${base.name}%`)\n .select();\n\n result.push(\n ...rows.flatMap(row => {\n try {\n // We do this pretty explicit set of checks because we want to support\n // providers that have a URL format where the path isn't necessarily at\n // the end of the URL string (e.g. in the query part). Some of these may\n // be empty strings etc, but that's fine as long as they parse to the\n // same thing as above.\n const candidate = parseGitUrl(row.target);\n\n if (\n candidate.protocol === base.protocol &&\n candidate.resource === base.resource &&\n candidate.port === base.port &&\n candidate.organization === base.organization &&\n candidate.owner === base.owner &&\n candidate.name === base.name &&\n // If the base has no ref (for example didn't have the \"/blob/master\"\n // part and therefore targeted an entire repository) then we match any\n // ref below that\n (!base.ref || candidate.ref === base.ref) &&\n // Match both on exact equality and any subpath with a slash between\n (candidate.filepath === base.filepath ||\n candidate.filepath.startsWith(pathPrefix))\n ) {\n return [{ row, parsed: candidate }];\n }\n return [];\n } catch {\n return [];\n }\n }),\n );\n }\n\n return uniqBy(result, entry => entry.row.id);\n }\n\n // #endregion\n}\n\n/**\n * Recursively builds up the SQL expression corresponding to the given filter\n * predicate.\n *\n * @remarks\n *\n * Design note: The code prefers to let the SQL engine achieve case\n * insensitivity. We could attempt to use `.toUpperCase` etc on the client\n * side, but that would only work for the values being passed in, not the column\n * side of the expression. If we let the database perform UPPER on both, we know\n * that they will always be locale consistent etc as well.\n *\n * This does come at a runtime cost. However, the data set is typically rather\n * small in the grand scheme of things, and we can add the proper indices in the\n * future if needed. At this point I considered it not worth the effort.\n */\nfunction applyLocationFilterToQuery(\n clientType: string,\n inputQuery: Knex.QueryBuilder,\n query: FilterPredicate,\n): Knex.QueryBuilder {\n let result = inputQuery;\n\n if (!query || typeof query !== 'object' || Array.isArray(query)) {\n throw new InputError('Invalid filter predicate, expected an object');\n }\n\n if ('$all' in query) {\n // Explicitly handle the empty case to avoid malformed SQL\n if (query.$all.length === 0) {\n return result.whereRaw('1 = 0');\n }\n\n return result.where(outer => {\n for (const subQuery of query.$all) {\n outer.andWhere(inner => {\n applyLocationFilterToQuery(clientType, inner, subQuery);\n });\n }\n });\n }\n\n if ('$any' in query) {\n // Explicitly handle the empty case to avoid malformed SQL\n if (query.$any.length === 0) {\n return result.whereRaw('1 = 0');\n }\n\n return result.where(outer => {\n for (const subQuery of query.$any) {\n outer.orWhere(inner => {\n applyLocationFilterToQuery(clientType, inner, subQuery);\n });\n }\n });\n }\n\n if ('$not' in query) {\n return result.whereNot(inner => {\n applyLocationFilterToQuery(clientType, inner, query.$not);\n });\n }\n\n const entries = Object.entries(query);\n const keys = entries.map(e => e[0]);\n if (keys.some(k => k.startsWith('$'))) {\n throw new InputError(\n `Invalid filter predicate, unknown logic operator '${keys.join(', ')}'`,\n );\n }\n\n for (const [keyAnyCase, value] of entries) {\n const key = keyAnyCase.toLocaleLowerCase('en-US');\n if (!['id', 'type', 'target'].includes(key)) {\n throw new InputError(\n `Invalid filter predicate, expected key to be 'id', 'type', or 'target', got '${keyAnyCase}'`,\n );\n }\n\n result = applyFilterValueToQuery(clientType, result, key, value);\n }\n\n return result;\n}\n\nfunction applyFilterValueToQuery(\n clientType: string,\n result: Knex.QueryBuilder,\n key: string,\n value: FilterPredicateValue,\n): Knex.QueryBuilder {\n // Is it a primitive value?\n if (['string', 'number', 'boolean'].includes(typeof value)) {\n if (clientType === 'pg') {\n return result.whereRaw(`UPPER(??::text) = UPPER(?::text)`, [key, value]);\n }\n\n if (clientType.includes('mysql')) {\n return result.whereRaw(\n `UPPER(CAST(?? AS CHAR)) = UPPER(CAST(? AS CHAR))`,\n [key, value],\n );\n }\n\n return result.whereRaw(`UPPER(??) = UPPER(?)`, [key, value]);\n }\n\n // Is it a matcher object?\n if (typeof value === 'object') {\n if (!value || Array.isArray(value)) {\n throw new InputError(\n `Invalid filter predicate, got unknown matcher object '${JSON.stringify(\n value,\n )}'`,\n );\n }\n\n // Technically existence checks do not make much sense in the context of\n // this table at the time of writing (values are always present), but\n // there's nothing gained by prohibiting it.\n if ('$exists' in value) {\n return value.$exists ? result.whereNotNull(key) : result.whereNull(key);\n }\n\n if ('$in' in value) {\n // Explicitly handle the empty case to avoid malformed SQL\n if (value.$in.length === 0) {\n return result.whereRaw('1 = 0');\n }\n\n // The id is matched with plain equality; it's of UUID type and case\n // insensitivity does not apply.\n if (key === 'id') {\n return result.whereIn(key, value.$in);\n }\n\n if (clientType === 'pg') {\n const rhs = value.$in.map(() => 'UPPER(?::text)').join(', ');\n return result.whereRaw(`UPPER(??::text) IN (${rhs})`, [\n key,\n ...value.$in,\n ]);\n }\n\n if (clientType.includes('mysql')) {\n const rhs = value.$in.map(() => 'UPPER(CAST(? AS CHAR))').join(', ');\n return result.whereRaw(`UPPER(CAST(?? AS CHAR)) IN (${rhs})`, [\n key,\n ...value.$in,\n ]);\n }\n\n const rhs = value.$in.map(() => 'UPPER(?)').join(', ');\n return result.whereRaw(`UPPER(??) IN (${rhs})`, [key, ...value.$in]);\n }\n\n if ('$hasPrefix' in value) {\n const escaped = value.$hasPrefix.replace(/([\\\\%_])/g, '\\\\$1');\n\n if (clientType === 'pg') {\n return result.whereRaw(\"?? ilike ? escape '\\\\'\", [key, `${escaped}%`]);\n }\n\n if (clientType.includes('mysql')) {\n return result.whereRaw(\"UPPER(??) like UPPER(?) escape '\\\\\\\\'\", [\n key,\n `${escaped}%`,\n ]);\n }\n\n return result.whereRaw(\"UPPER(??) like UPPER(?) escape '\\\\'\", [\n key,\n `${escaped}%`,\n ]);\n }\n\n // There are no array shaped values for location queries, so we just always\n // fail here\n if ('$contains' in value) {\n return result.whereRaw('1 = 0');\n }\n\n throw new InputError(\n `Invalid filter predicate, got unknown matcher object '${JSON.stringify(\n value,\n )}'`,\n );\n }\n\n throw new InputError(\n `Invalid filter predicate, expected value to be a primitive value or a matcher object, got '${typeof value}'`,\n );\n}\n"],"names":["ConflictError","uuid","locationSpecToLocationEntity","getEntityLocationRef","stringifyEntityRef","NotFoundError","ANNOTATION_ORIGIN_LOCATION","parseLocationRef","chunk","parseGitUrl","uniqBy","InputError","rhs"],"mappings":"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"}
|