@aspan-corporation/ac-shared 1.2.25 → 1.2.27
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/lib/services/dynamoDB.d.ts +4 -1
- package/lib/services/dynamoDB.js +4 -1
- package/lib/services/s3.d.ts +7 -0
- package/lib/services/s3.js +23 -2
- package/lib/services/ssm.d.ts +6 -4
- package/lib/services/ssm.js +12 -1
- package/lib/utils/index.d.ts +1 -0
- package/lib/utils/index.js +1 -0
- package/lib/utils/parseFolderDate.d.ts +29 -0
- package/lib/utils/parseFolderDate.js +25 -0
- package/lib/utils/processMeta.d.ts +11 -0
- package/lib/utils/processMeta.js +42 -31
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
2
2
|
import { AssumeRoleCommandOutput } from "@aws-sdk/client-sts";
|
|
3
|
-
import { DynamoDBDocumentClient, GetCommandInput, GetCommandOutput, PutCommandInput, PutCommandOutput, QueryCommandInput, QueryCommandOutput, UpdateCommandInput, UpdateCommandOutput, ScanCommandInput, ScanCommandOutput, BatchWriteCommandInput, BatchWriteCommandOutput } from "@aws-sdk/lib-dynamodb";
|
|
3
|
+
import { DynamoDBDocumentClient, GetCommandInput, GetCommandOutput, PutCommandInput, PutCommandOutput, QueryCommandInput, QueryCommandOutput, UpdateCommandInput, UpdateCommandOutput, ScanCommandInput, ScanCommandOutput, BatchWriteCommandInput, BatchWriteCommandOutput, BatchGetCommandInput, BatchGetCommandOutput } from "@aws-sdk/lib-dynamodb";
|
|
4
4
|
import { Logger } from "@aws-lambda-powertools/logger";
|
|
5
5
|
export declare class DynamoDBService {
|
|
6
6
|
logger: Logger;
|
|
@@ -19,6 +19,7 @@ export declare class DynamoDBService {
|
|
|
19
19
|
checkIfItemExists(checkInput: Pick<GetCommandInput, "TableName" | "Key">): Promise<boolean>;
|
|
20
20
|
scanCommand(scanCommandInput: ScanCommandInput): Promise<ScanCommandOutput>;
|
|
21
21
|
batchWriteCommand(batchWriteCommandInput: BatchWriteCommandInput): Promise<BatchWriteCommandOutput>;
|
|
22
|
+
batchGetCommand(batchGetCommandInput: BatchGetCommandInput): Promise<BatchGetCommandOutput>;
|
|
22
23
|
}
|
|
23
24
|
export type { PutCommandOutput } from "@aws-sdk/lib-dynamodb";
|
|
24
25
|
export type { BatchWriteCommandOutput } from "@aws-sdk/lib-dynamodb";
|
|
@@ -27,6 +28,8 @@ export type { QueryCommandOutput } from "@aws-sdk/lib-dynamodb";
|
|
|
27
28
|
export type { GetCommandOutput } from "@aws-sdk/lib-dynamodb";
|
|
28
29
|
export type { UpdateCommandOutput } from "@aws-sdk/lib-dynamodb";
|
|
29
30
|
export type { BatchWriteCommandInput } from "@aws-sdk/lib-dynamodb";
|
|
31
|
+
export type { BatchGetCommandInput } from "@aws-sdk/lib-dynamodb";
|
|
32
|
+
export type { BatchGetCommandOutput } from "@aws-sdk/lib-dynamodb";
|
|
30
33
|
export type { PutCommandInput } from "@aws-sdk/lib-dynamodb";
|
|
31
34
|
export type { ScanCommandInput } from "@aws-sdk/lib-dynamodb";
|
|
32
35
|
export type { QueryCommandInput } from "@aws-sdk/lib-dynamodb";
|
package/lib/services/dynamoDB.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
2
|
-
import { DynamoDBDocumentClient, GetCommand, PutCommand, QueryCommand, UpdateCommand, ScanCommand, BatchWriteCommand } from "@aws-sdk/lib-dynamodb";
|
|
2
|
+
import { DynamoDBDocumentClient, GetCommand, PutCommand, QueryCommand, UpdateCommand, ScanCommand, BatchWriteCommand, BatchGetCommand } from "@aws-sdk/lib-dynamodb";
|
|
3
3
|
import assert from "node:assert/strict";
|
|
4
4
|
import { getObjectWithAssumeRoleCommandOutputAttribute } from "../utils/index.js";
|
|
5
5
|
export class DynamoDBService {
|
|
@@ -42,4 +42,7 @@ export class DynamoDBService {
|
|
|
42
42
|
async batchWriteCommand(batchWriteCommandInput) {
|
|
43
43
|
return await this.client.send(new BatchWriteCommand(batchWriteCommandInput));
|
|
44
44
|
}
|
|
45
|
+
async batchGetCommand(batchGetCommandInput) {
|
|
46
|
+
return await this.client.send(new BatchGetCommand(batchGetCommandInput));
|
|
47
|
+
}
|
|
45
48
|
}
|
package/lib/services/s3.d.ts
CHANGED
|
@@ -22,6 +22,13 @@ export declare class S3Service {
|
|
|
22
22
|
putObject(putObjectCommandInput: PutObjectCommandInput): Promise<PutObjectCommandOutput>;
|
|
23
23
|
headObject(headObjectCommandInput: HeadObjectCommandInput): Promise<import("@aws-sdk/client-s3").HeadObjectCommandOutput>;
|
|
24
24
|
checkIfObjectExists(headObjectCommandInput: HeadObjectCommandInput): Promise<boolean>;
|
|
25
|
+
checkIfObjectExistsAndNonEmpty(headObjectCommandInput: HeadObjectCommandInput): Promise<boolean>;
|
|
26
|
+
/**
|
|
27
|
+
* Returns true for both 404 (NotFound) and 403 responses.
|
|
28
|
+
* Some S3 bucket policies return 403 instead of 404 for missing keys
|
|
29
|
+
* to avoid revealing whether a key exists — treat both as "not found".
|
|
30
|
+
*/
|
|
31
|
+
private isNotFound;
|
|
25
32
|
deleteObject(deleteObjectCommandInput: DeleteObjectCommandInput): Promise<DeleteObjectCommandOutput>;
|
|
26
33
|
deleteObjects(deleteObjectsCommandInput: DeleteObjectsCommandInput): Promise<DeleteObjectsCommandOutput>;
|
|
27
34
|
}
|
package/lib/services/s3.js
CHANGED
|
@@ -75,12 +75,33 @@ export class S3Service {
|
|
|
75
75
|
return true;
|
|
76
76
|
}
|
|
77
77
|
catch (error) {
|
|
78
|
-
if (error
|
|
78
|
+
if (this.isNotFound(error))
|
|
79
79
|
return false;
|
|
80
|
-
}
|
|
81
80
|
throw error;
|
|
82
81
|
}
|
|
83
82
|
}
|
|
83
|
+
async checkIfObjectExistsAndNonEmpty(headObjectCommandInput) {
|
|
84
|
+
this.logger.debug("checkIfObjectExistsAndNonEmpty", { headObjectCommandInput });
|
|
85
|
+
try {
|
|
86
|
+
const head = await this.headObject(headObjectCommandInput);
|
|
87
|
+
return (head.ContentLength ?? 0) > 0;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (this.isNotFound(error))
|
|
91
|
+
return false;
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Returns true for both 404 (NotFound) and 403 responses.
|
|
97
|
+
* Some S3 bucket policies return 403 instead of 404 for missing keys
|
|
98
|
+
* to avoid revealing whether a key exists — treat both as "not found".
|
|
99
|
+
*/
|
|
100
|
+
isNotFound(error) {
|
|
101
|
+
return (error.name === "NotFound" ||
|
|
102
|
+
error.$metadata?.httpStatusCode === 404 ||
|
|
103
|
+
error.$metadata?.httpStatusCode === 403);
|
|
104
|
+
}
|
|
84
105
|
async deleteObject(deleteObjectCommandInput) {
|
|
85
106
|
this.logger.debug("deleteObject", { deleteObjectCommandInput });
|
|
86
107
|
return await this.client.send(new DeleteObjectCommand(deleteObjectCommandInput));
|
package/lib/services/ssm.d.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import { GetParameterCommandInput, GetParameterCommandOutput, PutParameterCommandInput, PutParameterCommandOutput, SSMClient } from "@aws-sdk/client-ssm";
|
|
1
|
+
import { GetParameterCommandInput, GetParameterCommandOutput, GetParametersByPathCommandInput, PutParameterCommandInput, PutParameterCommandOutput, SSMClient, type Parameter } from "@aws-sdk/client-ssm";
|
|
2
2
|
import { AssumeRoleCommandOutput } from "@aws-sdk/client-sts";
|
|
3
3
|
import { Logger } from "@aws-lambda-powertools/logger";
|
|
4
4
|
export declare class SSMService {
|
|
5
|
-
logger
|
|
5
|
+
logger?: Logger;
|
|
6
6
|
client: SSMClient;
|
|
7
7
|
constructor({ logger, client, assumeRoleCommandOutput, region }: {
|
|
8
|
-
logger
|
|
8
|
+
logger?: Logger;
|
|
9
9
|
client?: SSMClient;
|
|
10
10
|
assumeRoleCommandOutput?: AssumeRoleCommandOutput;
|
|
11
11
|
region?: string;
|
|
12
12
|
});
|
|
13
13
|
getParameter(params: GetParameterCommandInput): Promise<GetParameterCommandOutput>;
|
|
14
14
|
putParameter(params: PutParameterCommandInput): Promise<PutParameterCommandOutput>;
|
|
15
|
+
/** Fetches all parameters under the given path, handling pagination automatically. */
|
|
16
|
+
getParametersByPath(params: GetParametersByPathCommandInput): Promise<Parameter[]>;
|
|
15
17
|
}
|
|
16
|
-
export type { GetParameterCommandInput, GetParameterCommandOutput, PutParameterCommandInput, PutParameterCommandOutput } from "@aws-sdk/client-ssm";
|
|
18
|
+
export type { GetParameterCommandInput, GetParameterCommandOutput, GetParametersByPathCommandInput, Parameter, PutParameterCommandInput, PutParameterCommandOutput } from "@aws-sdk/client-ssm";
|
package/lib/services/ssm.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GetParameterCommand, PutParameterCommand, SSMClient } from "@aws-sdk/client-ssm";
|
|
1
|
+
import { GetParameterCommand, GetParametersByPathCommand, PutParameterCommand, SSMClient } from "@aws-sdk/client-ssm";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { getObjectWithAssumeRoleCommandOutputAttribute } from "../utils/index.js";
|
|
4
4
|
export class SSMService {
|
|
@@ -23,4 +23,15 @@ export class SSMService {
|
|
|
23
23
|
async putParameter(params) {
|
|
24
24
|
return await this.client.send(new PutParameterCommand(params));
|
|
25
25
|
}
|
|
26
|
+
/** Fetches all parameters under the given path, handling pagination automatically. */
|
|
27
|
+
async getParametersByPath(params) {
|
|
28
|
+
const results = [];
|
|
29
|
+
let nextToken;
|
|
30
|
+
do {
|
|
31
|
+
const response = await this.client.send(new GetParametersByPathCommand({ ...params, NextToken: nextToken }));
|
|
32
|
+
results.push(...(response.Parameters ?? []));
|
|
33
|
+
nextToken = response.NextToken;
|
|
34
|
+
} while (nextToken);
|
|
35
|
+
return results;
|
|
36
|
+
}
|
|
26
37
|
}
|
package/lib/utils/index.d.ts
CHANGED
package/lib/utils/index.js
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a date prefix from a folder name.
|
|
3
|
+
*
|
|
4
|
+
* Recognises:
|
|
5
|
+
* YYYYMMDD[non-digit...] e.g. "20100321. Playground"
|
|
6
|
+
* YYYY-MM-DD[...] e.g. "2024-08-15 Vacation"
|
|
7
|
+
* YYYY.MM.DD[...] e.g. "2024.08.15 Vacation"
|
|
8
|
+
*
|
|
9
|
+
* Folder ids may be passed as the full path ("media/2024/08/15/foo/") or
|
|
10
|
+
* just the bare segment — we always extract the last non-empty segment first.
|
|
11
|
+
*
|
|
12
|
+
* Returns:
|
|
13
|
+
* { year, month1, day, isoDate } where month1 is 1-indexed (1=Jan, 12=Dec).
|
|
14
|
+
* null if the segment doesn't start with a recognisable date.
|
|
15
|
+
*
|
|
16
|
+
* The previous implementation in `processMeta.ts` had a bug rejecting January
|
|
17
|
+
* (it checked `month0 < 1`, where month0 was 0-indexed). It also constructed
|
|
18
|
+
* dates in local time, which differs by region. This implementation:
|
|
19
|
+
* - validates 1 ≤ month ≤ 12 and 1 ≤ day ≤ 31 (so January is included)
|
|
20
|
+
* - emits a UTC ISO timestamp via Date.UTC, eliminating timezone drift
|
|
21
|
+
* - returns 1-indexed month for consumer convenience
|
|
22
|
+
*/
|
|
23
|
+
export type ParsedFolderDate = {
|
|
24
|
+
year: number;
|
|
25
|
+
month1: number;
|
|
26
|
+
day: number;
|
|
27
|
+
isoDate: string;
|
|
28
|
+
};
|
|
29
|
+
export declare const parseFolderDate: (id: string) => ParsedFolderDate | null;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const COMPACT = /^(\d{4})(\d{2})(\d{2})/;
|
|
2
|
+
const SEPARATED = /^(\d{4})[-.](\d{2})[-.](\d{2})/;
|
|
3
|
+
export const parseFolderDate = (id) => {
|
|
4
|
+
// Last non-empty path segment
|
|
5
|
+
const segment = id.replace(/\/$/, "").split("/").pop() ?? "";
|
|
6
|
+
if (!segment)
|
|
7
|
+
return null;
|
|
8
|
+
const match = COMPACT.exec(segment) ?? SEPARATED.exec(segment);
|
|
9
|
+
if (!match)
|
|
10
|
+
return null;
|
|
11
|
+
const year = Number(match[1]);
|
|
12
|
+
const month1 = Number(match[2]);
|
|
13
|
+
const day = Number(match[3]);
|
|
14
|
+
if (Number.isNaN(year) || Number.isNaN(month1) || Number.isNaN(day))
|
|
15
|
+
return null;
|
|
16
|
+
if (year < 1900 || year > 2100)
|
|
17
|
+
return null;
|
|
18
|
+
if (month1 < 1 || month1 > 12)
|
|
19
|
+
return null;
|
|
20
|
+
if (day < 1 || day > 31)
|
|
21
|
+
return null;
|
|
22
|
+
// UTC midnight to avoid timezone drift in toISOString().
|
|
23
|
+
const isoDate = new Date(Date.UTC(year, month1 - 1, day)).toISOString();
|
|
24
|
+
return { year, month1, day, isoDate };
|
|
25
|
+
};
|
|
@@ -21,4 +21,15 @@ type ExtractMetadataParams = {
|
|
|
21
21
|
*
|
|
22
22
|
*/
|
|
23
23
|
export declare const processMeta: ({ id, meta, metaTableName, placeIndexName, size, logger, locationService, dynamoDBService }: ExtractMetadataParams) => Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Parent folder of an S3 key, used as the `folder` GSI partition key on the meta table.
|
|
26
|
+
*
|
|
27
|
+
* "2024/08/15/photo.jpg" → "2024/08/15/"
|
|
28
|
+
* "2024/08/15/" → "2024/08/"
|
|
29
|
+
* "2024/" → "/"
|
|
30
|
+
* "" → "/"
|
|
31
|
+
*
|
|
32
|
+
* Root sentinel "/" lets us partition top-level entries under a single PK.
|
|
33
|
+
*/
|
|
34
|
+
export declare const deriveFolder: (id: string) => string;
|
|
24
35
|
export {};
|
package/lib/utils/processMeta.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
1
|
import { getKeyExtension } from "./thumbsKey.js";
|
|
2
|
+
import { parseFolderDate } from "./parseFolderDate.js";
|
|
3
3
|
/**
|
|
4
4
|
* 1. read <id>
|
|
5
5
|
* 2. if <id> doesn't exist then use PutCommand
|
|
@@ -85,9 +85,13 @@ export const processMeta = async ({ id, meta, metaTableName, placeIndexName, siz
|
|
|
85
85
|
Key: {
|
|
86
86
|
id
|
|
87
87
|
},
|
|
88
|
-
UpdateExpression: "set tags = :tags",
|
|
88
|
+
UpdateExpression: "set tags = :tags, #folder = :folder",
|
|
89
|
+
ExpressionAttributeNames: {
|
|
90
|
+
"#folder": "folder"
|
|
91
|
+
},
|
|
89
92
|
ExpressionAttributeValues: {
|
|
90
|
-
":tags": reconciledTags
|
|
93
|
+
":tags": reconciledTags,
|
|
94
|
+
":folder": deriveFolder(id)
|
|
91
95
|
},
|
|
92
96
|
ReturnValues: "ALL_NEW"
|
|
93
97
|
});
|
|
@@ -102,38 +106,45 @@ const reconcileTags = ({ oldTags = [], newTags = [] }, logger) => oldTags.reduce
|
|
|
102
106
|
return [...acc, cur];
|
|
103
107
|
}
|
|
104
108
|
}, newTags);
|
|
105
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Parent folder of an S3 key, used as the `folder` GSI partition key on the meta table.
|
|
111
|
+
*
|
|
112
|
+
* "2024/08/15/photo.jpg" → "2024/08/15/"
|
|
113
|
+
* "2024/08/15/" → "2024/08/"
|
|
114
|
+
* "2024/" → "/"
|
|
115
|
+
* "" → "/"
|
|
116
|
+
*
|
|
117
|
+
* Root sentinel "/" lets us partition top-level entries under a single PK.
|
|
118
|
+
*/
|
|
119
|
+
export const deriveFolder = (id) => {
|
|
120
|
+
if (!id)
|
|
121
|
+
return "/";
|
|
122
|
+
const trimmed = id.endsWith("/") ? id.slice(0, -1) : id;
|
|
123
|
+
const lastSlash = trimmed.lastIndexOf("/");
|
|
124
|
+
if (lastSlash < 0)
|
|
125
|
+
return "/";
|
|
126
|
+
return trimmed.slice(0, lastSlash + 1);
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Derive date tags from the parent folder's name when it begins with a
|
|
130
|
+
* recognised date prefix (YYYYMMDD, YYYY-MM-DD, YYYY.MM.DD).
|
|
131
|
+
*
|
|
132
|
+
* Operates on the parent folder of the given key — for "media/20240815/photo.jpg"
|
|
133
|
+
* it parses "20240815". The previous implementation skipped January due to
|
|
134
|
+
* a 0-vs-1-indexed month bug; that has been fixed in `parseFolderDate`.
|
|
135
|
+
*/
|
|
106
136
|
const extractMetaFromKey = (key) => {
|
|
107
137
|
if (!key)
|
|
108
138
|
return [];
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
firstToken = folder.split(".")[0];
|
|
114
|
-
}
|
|
115
|
-
catch (error) { }
|
|
116
|
-
if (firstToken === undefined ||
|
|
117
|
-
firstToken.length !== 8 ||
|
|
118
|
-
!firstToken.startsWith(SUBSTRING_ANSI_DATES_BEGIN_WITH))
|
|
119
|
-
return [];
|
|
120
|
-
const year = Number(firstToken.substring(0, 4));
|
|
121
|
-
const month = Number(firstToken.substring(4, 6)) - 1;
|
|
122
|
-
const day = Number(firstToken.substring(6));
|
|
123
|
-
if (isNaN(year) || isNaN(month) || isNaN(day))
|
|
124
|
-
return [];
|
|
125
|
-
// mon starts from 0
|
|
126
|
-
// day starts from 1
|
|
127
|
-
if (year < 2000 || month < 1 || month > 11 || day < 1 || day > 31)
|
|
139
|
+
// Look at the parent folder, not the file itself.
|
|
140
|
+
const parent = key.endsWith("/") ? key : key.slice(0, key.lastIndexOf("/") + 1);
|
|
141
|
+
const parsed = parseFolderDate(parent);
|
|
142
|
+
if (!parsed)
|
|
128
143
|
return [];
|
|
129
|
-
const dateCreatedBin = new Date(year, month, day);
|
|
130
144
|
return [
|
|
131
|
-
{ key: "dateCreated", value:
|
|
132
|
-
{ key: "yearCreated", value:
|
|
133
|
-
{ key: "dayCreated", value:
|
|
134
|
-
{
|
|
135
|
-
key: "monthCreated",
|
|
136
|
-
value: (dateCreatedBin.getMonth() + 1).toString()
|
|
137
|
-
}
|
|
145
|
+
{ key: "dateCreated", value: parsed.isoDate },
|
|
146
|
+
{ key: "yearCreated", value: String(parsed.year) },
|
|
147
|
+
{ key: "dayCreated", value: String(parsed.day) },
|
|
148
|
+
{ key: "monthCreated", value: String(parsed.month1) },
|
|
138
149
|
];
|
|
139
150
|
};
|