Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
57ea6d0
feat: implementing layer deletion from geoserver
almog8k Jun 28, 2026
ccf6e88
refactor: rename deleteLayer to unpublishLayer in GeoserverClient (MA…
almog8k Jun 28, 2026
27a9da0
feat: add deleteEntities to PolygonPartsMangerClient (MAPCO-7285)
almog8k Jun 28, 2026
e2099fa
feat: add deleteRecord to CatalogClient (MAPCO-7285)
almog8k Jun 28, 2026
7f3b301
feat: add removeLayer to MapproxyApiClient (MAPCO-7285)
almog8k Jun 28, 2026
f6c856b
refactor: split S3 bucket config into artifactsBucket and tilesBucket…
almog8k Jul 2, 2026
3d668f3
feat: add getS3CacheBucketName to MapproxyApiClient (MAPCO-7285)
almog8k Jul 2, 2026
13f9f92
refactor: extract polling task type selection by domain into getPolli…
almog8k Jul 5, 2026
4a0724a
feat: add Delete_Layer job and delete task schemas and handler interf…
almog8k Jul 5, 2026
3669685
feat: implement DeleteLayerHandler for full layer deletion flow (MAPC…
almog8k Jul 5, 2026
23ba16b
feat: register Delete_Layer job and delete task in polling and DI (MA…
almog8k Jul 5, 2026
e3b062b
feat: update raster-shared to version 8.3.0-alpha.0 in package.json a…
almog8k Jul 5, 2026
58bad6f
feat: update raster-shared to version 8.3.0-alpha.1, improved storage…
almog8k Jul 8, 2026
c896549
feat: derive tiles-deletion path from mapproxy cache directory (MAPCO…
almog8k Jul 8, 2026
c823889
Merge branch 'master' into feat/delete-layer-MAPCO-7285
almog8k Jul 8, 2026
00d9851
feat: update dependencies in package-lock.json for improved stability
almog8k Jul 8, 2026
2d45e1a
feat: remove duplicate import of NotFoundError in catalogClient.ts
almog8k Jul 8, 2026
2bdb9dc
feat: refactor layer cache retrieval methods in MapproxyApiClient
almog8k Jul 8, 2026
a31b872
feat: add delete layer job definitions and update config for ingestio…
almog8k Jul 11, 2026
dc18b37
feat: update unpublishLayer method to support recursive deletion in G…
almog8k Jul 11, 2026
5e93e56
feat: feat: update warning messages for not found errors in clients t…
almog8k Jul 14, 2026
8f3c3ac
refactor: rename getCacheName to getRedisCacheName in mapproxyClient …
almog8k Jul 14, 2026
f716873
refactor: error handling in removeLayer method to throw DeleteLayerEr…
almog8k Jul 14, 2026
3220dd8
refactor: move steps cosnt to the top and reuse it
almog8k Jul 14, 2026
af5dad9
fix: use block duplication on tiles deletion task
almog8k Jul 14, 2026
9e2716d
feat: update job processing to handle delete task type and add corres…
almog8k Jul 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,12 @@ dist
#helm
Chart.lock

.claude
.claude-flow
.swarm
.mcp.json
CLAUDE.md
/ai-docs
# ruflo / claude-flow vector memory store
ruvector.db
/docs
9 changes: 7 additions & 2 deletions config/custom-environment-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@
"accessKeyId": "S3_ACCESS_KEY_ID",
"secretAccessKey": "S3_SECRET_ACCESS_KEY",
"endpointUrl": "S3_ENDPOINT_URL",
"bucket": "S3_ARTIFACTS_BUCKET",
"artifactsBucket": "S3_ARTIFACTS_BUCKET",
"tilesBucket": "S3_TILES_BUCKET",
"sslEnabled": {
"__name": "S3_SSL_ENABLED",
"__format": "boolean"
Expand Down Expand Up @@ -124,7 +125,8 @@
"tasks": {
"createTasks": "POLLING_CREATE_TASKS_TASK",
"finalize": "POLLING_FINALIZE_TASK",
"init": "POLLING_INIT_TASK"
"init": "POLLING_INIT_TASK",
"delete": "POLLING_DELETE_TASK"
}
},
"ingestion": {
Expand All @@ -137,6 +139,9 @@
},
"swapUpdate": {
"type": "INGESTION_SWAP_UPDATE_JOB_TYPE"
},
"deleteLayer": {
"type": "INGESTION_DELETE_LAYER_JOB_TYPE"
}
},
"jobs": {
Expand Down
9 changes: 7 additions & 2 deletions config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
"accessKeyId": "minioadmin",
"secretAccessKey": "minioadmin",
"endpointUrl": "http://localhost:9000",
"bucket": "",
"artifactsBucket": "",
"tilesBucket": "",
"sslEnabled": false
},
"disableHttpClientLogs": true,
Expand Down Expand Up @@ -91,7 +92,8 @@
"tasks": {
"createTasks": "create-tasks",
"init": "init",
"finalize": "finalize"
"finalize": "finalize",
"delete": "delete"
}
},
"ingestion": {
Expand All @@ -104,6 +106,9 @@
},
"swapUpdate": {
"type": "Ingestion_Swap_Update"
},
"deleteLayer": {
"type": "Delete_Layer"
}
},
"jobs": {
Expand Down
5 changes: 5 additions & 0 deletions helm/templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ data:
S3_ARTIFACTS_BUCKET: {{ $storage.s3.artifactsBucket | quote }}
S3_SSL_ENABLED: {{ $storage.s3.sslEnabled | quote }}
{{- end }}
{{- if eq (upper $storage.tilesStorageProvider) "S3" }}
S3_TILES_BUCKET: {{ $storage.s3.tilesBucket | quote }}
{{- end }}
npm_config_cache: /tmp/

HTTP_RETRY_ATTEMPTS: {{ .Values.env.httpRetry.attempts | quote }}
Expand All @@ -49,9 +52,11 @@ data:
POLLING_CREATE_TASKS_TASK: {{ $jobDefinitions.tasks.createTasks.type | quote }}
POLLING_INIT_TASK: {{ $jobDefinitions.tasks.init.type | quote }}
POLLING_FINALIZE_TASK: {{ $jobDefinitions.tasks.finalize.type | quote }}
POLLING_DELETE_TASK: {{ $jobDefinitions.tasks.delete.type | quote }}
INGESTION_NEW_JOB_TYPE: {{ $jobDefinitions.jobs.new.type | quote }}
INGESTION_UPDATE_JOB_TYPE: {{ $jobDefinitions.jobs.update.type | quote }}
INGESTION_SWAP_UPDATE_JOB_TYPE: {{ $jobDefinitions.jobs.swapUpdate.type | quote }}
INGESTION_DELETE_LAYER_JOB_TYPE: {{ $jobDefinitions.jobs.deleteLayer.type | quote }}
INGESTION_SEED_JOB_TYPE : {{ $jobDefinitions.jobs.seed.type | quote }}
EXPORT_JOB_TYPE: {{ $jobDefinitions.jobs.export.type | quote }}
EXPORT_CLEANUP_EXPIRATION_DAYS: {{ $jobDefinitions.jobs.export.cleanupExpirationDays | quote }}
Expand Down
5 changes: 5 additions & 0 deletions helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ storage:
s3:
endpointUrl: ""
artifactsBucket: ""
tilesBucket: ""
sslEnabled: false
secretName: ""
fs:
Expand Down Expand Up @@ -137,6 +138,8 @@ jobDefinitions:
type: ""
swapUpdate:
type: ""
deleteLayer:
type: ""
seed:
type: ""
export:
Expand All @@ -150,6 +153,8 @@ jobDefinitions:
type: ""
finalize:
type: ""
delete:
type: ""
merge:
type: ""
tileBatchSize: 10000
Expand Down
22 changes: 11 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"@map-colonies/mc-priority-queue": "^9.1.2",
"@map-colonies/mc-utils": "^6.0.1",
"@map-colonies/prometheus": "^1.0.0",
"@map-colonies/raster-shared": "8.3.0-alpha.0",
"@map-colonies/raster-shared": "^8.3.0-alpha.1",
"@map-colonies/read-pkg": "^1.0.0",
"@map-colonies/schemas": "^1.18.0",
"@map-colonies/shapefile-reader": "^1.0.1",
Expand Down
8 changes: 8 additions & 0 deletions src/common/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ export class UpdateLayerError extends Error {
}
}

export class DeleteLayerError extends Error {
public constructor(targetClient: string, layerIdentifier: string, err: Error) {
super(`Failed to delete layer ${layerIdentifier} from ${targetClient} client: ${err.message}`);
this.name = DeleteLayerError.name;
this.stack = err.stack;
}
}

export class LayerNotFoundError extends Error {
public constructor(id: string) {
super(`Record with id ${id} not found`);
Expand Down
22 changes: 17 additions & 5 deletions src/common/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export interface IngestionPollingJobsConfig {
new: JobConfig | undefined;
update: JobConfig | undefined;
swapUpdate: JobConfig | undefined;
deleteLayer: JobConfig | undefined;
}

export interface ExportPollingJobsConfig {
Expand All @@ -90,6 +91,7 @@ export interface PollingTasks {
createTasks: string;
init: string;
finalize: string;
delete: string;
}

export interface PollingConfig {
Expand Down Expand Up @@ -137,9 +139,17 @@ export interface JobManagementConfig {
//#endregion config

//#region job/task interfaces
export interface IJobHandler<TInitJob = unknown, TInitTask = unknown, TFinalizeJob = unknown, TFinalizeTask = unknown> {
handleJobInit: (job: TInitJob, task: TInitTask) => Promise<void>;
handleJobFinalize: (job: TFinalizeJob, task: TFinalizeTask) => Promise<void>;
export interface IJobHandler<
TInitJob = unknown,
TInitTask = unknown,
TFinalizeJob = unknown,
TFinalizeTask = unknown,
TDeleteJob = unknown,
TDeleteTask = unknown,
> {
handleJobInit?: (job: TInitJob, task: TInitTask) => Promise<void>;
handleJobFinalize?: (job: TFinalizeJob, task: TFinalizeTask) => Promise<void>;
handleJobDelete?: (job: TDeleteJob, task: TDeleteTask) => Promise<void>;
}

export interface JobAndTaskResponse {
Expand Down Expand Up @@ -327,7 +337,8 @@ export interface GetMapproxyCacheRequest {

export interface GetMapproxyCacheResponse {
cacheName: string;
cache: { type: LayerCacheType };
// eslint-disable-next-line @typescript-eslint/naming-convention
cache: { type: LayerCacheType; directory?: string; directory_layout?: string; bucket_name?: string };
}
//#endregion mapproxyApi

Expand Down Expand Up @@ -412,7 +423,8 @@ export interface IS3Config {
accessKeyId: string;
secretAccessKey: string;
endpointUrl: string;
bucket: string;
artifactsBucket: string;
tilesBucket: string;
objectKey: string;
sslEnabled: boolean;
}
Expand Down
3 changes: 3 additions & 0 deletions src/containerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ExportJobHandler } from './job/models/export/exportJobHandler';
import { NewJobHandler } from './job/models/ingestion/newJobHandler';
import { SwapJobHandler } from './job/models/ingestion/swapJobHandler';
import { UpdateJobHandler } from './job/models/ingestion/updateJobHandler';
import { DeleteLayerHandler } from './job/models/deletion/deleteLayerHandler';
import { JOB_HANDLER_FACTORY_SYMBOL, jobHandlerFactory } from './job/models/jobHandlerFactory';
import { getPollingJobs, parseInstanceType, validateAndGetHandlersTokens } from './utils/configUtil';
import { productReaderFactory } from './utils/storage/productReader';
Expand Down Expand Up @@ -71,6 +72,8 @@ const registerInstanceHandlers = (instanceType: InstanceType, handlersTokens: Re
{ token: handlersTokens['Ingestion_Update']!, provider: { useClass: UpdateJobHandler } },

{ token: handlersTokens['Ingestion_Swap_Update']!, provider: { useClass: SwapJobHandler } },

{ token: handlersTokens['Delete_Layer']!, provider: { useClass: DeleteLayerHandler } },
];
case 'export':
return [{ token: handlersTokens['Export']!, provider: { useClass: ExportJobHandler } }];
Expand Down
29 changes: 28 additions & 1 deletion src/httpClients/catalogClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import type { Logger } from '@map-colonies/js-logger';
import { LayerNameFormats, PolygonPartsEntityName, type LayerName } from '@map-colonies/raster-shared';
import { HttpClient, type IHttpRetryConfig } from '@map-colonies/mc-utils';
import { type IRasterCatalogUpsertRequestBody, LayerMetadata, Link, PycswLayerCatalogRecord } from '@map-colonies/mc-model-types';
import { NotFoundError } from '@map-colonies/error-types';
import { RecordType } from '@map-colonies/types';
import { context, SpanStatusCode, trace, type Tracer } from '@opentelemetry/api';
import { inject, injectable } from 'tsyringe';
import type { IConfig, CatalogUpdateRequestBody, FindLayerResponse, FindLayerBody } from '../common/interfaces';
import { IngestionNewFinalizeJob, IngestionSwapUpdateFinalizeJob, IngestionUpdateFinalizeJob } from '../utils/zod/schemas/job.schema';
import { SERVICES } from '../common/constants';
import { internalIdSchema } from '../utils/zod/schemas/jobParameters.schema';
import { LayerNotFoundError, PublishLayerError, UpdateLayerError } from '../common/errors';
import { DeleteLayerError, LayerNotFoundError, PublishLayerError, UpdateLayerError } from '../common/errors';
import { LinkBuilder, type ILinkBuilderData } from '../utils/linkBuilder';
import { PolygonPartsMangerClient } from './polygonPartsMangerClient';

Expand Down Expand Up @@ -86,6 +87,32 @@ export class CatalogClient extends HttpClient {
});
}

public async deleteRecord(catalogId: string): Promise<void> {
await context.with(trace.setSpan(context.active(), this.tracer.startSpan(`${CatalogClient.name}.${this.deleteRecord.name}`)), async () => {
const activeSpan = trace.getActiveSpan();
activeSpan?.setAttribute('catalogId', catalogId);

try {
const url = `/records/${catalogId}`;
await this.delete(url);
activeSpan?.setStatus({ code: SpanStatusCode.OK, message: 'Catalog record deleted successfully' });
} catch (err) {
if (err instanceof NotFoundError) {
this.logger.warn({ msg: 'catalog record not found, skipping', catalogId });
activeSpan?.setStatus({ code: SpanStatusCode.OK, message: 'catalog record not found, skipping' });
return;
}
if (err instanceof Error) {
activeSpan?.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
activeSpan?.recordException(err);
throw new DeleteLayerError(this.targetService, catalogId, err);
}
} finally {
activeSpan?.end();
}
});
}

public async findLayer(id: string): Promise<FindLayerResponse> {
// eslint-disable-next-line @typescript-eslint/return-await
return await context.with(trace.setSpan(context.active(), this.tracer.startSpan(`${CatalogClient.name}.${this.findLayer.name}`)), async () => {
Expand Down
30 changes: 29 additions & 1 deletion src/httpClients/geoserverClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import type { LayerNameFormats } from '@map-colonies/raster-shared';
import { HttpClient, type IHttpRetryConfig } from '@map-colonies/mc-utils';
import { context, SpanStatusCode, trace, type Tracer } from '@opentelemetry/api';
import { inject, injectable } from 'tsyringe';
import { NotFoundError } from '@map-colonies/error-types';
import type { IConfig, InsertGeoserverRequest } from '../common/interfaces';
import { SERVICES } from '../common/constants';
import { PublishLayerError } from '../common/errors';
import { DeleteLayerError, PublishLayerError } from '../common/errors';

@injectable()
export class GeoserverClient extends HttpClient {
Expand Down Expand Up @@ -52,4 +53,31 @@ export class GeoserverClient extends HttpClient {
}
});
}

public async unpublishLayer(layerName: string): Promise<void> {
await context.with(trace.setSpan(context.active(), this.tracer.startSpan(`${GeoserverClient.name}.${this.unpublishLayer.name}`)), async () => {
const activeSpan = trace.getActiveSpan();
activeSpan?.setAttribute('layerName', layerName);

try {
const url = `/featureTypes/${this.workspace}/${this.dataStore}/${layerName}`;
await this.delete(url, { isRecursive: true });
activeSpan?.setStatus({ code: SpanStatusCode.OK, message: 'Layer unpublished successfully from geoserver' });
} catch (err) {
if (err instanceof NotFoundError) {
// already gone — unpublish is idempotent, a 404 on (re)run is success (§6)
this.logger.warn({ msg: 'Layer feature type not found in geoserver, skipping', layerName });
activeSpan?.setStatus({ code: SpanStatusCode.OK, message: 'Layer feature type not found in geoserver, skipping' });
return;
}
if (err instanceof Error) {
activeSpan?.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
activeSpan?.recordException(err);
throw new DeleteLayerError(this.targetService, layerName, err);
}
} finally {
activeSpan?.end();
}
});
}
}
Loading
Loading