Skip to content
Closed
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
84 changes: 84 additions & 0 deletions openapi3.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,12 @@ paths:
directory: >-
/f9f92153-96e1-46fb-a44b-5c1b0955639c/c67ef7fb-a08c-41f8-ba4b-f2ba9a4b9a2a/
directory_layout: tms
sources: []
grids:
- epsg4326dir
format: image/png
upscale_tiles: 18
minimize_meta_requests: true
s3:
summary: s3 cache
value:
Expand All @@ -256,6 +262,12 @@ paths:
/5eedfc75-861c-42fc-81a9-ab2c0b95c274/d8527f15-5377-4a28-b5ce-92891d897aec/
directory_layout: tms
bucket_name: bucket-name
sources: []
grids:
- epsg4326dir
format: image/png
upscale_tiles: 18
minimize_meta_requests: true
redis:
summary: redis cache
value:
Expand All @@ -267,6 +279,25 @@ paths:
password: password
username: ''
default_ttl: 86400
sources:
- example-layer
grids:
- epsg4326dir
format: image/png
geopackage:
summary: geopackage cache
value:
cacheName: example-layer
cache:
type: geopackage
filename: /path/to/tiles.gpkg
table_name: tiles
sources: []
grids:
- epsg4326dir
format: image/png
upscale_tiles: 18
minimize_meta_requests: true
'400':
description: Not valid request
content:
Expand Down Expand Up @@ -423,25 +454,78 @@ components:
default_ttl:
type: integer
example: 86400
gpkgCache:
type: object
required:
- type
properties:
type:
type: string
enum:
- geopackage
filename:
type: string
description: >-
Path to the geopackage file. Defaults to cachename.gpkg. Ignored
when levels is true.
example: /path/to/tiles.gpkg
table_name:
type: string
description: >-
Table the tiles are stored in. Defaults to cachename_gridname.
example: tiles
levels:
type: boolean
description: >-
Store each level in a separate geopackage under directory. Defaults
to false.
directory:
type: string
description: Directory of the geopackage files, used only when levels is true.
getCacheResponse:
type: object
description: >-
A mapproxy cache, as it is written in the configuration, alongside its
name. Properties other than cacheName are the cache entry itself, so
options that are not listed here may also be present.
required:
- cacheName
- cache
properties:
cacheName:
type: string
example: example-layer
cache:
oneOf:
- $ref: '#/components/schemas/fileCache'
- $ref: '#/components/schemas/s3Cache'
- $ref: '#/components/schemas/redisCache'
- $ref: '#/components/schemas/gpkgCache'
discriminator:
propertyName: type
mapping:
file: '#/components/schemas/fileCache'
s3: '#/components/schemas/s3Cache'
redis: '#/components/schemas/redisCache'
geopackage: '#/components/schemas/gpkgCache'
sources:
type: array
items:
type: string
grids:
type: array
items:
type: string
example:
- epsg4326dir
format:
type: string
example: image/png
upscale_tiles:
type: number
example: 18
minimize_meta_requests:
type: boolean
getConfigResponse:
type: object
properties:
Expand Down
9 changes: 2 additions & 7 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 @@ -116,15 +116,10 @@ export interface IRedisSource extends ICacheSource {
default_ttl: number;
}

export interface ICacheName {
export interface IGetCacheResponse extends IMapProxyCache {
cacheName: string;
}

export interface ICacheObject {
cacheName: string;
cache: IRedisSource | IS3Source | IFSSource;
}

export interface IGpkgSource extends ICacheSource {
filename: string;
table_name: string;
Expand Down
16 changes: 15 additions & 1 deletion src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { promises as fsp } from 'node:fs';
import { safeLoad, safeDump, YAMLException } from 'js-yaml';
import { container } from 'tsyringe';
import { SERVICES } from '../common/constants';
import { IFSConfig, IMapProxyJsonDocument } from './interfaces';
import { IFSConfig, IMapProxyCache, IMapProxyJsonDocument } from './interfaces';
import { SourceTypes } from './enums';

// read mapproxy yaml config file and convert it into a json object
Expand Down Expand Up @@ -61,6 +61,20 @@ export function isLayerNameSuffixRedis(layerName: string): boolean {
return layerName.endsWith('-redis');
}

/**
* Check if an entry of the mapproxy 'caches' section is a well formed cache, holding a cache source.
* Hand edited configurations may hold entries that are not objects at all, or objects with no 'cache' section.
* @param cache entry taken from the mapproxy 'caches' section
* @return boolean - if the entry can be treated as a cache holding a cache source.
*/
export function isMapProxyCache(cache: unknown): cache is IMapProxyCache {
if (typeof cache !== 'object' || cache === null) {
return false;
}
const cacheSource: unknown = (cache as Record<string, unknown>).cache;
return typeof cacheSource === 'object' && cacheSource !== null;
}

export function adjustTilesPath(tilesPath: string, cacheSource: SourceTypes): string {
const fsConfig = container.resolve<IFSConfig>(SERVICES.FS);
switch (cacheSource) {
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
25 changes: 9 additions & 16 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 All @@ -29,7 +26,7 @@ import { FSSource } from '../../common/cacheProviders/fsSource';
import { isSourceType, SourceTypes, sourceTypeValues } from '../../common/enums';
import { RedisSource } from '../../common/cacheProviders/redisSource';
import { ConfigsManager } from '../../configs/models/configsManager';
import { getRedisCacheName, getRedisCacheOriginalName, isLayerNameSuffixRedis } from '../../common/utils';
import { getRedisCacheName, getRedisCacheOriginalName, isLayerNameSuffixRedis, isMapProxyCache } from '../../common/utils';

@injectable()
class LayersManager {
Expand All @@ -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,26 +63,22 @@ 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 = configJson.caches[cacheName];

if (currentSourceCache === undefined) {
// a missing entry, an entry that is not an object, or an entry with no cache source, are all unusable as a cache
if (!isMapProxyCache(requestedCache)) {
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);
}

type AvailableSources = IRedisSource | IS3Source | IFSSource;

return {
cacheName: cacheName,
cache: currentSourceCache.cache as AvailableSources,
};
// the whole cache is returned as it is written in the configuration, the cache name is ours and always wins
return { ...requestedCache, cacheName };
}

@withSpanAsyncV4
Expand Down
16 changes: 13 additions & 3 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 { IGetCacheResponse, 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,24 @@ 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', async () => {
const response = await requestSender.getLayersCache('mockLayerNameExists', 's3');

expect(response.status).toBe(httpStatusCodes.OK);

const resource = response.body as ICacheName;
const resource = response.body as IGetCacheResponse;
expect(response).toSatisfyApiSpec();
expect(resource.cacheName).toBe('mockLayerNameExists');
expect(resource).toStrictEqual({
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('Sad Path - should fail with response status 404 Not Found and layer name is not exists', async () => {
Expand Down
Loading
Loading