@lightbend/akkaserverless-javascript-sdk
Version:
Akka Serverless JavaScript SDK
318 lines (291 loc) • 8.69 kB
text/typescript
/*
* Copyright 2021 Lightbend Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Cloudevent } from './cloudevent';
import { JwtClaims } from './jwt-claims';
type MetadataValue = string | Buffer;
// Using an interface for compatibility with legacy JS code
interface MetadataEntry {
readonly key: string;
readonly bytesValue: Buffer | undefined;
readonly stringValue: string | undefined;
}
interface MetadataMap {
[key: string]: string | Buffer | undefined;
}
class MetadataMapProxyHandler implements ProxyHandler<MetadataMap> {
private metadata: Metadata;
constructor(metadata: Metadata) {
this.metadata = metadata;
}
ownKeys(target: MetadataMap): ArrayLike<string | symbol> {
const keys = new Array<string>();
for (const entry of this.metadata.entries) {
keys.push(entry.key);
}
return keys;
}
deleteProperty(target: MetadataMap, key: string | symbol): boolean {
if (typeof key === 'string') {
const hasKey = this.metadata.has(key as string);
if (hasKey) {
this.metadata.delete(key);
}
return hasKey;
}
return false;
}
get(target: MetadataMap, key: string | symbol, receiver: any): any {
if (typeof key === 'string') {
const lowercaseKey = (key as string).toLowerCase();
for (const entry of this.metadata.entries) {
if (lowercaseKey === entry.key.toLowerCase()) {
if (entry.stringValue) {
return entry.stringValue;
}
if (entry.bytesValue) {
return entry.bytesValue;
}
}
}
}
return undefined;
}
has(target: MetadataMap, key: string | symbol): boolean {
if (typeof key === 'string') {
const lowercaseKey = (key as string).toLowerCase();
for (const entry of this.metadata.entries) {
if (lowercaseKey === entry.key.toLowerCase()) {
return true;
}
}
}
return false;
}
set(
target: MetadataMap,
key: string | symbol,
value: any,
receiver: any,
): boolean {
if (typeof key === 'string') {
this.metadata.delete(key as string);
this.metadata.set(key as string, value);
return true;
}
return false;
}
getOwnPropertyDescriptor(
target: MetadataMap,
key: string | symbol,
): PropertyDescriptor | undefined {
const superThis = this;
if (typeof key === 'string') {
const v = this.get(target, key as string, null);
if (v !== undefined) {
return new (class implements PropertyDescriptor {
readonly value = v;
readonly writable = true;
readonly enumerable = true;
readonly configurable = false;
get(): any {
return v;
}
set(v: any): void {
superThis.set(target, key, v, null);
}
})();
}
}
return undefined;
}
}
/**
* Akka Serverless metadata.
*
* Metadata is treated as case insensitive on lookup, and case sensitive on set. Multiple values per key are supported,
* setting a value will add it to the current values for that key. You should delete first if you wish to replace a
* value.
*
* Values can either be strings or byte buffers. If a non string or byte buffer value is set, it will be converted to
* a string using toString.
*
* @param entries - the list of entries
*/
export class Metadata {
readonly entries: MetadataEntry[];
/**
* The metadata expressed as an object/map.
*
* The map is backed by the this Metadata object - changes to this map will be reflected in this metadata object and
* changes to this object will be reflected in the map.
*
* The map will return the first metadata entry that matches the key, case insensitive, when properties are looked up.
* When setting properties, it will replace all entries that match the key, case insensitive.
*/
readonly asMap: MetadataMap = new Proxy(
new (class implements MetadataMap {
[key: string]: string | Buffer | undefined;
})(),
new MetadataMapProxyHandler(this),
);
/**
* The Cloudevent data from this Metadata.
*
* This object is backed by this Metadata, changes to the Cloudevent will be reflected in the Metadata.
*/
readonly cloudevent: Cloudevent = new Cloudevent(this);
/**
* The JWT claims, if there was a validated bearer token with this request.
*/
readonly jwtClaims: JwtClaims = new JwtClaims(this);
constructor(entries: MetadataEntry[] = []) {
this.entries = entries;
}
/**
* If this metadata object is being returned as the metadata for an HTTP transcoded response, this will set the
* HTTP status code for the response.
*
* This will have no impact on gRPC responses.
*
* @param code The status code to set.
*/
setHttpStatusCode(code: number) {
if (code < 100 || code >= 600) {
throw new Error('Invalid HTTP status code: ' + code);
}
this.set('_akkasls-http-code', code.toString());
}
/**
* @returns CloudEvent subject value
* @deprecated Use cloudevent.subject instead.
*/
getSubject(): MetadataValue | undefined {
const subject = this.get('ce-subject');
if (subject.length > 0) {
return subject[0];
} else {
return undefined;
}
}
/**
* Create a new MetadataEntry.
*
* @param key - the key for the entry
* @param value - the value for the entry
* @returns a new MetadataEntry
*/
private createMetadataEntry(key: string, value: any): MetadataEntry {
if (typeof value === 'string') {
return { key: key, stringValue: value, bytesValue: undefined };
} else if (Buffer.isBuffer(value)) {
return { key: key, bytesValue: value, stringValue: undefined };
} else {
return { key: key, stringValue: value.toString(), bytesValue: undefined };
}
}
/**
* Get the value from a metadata entry.
*
* @param entry - the metadata entry
* @returns the value for the given entry
*/
private getValue(entry: MetadataEntry): MetadataValue | undefined {
if (entry.bytesValue !== undefined) {
return entry.bytesValue;
} else {
return entry.stringValue;
}
}
/**
* Get all the values for the given key.
*
* The key is case insensitive.
*
* @param key - the key to get
* @returns all the values, or an empty array if no values exist for the key
*/
get(key: string): MetadataValue[] {
const values: MetadataValue[] = [];
this.entries.forEach((entry) => {
if (key.toLowerCase() === entry.key.toLowerCase()) {
const value = this.getValue(entry);
if (value) {
values.push(value);
}
}
});
return values;
}
/**
* Set a given key value.
*
* This will append the key value to the metadata, it won't replace any existing values for existing keys.
*
* @param key - the key to set
* @param value - the value to set
* @returns this updated metadata
*/
set(key: string, value: any): Metadata {
this.entries.push(this.createMetadataEntry(key, value));
return this;
}
/**
* Delete all values with the given key.
*
* The key is case insensitive.
*
* @param key - the key to delete
* @returns this updated metadata
*/
delete(key: string) {
let idx = 0;
while (idx < this.entries.length) {
const entry = this.entries[idx];
if (key.toLowerCase() !== entry.key.toLowerCase()) {
idx++;
} else {
this.entries.splice(idx, 1);
}
}
return this;
}
/**
* Whether there exists a metadata value for the given key.
*
* The key is case insensitive.
*
* @param key - the key to check
* @returns whether values exist for the given key
*/
has(key: string): boolean {
for (const idx in this.entries) {
const entry = this.entries[idx];
if (key.toLowerCase() === entry.key.toLowerCase()) {
return true;
}
}
return false;
}
/**
* Clear the metadata.
*
* @returns this updated metadata
*/
clear() {
this.entries.splice(0, this.entries.length);
return this;
}
}