Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
101 changes: 93 additions & 8 deletions openapi3.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,12 @@ paths:
summary: file cache
value:
cacheName: example-layer
sources: []
grids:
- epsg4326dir
format: image/png
upscale_tiles: 18
minimize_meta_requests: true
cache:
type: file
directory: >-
Expand All @@ -250,16 +256,43 @@ paths:
summary: s3 cache
value:
cacheName: example-layer
sources: []
grids:
- epsg4326dir
format: image/png
upscale_tiles: 18
minimize_meta_requests: true
cache:
type: s3
directory: >-
/5eedfc75-861c-42fc-81a9-ab2c0b95c274/d8527f15-5377-4a28-b5ce-92891d897aec/
directory_layout: tms
bucket_name: bucket-name
geopackage:
summary: geopackage cache
value:
cacheName: example-layer
sources: []
grids:
- epsg4326dir
format: image/png
upscale_tiles: 18
minimize_meta_requests: true
cache:
type: geopackage
filename: /path/to/tiles/directory/example-layer.gpkg
table_name: example-layer
redis:
summary: redis cache
summary: >-
redis cache, resolved by the '-redis' suffix. It carries no
upscale_tiles and no minimize_meta_requests.
value:
cacheName: example-layer-redis
sources:
- example-layer
grids:
- epsg4326dir
format: image/png
cache:
host: mapproxy-redis-master
port: 6379
Expand Down Expand Up @@ -355,10 +388,12 @@ components:
- image/jpeg
fileCache:
type: object
description: >-
A file cache source. Only `type` is guaranteed; a cache source missing an
optional field is still a valid response.
required:
- type
- directory
- directory_layout
additionalProperties: true
properties:
type:
type: string
Expand All @@ -373,10 +408,12 @@ components:
example: tms
s3Cache:
type: object
description: >-
An s3 cache source. Only `type` is guaranteed; a cache source missing an
optional field is still a valid response.
required:
- type
- directory
- directory_layout
additionalProperties: true
properties:
type:
type: string
Expand All @@ -396,11 +433,12 @@ components:
example: bucket-name
redisCache:
type: object
description: >-
A redis cache source. Only `type` is guaranteed; a cache source missing an
optional field is still a valid response.
required:
- type
- host
- port
- default_ttl
additionalProperties: true
properties:
type:
type: string
Expand All @@ -423,25 +461,72 @@ components:
default_ttl:
type: integer
example: 86400
geopackageCache:
type: object
description: >-
A geopackage cache source. Only `type` is guaranteed; a cache source missing
an optional field is still a valid response.
required:
- type
additionalProperties: true
properties:
type:
type: string
enum:
- geopackage
filename:
type: string
example: /path/to/tiles/directory/amsterdam_5cm.gpkg
table_name:
type: string
example: amsterdam_5cm
getCacheResponse:
type: object
description: >-
The whole cache as written in the mapproxy configuration, alongside its name.
Only `cacheName` and `cache` are guaranteed: the cache is returned verbatim, so
mapproxy options this service does not model are present too, and options a cache
genuinely lacks are absent rather than null or defaulted.
required:
- cacheName
- cache
additionalProperties: true
properties:
cacheName:
type: string
description: >-
The resolved cache name, which for a redis request is the '-redis' suffixed
name rather than the requested layer name.
example: example-layer
sources:
type: array
items:
type: string
grids:
type: array
items:
type: string
format:
type: string
example: image/png
upscale_tiles:
type: number
example: 18
minimize_meta_requests:
type: boolean
cache:
oneOf:
- $ref: '#/components/schemas/fileCache'
- $ref: '#/components/schemas/s3Cache'
- $ref: '#/components/schemas/redisCache'
- $ref: '#/components/schemas/geopackageCache'
discriminator:
propertyName: type
mapping:
file: '#/components/schemas/fileCache'
s3: '#/components/schemas/s3Cache'
redis: '#/components/schemas/redisCache'
geopackage: '#/components/schemas/geopackageCache'
getConfigResponse:
type: object
properties:
Expand Down
9 changes: 3 additions & 6 deletions src/common/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export interface IRedisConfig {
export interface IMapProxyJsonDocument {
services: JsonObject;
layers: IMapProxyLayer[];
caches: IMapProxyCache;
caches: Record<string, IMapProxyCache>;
grids: JsonObject;
globals: IMapProxyGlobalConfig;
}
Expand Down Expand Up @@ -120,10 +120,7 @@ export interface ICacheName {
cacheName: string;
}

export interface ICacheObject {
cacheName: string;
cache: IRedisSource | IS3Source | IFSSource;
}
export type IGetCacheResponse = ICacheName & Pick<IMapProxyCache, 'cache'> & Record<string, unknown>;

export interface IGpkgSource extends ICacheSource {
filename: string;
Expand All @@ -142,7 +139,7 @@ export interface IMapProxyCache {
grids: string[];
format: string;
upscale_tiles?: number;
cache: ICacheSource;
cache?: ICacheSource;
minimize_meta_requests?: boolean;
}

Expand Down
4 changes: 2 additions & 2 deletions src/layers/controllers/layersController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import type { RequestHandler } from 'express';
import httpStatus from 'http-status-codes';
import { injectable, inject } from 'tsyringe';
import { SERVICES } from '../../common/constants';
import type { ICacheName, ILayerPostRequest, IMapProxyCache } from '../../common/interfaces';
import type { IGetCacheResponse, ILayerPostRequest, IMapProxyCache } from '../../common/interfaces';
import { LayersManager } from '../models/layersManager';

type CreateLayerHandler = RequestHandler<undefined, ILayerPostRequest, ILayerPostRequest>;
type GetLayerHandler = RequestHandler<{ name: string }, IMapProxyCache, IMapProxyCache>;
type GetCacheHandler = RequestHandler<{ layerName: string; cacheType: string }, ICacheName>;
type GetCacheHandler = RequestHandler<{ layerName: string; cacheType: string }, IGetCacheResponse>;
type UpdateLayerHandler = RequestHandler<{ name: string }, ILayerPostRequest, ILayerPostRequest>;
type DeleteLayerHandler = RequestHandler<undefined, string[] | void, undefined, { layerNames: string[] }>;
@injectable()
Expand Down
23 changes: 9 additions & 14 deletions src/layers/models/layersManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ import type {
ICacheProvider,
ICacheSource,
IRedisConfig,
ICacheObject,
IRedisSource,
IS3Source,
IFSSource,
IGetCacheResponse,
} from '../../common/interfaces';
import { isLayerNameExists } from '../../common/validations/isLayerNameExists';
import { S3Source } from '../../common/cacheProviders/S3Source';
Expand Down Expand Up @@ -54,7 +51,7 @@ class LayersManager {
}

@withSpanAsyncV4
public async getCacheByNameAndType(layerName: string, cacheType: string): Promise<ICacheObject> {
public async getCacheByNameAndType(layerName: string, cacheType: string): Promise<IGetCacheResponse> {
const configJson = await this.configProvider.getJson();
const requestedLayer = configJson.layers.find((layer) => layer.name === layerName);

Expand All @@ -66,25 +63,23 @@ class LayersManager {

// our current only real cache layer, other caches cases are known as the source layers
const cacheName = isSourceType(cacheType) && cacheType === SourceTypes.REDIS ? getRedisCacheName(layerName) : layerName;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const currentSourceCache: IMapProxyCache | undefined = configJson.caches[cacheName];
const requestedCache: IMapProxyCache | undefined = configJson.caches[cacheName];

if (currentSourceCache === undefined) {
if (requestedCache === undefined) {
const errorMsg = `cache not found for ${layerName} layer`;
this.logger.warn({ msg: errorMsg, layerName, cacheType });
throw new NotFoundError(errorMsg);
}
if (currentSourceCache.cache.type !== cacheType) {

if (requestedCache.cache?.type !== cacheType) {
const errorMsg = `${layerName} layer cache not found with requested cache type: ${cacheType}`;
this.logger.warn({ msg: errorMsg, layerName, cacheType });
throw new BadRequestError(errorMsg);
this.logger.warn({ msg: errorMsg, layerName, cacheType, requestedCache });
throw new NotFoundError(errorMsg);
}

type AvailableSources = IRedisSource | IS3Source | IFSSource;

return {
cacheName: cacheName,
cache: currentSourceCache.cache as AvailableSources,
...requestedCache,
};
}

Expand Down
88 changes: 84 additions & 4 deletions tests/integration/layers/layersManager.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { promises as fsp } from 'node:fs';
import httpStatusCodes from 'http-status-codes';
import { container } from 'tsyringe';
import { ICacheName, ILayerPostRequest, IMapProxyCache } from '../../../src/common/interfaces';
import { ILayerPostRequest, IMapProxyCache } from '../../../src/common/interfaces';
import { mockLayerNameIsNotExists } from '../../unit/mock/mockLayerNameIsNotExists';
import { mockLayerNameAlreadyExists } from '../../unit/mock/mockLayerNameAlreadyExists';
import { init as configProviderInit, updateJsonMock } from '../../unit/mock/mockConfigProvider';
Expand Down Expand Up @@ -65,14 +65,53 @@ describe('layerManager', () => {
});

describe('#getLayersCache', () => {
it('Happy Path - should return status 200 and the cacheName', async () => {
it('Happy Path - should return status 200 and the whole Cache of an s3 Cache', async () => {
const response = await requestSender.getLayersCache('mockLayerNameExists', 's3');

expect(response.status).toBe(httpStatusCodes.OK);
expect(response).toSatisfyApiSpec();
expect(response.body).toEqual({
cacheName: 'mockLayerNameExists',
sources: [],
grids: ['epsg4326dir'],
format: 'image/png',
// eslint-disable-next-line @typescript-eslint/naming-convention
upscale_tiles: 18,
// eslint-disable-next-line @typescript-eslint/naming-convention
cache: { type: 's3', directory: '/path/to/s3/directory/tile', directory_layout: 'tms' },
});
});

it('Happy Path - should resolve a redis request to the -redis Cache and return it whole', async () => {
const response = await requestSender.getLayersCache('redisExists', 'redis');

expect(response.status).toBe(httpStatusCodes.OK);
expect(response).toSatisfyApiSpec();
expect(response.body).toEqual({
cacheName: 'redisExists-redis',
sources: ['redisExists'],
grids: ['epsg4326dir'],
format: 'image/png',
cache: {
host: 'raster-mapproxy-redis-master',
port: 6379,
username: 'mapcolonies',
password: 'mapcolonies',
prefix: 'mcrl:',
type: 'redis',
// eslint-disable-next-line @typescript-eslint/naming-convention
default_ttl: 86400,
},
});
});

it('Happy Path - should return mapproxy options this service does not model, verbatim', async () => {
const response = await requestSender.getLayersCache('NameIsAlreadyExists', 's3');

const resource = response.body as ICacheName;
expect(response.status).toBe(httpStatusCodes.OK);
expect(response).toSatisfyApiSpec();
expect(resource.cacheName).toBe('mockLayerNameExists');
expect(response.body).toHaveProperty('link_single_color_images', true);
expect(response.body).toHaveProperty('cache.region', 'us-east-1');
});

it('Sad Path - should fail with response status 404 Not Found and layer name is not exists', async () => {
Expand All @@ -85,6 +124,47 @@ describe('layerManager', () => {
expect(response.body).toEqual({ message: notFoundErrorMessage });
});

it('Sad Path - should fail with response status 404 when the Cache is of another Cache Type', async () => {
const mockLayerName = 'mockLayerNameExists';
const cacheType = 'file';
const response = await requestSender.getLayersCache(mockLayerName, cacheType);
const notFoundErrorMessage = `${mockLayerName} layer cache not found with requested cache type: ${cacheType}`;

expect(response).toSatisfyApiSpec();
expect(response.status).toBe(httpStatusCodes.NOT_FOUND);
expect(response.body).toEqual({ message: notFoundErrorMessage });
});

it('Sad Path - should fail with response status 404 when the Layer has no Cache under the resolved name', async () => {
const mockLayerName = 'noCacheForLayer';
const response = await requestSender.getLayersCache(mockLayerName, 's3');
const notFoundErrorMessage = `cache not found for ${mockLayerName} layer`;

expect(response).toSatisfyApiSpec();
expect(response.status).toBe(httpStatusCodes.NOT_FOUND);
expect(response.body).toEqual({ message: notFoundErrorMessage });
});

it('Sad Path - should fail with response status 404 when the configuration entry is not an object', async () => {
const mockLayerName = 'mock';
const response = await requestSender.getLayersCache(mockLayerName, 's3');
const notFoundErrorMessage = `${mockLayerName} layer cache not found with requested cache type: s3`;

expect(response).toSatisfyApiSpec();
expect(response.status).toBe(httpStatusCodes.NOT_FOUND);
expect(response.body).toEqual({ message: notFoundErrorMessage });
});

it('Sad Path - should fail with response status 404 when the configuration entry holds no Cache Source', async () => {
const mockLayerName = 'combined_layers';
const response = await requestSender.getLayersCache(mockLayerName, 's3');
const notFoundErrorMessage = `${mockLayerName} layer cache not found with requested cache type: s3`;

expect(response).toSatisfyApiSpec();
expect(response.status).toBe(httpStatusCodes.NOT_FOUND);
expect(response.body).toEqual({ message: notFoundErrorMessage });
});

it('Sad Path - should fail with error not valid type format', async () => {
const mockLayerName = 'mockLayerNameIsExists';
const cacheType = 'notValid';
Expand Down
Loading
Loading