Skip to content

Commit c58ffb4

Browse files
Typescript Migration: Tests (#1095)
* feat: convert errors.js to typescript fix: fix issue with ava test runner not identifying the error typescript file and finding temporary workaround during migration. * feat: creating base STAC types fix: updating linter overrides fro typescript * feat: convert geo-utils and logger to typescript feat: use BBox type from geojson lib * feat: typing stac-utils fix: tweaking types based on experience in stac-utils.ts * feat: deleted s3-utils.js which was slightly different from s3-utils.ts. Confirmed using s3-utils.ts passes all tests * feat: rounding out typing for s3-utils.ts * feat: typing sns file * feat: typing aws clients * feat: converting database clients to typescript * complete typing of asset-proxy and asset-buckets files tests: flesh out input object in test to be a more complete StacItem to pass an expected logic gate * feat: first quick pass at typing Searchparams and DbOperation * feat: updating changelog * logger: change to 'info' not 'error' in case of index already existing * feat: typing database.ts and updating types as required * feat: cleaning up types, remove unused, consolidating where appropriate * updating changelog * feat: typing a few remainig untyped params * feat: moved some utils to api-utils.ts and typed them feat: changed api.js to api.ts * feat: typing api.ts, adjusting types as needed * feat: adding missing type, adding return type in a few spots where it was missing * updating changelog * PR feedback * feat: typing ingest lambda and related files, adjusting types as needed * converting lambdas to typescript * feat: finishing typing lambdas, extending base express Request type, adding types as necessary * updating changelog * feat: converting test helpers to typescript * typing unit tests * feat: tweaking package and linter rules for typesript test conversion feat: some of the LLM converted tests * feat: rest of the system tests converted to typescript * fix: PR feedback * merge changes from api PR * PR feedback changs after reviewing Claude suggestions * updating .nsprc file * feat: partial commit of typed tests after setting config to typecheck tests as well * feat: fixing typing issues from removing implictAny: true setting * feat: more tidying up of tests typescript conversion, linter fixes * linter fix --------- Co-authored-by: Matthew Hanson <matt.a.hanson@gmail.com>
1 parent fc239e4 commit c58ffb4

77 files changed

Lines changed: 4511 additions & 4516 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.eslintrc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,8 @@
154154
"overrides": [
155155
{
156156
"files": [
157-
"test-*.js"
157+
"test-*.js",
158+
"test-*.ts"
158159
],
159160
"rules": {
160161
"max-len": [

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"test:system:coverage": "c8 npm run test:system",
2222
"test:unit": "NODE_OPTIONS='--import=tsx' ava tests/unit/*.[tj]s --no-worker-threads",
2323
"test:unit:coverage": "c8 npm run test:unit",
24-
"typecheck": "tsc",
24+
"typecheck": "tsc && tsc -p tsconfig.test.json",
2525
"audit-prod": "npx better-npm-audit audit --production",
2626
"audit": "npx better-npm-audit audit",
2727
"deploy": "sls deploy",
@@ -33,7 +33,10 @@
3333
},
3434
"ava": {
3535
"verbose": true,
36-
"timeout": "1m"
36+
"timeout": "1m",
37+
"extensions": [
38+
"ts"
39+
]
3740
},
3841
"publishConfig": {
3942
"access": "public"

src/lambdas/api/app.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// @ts-nocheck
21
import cors from 'cors'
32
import createError, { HttpError } from 'http-errors'
43
import express, { Request, Response, NextFunction } from 'express'

src/lambdas/api/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const internalServerError: Readonly<APIGatewayProxyResult> = Object.freeze({
2525
'content-type': 'text/plain'
2626
},
2727
body: 'Internal Server Error'
28-
})
28+
}) as APIGatewayProxyResult
2929

3030
let appInstance: express.Express | undefined
3131

src/lib/api-utils.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,19 @@ export const extractLimit = function (params: APIParameters): number {
3939
return DEFAULT_LIMIT
4040
}
4141

42+
type PrecisionField =
43+
| 'grid_geohash_frequency_precision'
44+
| 'grid_geohex_frequency_precision'
45+
| 'grid_geotile_frequency_precision'
46+
| 'centroid_geohash_grid_frequency_precision'
47+
| 'centroid_geohex_grid_frequency_precision'
48+
| 'centroid_geotile_grid_frequency_precision'
49+
| 'geometry_geohash_grid_frequency_precision'
50+
| 'geometry_geotile_grid_frequency_precision'
51+
4252
export const extractPrecision = function (
4353
params: APIParameters,
44-
name: string,
54+
name: PrecisionField,
4555
min: number,
4656
max: number
4757
): number {
@@ -435,7 +445,8 @@ export const isCollectionIdAllowed = function (
435445
|| allowedCollectionIds.includes('*')
436446
}
437447

438-
export const extractSortby = function (params: APIParameters): string[] {
448+
export const extractSortby = function (params: APIParameters):
449+
string[] | { field: string; direction: string; }[] | undefined {
439450
let sortbyRules
440451
const { sortby } = params
441452
if (sortby) {

src/lib/api.ts

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -284,15 +284,14 @@ const wrapResponseInFeatureCollection = function (
284284
numberMatched,
285285
numberReturned,
286286
features,
287-
links
288-
}
289-
290-
if (process.env['ENABLE_CONTEXT_EXTENSION']) {
291-
fc['context'] = {
292-
matched: numberMatched,
293-
returned: numberReturned,
294-
limit
295-
}
287+
links,
288+
...(process.env['ENABLE_CONTEXT_EXTENSION'] ? {
289+
context: {
290+
matched: numberMatched,
291+
returned: numberReturned,
292+
limit
293+
}
294+
} : {})
296295
}
297296

298297
return fc
@@ -307,23 +306,24 @@ const buildPaginationLinks = function (
307306
filter: Cql2Filter,
308307
endpoint: string,
309308
httpMethod: string,
310-
_sortby: string[],
309+
_sortby: string[] | { field: string; direction: string; }[] | undefined,
311310
items: StacItem[],
312-
lastItemSort: string | null
311+
lastItemSort: string | null | undefined
313312
): Link[] {
314313
if (items.length) {
315-
const dictToURI = (dict: Partial<APIParameters>) => (
314+
const dictToURI = (dict: Record<string, unknown>) => (
316315
Object.keys(dict).map(
317316
(p) => {
318317
let value = dict[p]
319318
if (typeof value === 'object' && value !== null) {
320319
if (p === 'sortby') {
321320
const sortFields: string[] = []
322-
for (let i = 0; i < value.length; i += 1) {
323-
if (value[i]['direction'] === 'asc') {
324-
sortFields.push(value[i]['field'])
321+
const sortArray = value as Array<{ field: string; direction: string }>
322+
for (let i = 0; i < sortArray.length; i += 1) {
323+
if (sortArray[i]!['direction'] === 'asc') {
324+
sortFields.push(sortArray[i]!['field'])
325325
} else {
326-
sortFields.push('-'.concat(value[i]['field']))
326+
sortFields.push('-'.concat(sortArray[i]!['field']))
327327
}
328328
}
329329
value = sortFields.join(',')
@@ -333,7 +333,7 @@ const buildPaginationLinks = function (
333333
value = JSON.stringify(value)
334334
}
335335
}
336-
const query = encodeURIComponent(value)
336+
const query = encodeURIComponent(String(value))
337337
return `${encodeURIComponent(p)}=${query}`
338338
}
339339
).join('&')
@@ -367,7 +367,7 @@ const buildPaginationLinks = function (
367367
const searchItems = async function (
368368
backend: Backend,
369369
httpMethod: string,
370-
collectionId: string,
370+
collectionId: string | null,
371371
endpoint: string,
372372
parameters: APIParameters,
373373
headers: IncomingHttpHeaders
@@ -498,7 +498,7 @@ const searchItems = async function (
498498
links.push({
499499
rel: 'collection',
500500
type: 'application/json',
501-
href: collectionEndpoint
501+
href: collectionEndpoint!
502502
})
503503
}
504504

@@ -537,7 +537,7 @@ const agg = function (
537537
const aggregate = async function (
538538
backend: Backend,
539539
httpMethod: string,
540-
collectionId: string,
540+
collectionId: string | null,
541541
endpoint: string,
542542
parameters: APIParameters,
543543
headers: IncomingHttpHeaders
@@ -1083,19 +1083,19 @@ const getCollections = async function (
10831083
title: 'Root Catalog'
10841084
},
10851085
],
1086+
...(process.env['ENABLE_CONTEXT_EXTENSION'] ? {
1087+
context: {
1088+
page: 1,
1089+
limit: COLLECTION_LIMIT,
1090+
matched: collections.length,
1091+
returned: collections.length
1092+
}
1093+
} : {})
10861094
}
1087-
1088-
// note: adding this to the Collections response is not
1095+
// note: adding 'context' this to the Collections response is not
10891096
// part of the Context Extension, and was just a proprietary
10901097
// behavior of this implemenation
1091-
if (process.env['ENABLE_CONTEXT_EXTENSION']) {
1092-
resp['context'] = {
1093-
page: 1,
1094-
limit: COLLECTION_LIMIT,
1095-
matched: collections && collections.length,
1096-
returned: collections && collections.length
1097-
}
1098-
}
1098+
10991099
return resp
11001100
}
11011101

src/lib/asset-buckets.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export class AssetBuckets {
3131
* @param {string} bucketOption - Bucket option (NONE, ALL, ALL_BUCKETS_IN_ACCOUNT, LIST)
3232
* @param {string[]|null} bucketNames - Array of bucket names (required for LIST option)
3333
*/
34-
constructor(bucketOption, bucketNames) {
34+
constructor(bucketOption: string, bucketNames: string[] | null) {
3535
this.bucketOption = bucketOption
3636
this.bucketNames = bucketNames
3737
this.bucketCache = {}
@@ -42,7 +42,7 @@ export class AssetBuckets {
4242
* @param {string[]|null} bucketNames - Array of bucket names (required for LIST option)
4343
* @returns {Promise<AssetBuckets>} Initialized AssetBuckets instance
4444
*/
45-
static async create(bucketOption, bucketNames) {
45+
static async create(bucketOption: string, bucketNames: string[] | null) {
4646
const instance = new AssetBuckets(bucketOption, bucketNames)
4747
await instance._initBuckets()
4848
return instance

src/lib/asset-proxy.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,11 @@ export class AssetProxy {
4343
* @param {number} urlExpiry - Pre-signed URL expiry time in seconds
4444
* @param {string} bucketOption - Bucket option (NONE, ALL, ALL_BUCKETS_IN_ACCOUNT, LIST)
4545
*/
46-
constructor(buckets, urlExpiry, bucketOption) {
46+
constructor(
47+
buckets: AssetBuckets,
48+
urlExpiry: number,
49+
bucketOption: string
50+
) {
4751
this.buckets = buckets
4852
this.urlExpiry = urlExpiry
4953
this.isEnabled = bucketOption !== BucketOptionEnum.NONE
@@ -81,7 +85,7 @@ export class AssetProxy {
8185
collectionId: string | null,
8286
itemId: string | null
8387
): {assets: Assets, wasProxied: boolean} {
84-
const proxiedAssets = {}
88+
const proxiedAssets: Assets = {}
8589
let wasProxied = false
8690

8791
for (const [assetKey, asset] of Object.entries(assets)) {

src/lib/database.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@ import { bboxToPolygon } from './geo-utils.js'
99
import {
1010
Cql2Filter,
1111
Cql2Value,
12-
DateQuery,
1312
DateTimeRange,
1413
DbQueryParameters,
1514
FieldsFilter,
1615
ItemProperties,
1716
OpenSearchBody,
1817
OpenSearchFilterQuery,
18+
OpValue,
1919
PartialItemUpdate,
20+
QueryOperators,
2021
QueryParameters,
2122
RangeQuery,
2223
SearchParameters,
@@ -34,7 +35,7 @@ variable which is the URL to the search database host line
3435

3536
const COLLECTIONS_INDEX = process.env['COLLECTIONS_INDEX'] || 'collections'
3637
const DEFAULT_INDICES = ['*', '-.*', '-collections']
37-
const OP = {
38+
export const OP = {
3839
AND: 'and',
3940
OR: 'or',
4041
NOT: 'not',
@@ -49,7 +50,8 @@ const OP = {
4950
BETWEEN: 'between',
5051
LIKE: 'like',
5152
S_INTERSECTS: 's_intersects',
52-
}
53+
} as const
54+
5355
const RANGE_TRANSLATION = {
5456
'<': 'lt',
5557
'<=': 'lte',
@@ -80,7 +82,7 @@ const MAX_COLLECTIONS_IN_QUERY_PATH = 10
8082
let collectionToIndexMapping: Record<string, string> | null = null
8183
let unrestrictedIndices: string[] | null = null
8284

83-
export const isIndexNotFoundError = (e) => (
85+
export const isIndexNotFoundError = (e: unknown) => (
8486
e instanceof Error
8587
&& e.name === 'ResponseError'
8688
&& e.message.includes('index_not_found_exception'))
@@ -129,7 +131,7 @@ function buildRangeQuery(
129131
* assumes a valid RFC3339 datetime or interval
130132
* validation was previously done by api.extractDatetime
131133
*/
132-
export function buildDatetimeQuery(parameters: QueryParameters): DateQuery {
134+
export function buildDatetimeQuery(parameters: QueryParameters): OpenSearchFilterQuery | undefined {
133135
let dateQuery
134136
const { datetime } = parameters
135137
if (datetime) {
@@ -140,7 +142,7 @@ export function buildDatetimeQuery(parameters: QueryParameters): DateQuery {
140142
if (end && end !== '..') datetimeRange.lte = end
141143
dateQuery = {
142144
range: {
143-
'properties.datetime': datetimeRange
145+
'properties.datetime': datetimeRange as Record<string, unknown>
144146
}
145147
}
146148
} else {
@@ -244,7 +246,7 @@ function sIntersects(
244246
/**
245247
* build the opensearch property queries
246248
*/
247-
function buildQueryExtQuery(query: QueryParameters): OpenSearchFilterQuery {
249+
function buildQueryExtQuery(query: Record<string, QueryOperators>): OpenSearchFilterQuery {
248250
const eq = 'eq'
249251
const inop = 'in'
250252
const startsWith = 'startsWith'
@@ -260,6 +262,7 @@ function buildQueryExtQuery(query: QueryParameters): OpenSearchFilterQuery {
260262
property: string
261263
) => {
262264
const operatorsObject = query[property]
265+
if (!operatorsObject) return accumulator
263266
const operators = Object.keys(operatorsObject)
264267

265268
// eq
@@ -275,7 +278,7 @@ function buildQueryExtQuery(query: QueryParameters): OpenSearchFilterQuery {
275278
if (operators.includes(inop)) {
276279
accumulator.push({
277280
terms: {
278-
[`properties.${property}`]: operatorsObject.in
281+
[`properties.${property}`]: operatorsObject.in!
279282
}
280283
})
281284
}
@@ -329,6 +332,7 @@ function buildQueryExtQuery(query: QueryParameters): OpenSearchFilterQuery {
329332
property: string
330333
) => {
331334
const operatorsObject = query[property]
335+
if (!operatorsObject) return accumulator
332336
const operators = Object.keys(operatorsObject)
333337

334338
// neq
@@ -435,7 +439,7 @@ function buildLeafFilter(filter: Cql2Filter): OpenSearchFilterQuery {
435439

436440
// Routes to recursive or leaf handler based on operator
437441
function buildFilterExtQuery(filter: Cql2Filter): OpenSearchFilterQuery {
438-
const RECURSIVE_OPS = [OP.AND, OP.OR, OP.NOT]
442+
const RECURSIVE_OPS: OpValue[] = [OP.AND, OP.OR, OP.NOT]
439443
if (RECURSIVE_OPS.includes(filter.op)) {
440444
return buildRecursiveFilter(filter)
441445
}
@@ -629,7 +633,7 @@ function buildSearchAfter(parameters: QueryParameters): string[] | undefined {
629633
* a. null
630634
* b. an empty array
631635
*/
632-
function fieldsParamIsEmpty(fieldsSpec: object, paramName: string): boolean {
636+
function fieldsParamIsEmpty(fieldsSpec: Record<string, unknown>, paramName: string): boolean {
633637
return fieldsSpec.hasOwnProperty(paramName)
634638
&& (fieldsSpec[paramName] === null
635639
|| (Array.isArray(fieldsSpec[paramName]) && !fieldsSpec[paramName].length))
@@ -812,6 +816,7 @@ async function getCollections(
812816
size: limit,
813817
from: (page - 1) * limit
814818
})
819+
// @ts-ignore -- OpenSearch response body is of unknown shape
815820
return response.body['hits'].hits.map((r) => (r._source))
816821
} catch (e) {
817822
logger.error('Failure getting collections, maybe none exist?', e)
@@ -897,7 +902,7 @@ export async function constructSearchParams(
897902
if (!unrestrictedIndices) {
898903
await populateUnrestrictedIndices()
899904
}
900-
indices = unrestrictedIndices
905+
indices = unrestrictedIndices!
901906
}
902907
// hash indices
903908
indices = indices.map((index) => {
@@ -948,6 +953,7 @@ async function search(
948953
})
949954

950955
const hits = dbResponse.body['hits'].hits
956+
// @ts-ignore -- OpenSearch response body is of unknown shape
951957
const results = hits.map((r) => (r._source))
952958
const lastItem = hits.at(-1)
953959
let lastItemSort = null
@@ -1039,9 +1045,9 @@ async function aggregate(
10391045
// include all aggregations specified
10401046
// this will ignore aggregations with the wrong names
10411047
searchParams.body.aggs = Object.keys(ALL_AGGREGATIONS).reduce((o, k) => {
1042-
if (aggregations.includes(k)) o[k] = ALL_AGGREGATIONS[k]
1048+
if (aggregations.includes(k)) o[k] = ALL_AGGREGATIONS[k as keyof typeof ALL_AGGREGATIONS]
10431049
return o
1044-
}, {})
1050+
}, {} as Record<string, unknown>)
10451051

10461052
// deprecated centroid
10471053

0 commit comments

Comments
 (0)