@aspan-corporation/ac-shared 1.2.25 → 1.2.26

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.
@@ -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
  }
@@ -75,12 +75,33 @@ export class S3Service {
75
75
  return true;
76
76
  }
77
77
  catch (error) {
78
- if (error.name === "NotFound") {
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));
@@ -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: Logger;
5
+ logger?: Logger;
6
6
  client: SSMClient;
7
7
  constructor({ logger, client, assumeRoleCommandOutput, region }: {
8
- logger: 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";
@@ -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
  }
@@ -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 {};
@@ -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,6 +106,25 @@ const reconcileTags = ({ oldTags = [], newTags = [] }, logger) => oldTags.reduce
102
106
  return [...acc, cur];
103
107
  }
104
108
  }, newTags);
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
+ };
105
128
  const SUBSTRING_ANSI_DATES_BEGIN_WITH = "20";
106
129
  const extractMetaFromKey = (key) => {
107
130
  if (!key)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aspan-corporation/ac-shared",
3
- "version": "1.2.25",
3
+ "version": "1.2.26",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "exports": {