@kwiz/node 1.0.23 → 1.0.25
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/package.json +4 -3
- package/.github/workflows/npm-publish.yml +0 -24
- package/.madgerc +0 -3
- package/fix-folder-imports.js +0 -27
- package/src/SPO/common.ts +0 -17
- package/src/SPO/exports-index.ts +0 -1
- package/src/auth/discovery.test.js +0 -9
- package/src/auth/discovery.ts +0 -61
- package/src/auth/exports-index.ts +0 -2
- package/src/auth/msal.ts +0 -44
- package/src/axios.ts +0 -45
- package/src/exports-index.ts +0 -6
- package/src/get-with-cache.ts +0 -38
- package/src/graph/exports-index.ts +0 -1
- package/src/graph/graph.ts +0 -18
- package/src/index.ts +0 -1
- package/src/storage/common.ts +0 -15
- package/src/storage/exports-index.ts +0 -3
- package/src/storage/odata.ts +0 -87
- package/src/storage/table-storage.test.js +0 -150
- package/src/storage/table-storage.ts +0 -385
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kwiz/node",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.25",
|
|
4
4
|
"description": "KWIZ utilities and helpers for node applications",
|
|
5
5
|
"module": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"link-local-common": "npm link @kwiz/common",
|
|
28
28
|
"reindex-project": "cti create ./src -i _dependencies -w -b -n -o exports-index.ts && node fix-folder-imports.js",
|
|
29
29
|
"install-packages": "npm install azurite -g && npm ci",
|
|
30
|
-
"startDevStorage": "azurite -s -l"
|
|
30
|
+
"startDevStorage": "azurite -s -l",
|
|
31
|
+
"clear-npm-cache": "npm cache clean --force"
|
|
31
32
|
},
|
|
32
33
|
"repository": {
|
|
33
34
|
"type": "git",
|
|
@@ -69,7 +70,7 @@
|
|
|
69
70
|
"dependencies": {
|
|
70
71
|
"@azure/data-tables": "^13.2.2",
|
|
71
72
|
"@azure/msal-node": "^2.6.4",
|
|
72
|
-
"@kwiz/common": "^1.0.
|
|
73
|
+
"@kwiz/common": "^1.0.164",
|
|
73
74
|
"axios": "^1.6.7",
|
|
74
75
|
"esbuild": "^0.19.12",
|
|
75
76
|
"get-tsconfig": "^4.7.2",
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
|
|
2
|
-
# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
|
|
3
|
-
|
|
4
|
-
name: Node.js Package
|
|
5
|
-
|
|
6
|
-
on:
|
|
7
|
-
push:
|
|
8
|
-
tags:
|
|
9
|
-
- 'v*.*.*' # run every time we commit with a new version number
|
|
10
|
-
|
|
11
|
-
jobs:
|
|
12
|
-
build-publish-npm:
|
|
13
|
-
runs-on: ubuntu-latest
|
|
14
|
-
steps:
|
|
15
|
-
- uses: actions/checkout@v4
|
|
16
|
-
- uses: actions/setup-node@v4
|
|
17
|
-
with:
|
|
18
|
-
node-version: 18
|
|
19
|
-
registry-url: https://registry.npmjs.org/
|
|
20
|
-
- run: npm ci
|
|
21
|
-
- run: npm run build
|
|
22
|
-
- run: npm run npm-publish
|
|
23
|
-
env:
|
|
24
|
-
NODE_AUTH_TOKEN: ${{secrets.KWIZ_NPM_TOKEN}}
|
package/.madgerc
DELETED
package/fix-folder-imports.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
const glob = require('glob');
|
|
2
|
-
const fs = require("fs-extra");
|
|
3
|
-
const exportsFileName = "exports-index";
|
|
4
|
-
const exportsFileNameWithExt = `${exportsFileName}.ts`;
|
|
5
|
-
const exportsIndexFiles = glob.sync(`./src/**/${exportsFileNameWithExt}`);
|
|
6
|
-
//loop every exportsIndexFiles and find any import to a directory, and replace with /exports-index
|
|
7
|
-
console.time("fixing direcry imports");
|
|
8
|
-
exportsIndexFiles.forEach(file => {
|
|
9
|
-
var content = fs.readFileSync(file, "utf8").split("\n");
|
|
10
|
-
var parentFolderContent = fs.readdirSync(file.replace(exportsFileNameWithExt, ''));
|
|
11
|
-
var hasChanges = false;
|
|
12
|
-
//loop every import - if it does not match a file in the folder, but matches a sub-folder - append exports-index to it
|
|
13
|
-
content.forEach((line, idx) => {
|
|
14
|
-
if (line.replace(/ /g, '').length > 0) {
|
|
15
|
-
let importName = line.slice(line.indexOf('./') + 2, line.length - 2);
|
|
16
|
-
if (parentFolderContent.includes(importName))//its a folder, otherwise it would be .ts
|
|
17
|
-
{
|
|
18
|
-
content[idx] = line.replace(`./${importName}`, `./${importName}/${exportsFileName}`);
|
|
19
|
-
hasChanges = true;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
if (hasChanges) {
|
|
24
|
-
fs.writeFileSync(file, content.join('\n'));
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
console.timeEnd("fixing direcry imports");
|
package/src/SPO/common.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { AuthContextType, GetMSALSiteScope, ITenantInfo, isNullOrUndefined } from "@kwiz/common";
|
|
2
|
-
import { GetMSALToken } from "../auth/msal";
|
|
3
|
-
import { getAxiosConfigBearer } from "../axios";
|
|
4
|
-
|
|
5
|
-
var auth: AuthContextType = null;
|
|
6
|
-
export function ConfigureSPOAuth(config?: AuthContextType) {
|
|
7
|
-
auth = config;
|
|
8
|
-
}
|
|
9
|
-
export async function getAxiosConfigSharePoint(tenantInfo: ITenantInfo, hostName: string, clearCache?: boolean) {
|
|
10
|
-
if (isNullOrUndefined(auth)) throw Error("Call ConfigureSPOAuth first");
|
|
11
|
-
// only certificate supported
|
|
12
|
-
// DisableCustomAppAuthentication property, that disable this kind of auth., however it can be overriden using this command:
|
|
13
|
-
// Set-SPOTenant -DisableCustomAppAuthentication $false
|
|
14
|
-
let token = await GetMSALToken(tenantInfo, GetMSALSiteScope(hostName), auth, clearCache);
|
|
15
|
-
let config = getAxiosConfigBearer(token, { contantType: "application/json; odata=nometadata" });
|
|
16
|
-
return config;
|
|
17
|
-
}
|
package/src/SPO/exports-index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './common';
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import assert from 'assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import { DiscoverTenantInfo } from "./discovery";
|
|
4
|
-
|
|
5
|
-
test('DiscoverTenantInfo', async t => {
|
|
6
|
-
const tenantName = "kwizcom.com";
|
|
7
|
-
const tenantInfo = await DiscoverTenantInfo(tenantName);
|
|
8
|
-
assert.strictEqual(tenantInfo.idOrName, "7d034656-be03-457d-8d82-60e90cf5f400");
|
|
9
|
-
});
|
package/src/auth/discovery.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { $AzureEnvironment, GetAzureADLoginEndPoint, GetEnvironmentFromACSEndPoint, ITenantInfo, isNullOrEmptyString, isValidGuid, promiseOnce } from "@kwiz/common";
|
|
2
|
-
import axios from "axios";
|
|
3
|
-
|
|
4
|
-
export function DiscoverTenantInfo(hostName: string) {
|
|
5
|
-
hostName = hostName.toLowerCase();
|
|
6
|
-
return promiseOnce(`DiscoverTenantInfo|${hostName}`, async () => {
|
|
7
|
-
let data: ITenantInfo = {
|
|
8
|
-
environment: $AzureEnvironment.Production,
|
|
9
|
-
idOrName: null,
|
|
10
|
-
authorityUrl: null,
|
|
11
|
-
valid: false
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
let tenantId: string = null;
|
|
15
|
-
let friendlyName: string = null;
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
if (hostName.indexOf(".sharepoint.") !== -1) {
|
|
19
|
-
let hostParts = hostName.split('.');//should be xxx.sharepoint.com or xxx.sharepoint.us
|
|
20
|
-
let firstHostPart = hostParts[0];
|
|
21
|
-
let lastHostPart = hostParts[hostParts.length - 1] === "us" || hostParts[hostParts.length - 1] === "de" ? hostParts[hostParts.length - 1] : "com";
|
|
22
|
-
if (firstHostPart.endsWith("-admin")) firstHostPart = firstHostPart.substring(0, firstHostPart.length - 6);
|
|
23
|
-
friendlyName = `${firstHostPart}.onmicrosoft.${lastHostPart}`;
|
|
24
|
-
}
|
|
25
|
-
else friendlyName = hostName;//could be an exchange email domain, or bpos customer
|
|
26
|
-
|
|
27
|
-
let config = await axios.get<{
|
|
28
|
-
token_endpoint: string;//https://login.microsoftonline.com/7d034656-be03-457d-8d82-60e90cf5f400/oauth2/token
|
|
29
|
-
cloud_instance_name: string;//microsoftonline.com
|
|
30
|
-
token_endpoint_auth_methods_supported: string[];// ["client_secret_post", "private_key_jwt", "client_secret_basic"]
|
|
31
|
-
response_modes_supported: string[];// ["query", "fragment", "form_post"]
|
|
32
|
-
response_types_supported: string[];// ["code", "id_token", "code id_token", "token id_token", "token"]
|
|
33
|
-
scopes_supported: string[];// ["openid"]
|
|
34
|
-
issuer: string;//https://sts.windows.net/7d034656-be03-457d-8d82-60e90cf5f400/
|
|
35
|
-
authorization_endpoint: string;//https://login.microsoftonline.com/7d034656-be03-457d-8d82-60e90cf5f400/oauth2/authorize
|
|
36
|
-
device_authorization_endpoint: string;//https://login.microsoftonline.com/7d034656-be03-457d-8d82-60e90cf5f400/oauth2/devicecode
|
|
37
|
-
end_session_endpoint: string;//https://login.microsoftonline.com/7d034656-be03-457d-8d82-60e90cf5f400/oauth2/logout
|
|
38
|
-
userinfo_endpoint: string;//https://login.microsoftonline.com/7d034656-be03-457d-8d82-60e90cf5f400/openid/userinfo
|
|
39
|
-
tenant_region_scope: string;//NA
|
|
40
|
-
cloud_graph_host_name: string;//graph.windows.net
|
|
41
|
-
msgraph_host: string;//graph.microsoft.com
|
|
42
|
-
}>(`https://login.microsoftonline.com/${friendlyName}/v2.0/.well-known/openid-configuration`);
|
|
43
|
-
|
|
44
|
-
let endpoint = config.data.token_endpoint;//https://xxxx/{tenant}/....
|
|
45
|
-
tenantId = endpoint.replace("//", "/").split('/')[2];//replace :// with :/ split by / and take the second part.
|
|
46
|
-
let instance = config.data.cloud_instance_name;//microsoftonline.us
|
|
47
|
-
|
|
48
|
-
data.environment = GetEnvironmentFromACSEndPoint(instance);
|
|
49
|
-
if (!isNullOrEmptyString(tenantId) || isValidGuid(tenantId))
|
|
50
|
-
data.idOrName = tenantId;
|
|
51
|
-
else
|
|
52
|
-
data.idOrName = friendlyName;
|
|
53
|
-
|
|
54
|
-
data.authorityUrl = `${GetAzureADLoginEndPoint(data.environment)}/${data.idOrName}`;
|
|
55
|
-
data.valid = true;
|
|
56
|
-
}
|
|
57
|
-
catch (e) { }
|
|
58
|
-
|
|
59
|
-
return data;
|
|
60
|
-
});
|
|
61
|
-
}
|
package/src/auth/msal.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { ConfidentialClientApplication } from "@azure/msal-node";
|
|
2
|
-
import { AuthContextType, AuthenticationModes, ITenantInfo } from "@kwiz/common";
|
|
3
|
-
//find tenant id? https://login.microsoftonline.com/kwizcom.onmicrosoft.com/.well-known/openid-configuration
|
|
4
|
-
//https://stackoverflow.com/questions/54771270/msal-ad-token-not-valid-with-sharepoint-online-csom
|
|
5
|
-
|
|
6
|
-
var apps: { [tenant: string]: ConfidentialClientApplication } = {};
|
|
7
|
-
|
|
8
|
-
function GetApp(tenantInfo: ITenantInfo, auth: AuthContextType) {
|
|
9
|
-
let key = `${tenantInfo.idOrName}|${auth.authenticationMode}`
|
|
10
|
-
if (!apps[key]) {
|
|
11
|
-
auth.authenticationMode === AuthenticationModes.clientSecret
|
|
12
|
-
? apps[key] = new ConfidentialClientApplication({
|
|
13
|
-
auth: {
|
|
14
|
-
clientId: auth.clientId,
|
|
15
|
-
authority: tenantInfo.authorityUrl,
|
|
16
|
-
clientSecret: auth.clientSecret
|
|
17
|
-
},
|
|
18
|
-
|
|
19
|
-
})
|
|
20
|
-
: apps[key] = new ConfidentialClientApplication({
|
|
21
|
-
auth: {
|
|
22
|
-
clientId: auth.clientId,
|
|
23
|
-
authority: tenantInfo.authorityUrl,
|
|
24
|
-
clientCertificate: {
|
|
25
|
-
thumbprint: auth.thumbprint,
|
|
26
|
-
privateKey: auth.privateKey
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
return apps[key];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** client secret not supported by SharePoint, must use certificate */
|
|
36
|
-
export async function GetMSALToken(tenantInfo: ITenantInfo, scope: string, auth: AuthContextType, clearCache?: boolean) {
|
|
37
|
-
const app = GetApp(tenantInfo, auth);
|
|
38
|
-
if (clearCache)
|
|
39
|
-
app.clearCache();
|
|
40
|
-
let token = await app.acquireTokenByClientCredential({
|
|
41
|
-
scopes: [`${scope}/.default`]
|
|
42
|
-
});
|
|
43
|
-
return token.accessToken;
|
|
44
|
-
}
|
package/src/axios.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { isNullOrEmptyString } from "@kwiz/common";
|
|
2
|
-
import { AxiosError, AxiosRequestConfig } from "axios";
|
|
3
|
-
import { Agent, globalAgent } from "https";
|
|
4
|
-
|
|
5
|
-
type axiosConfigOptions = {
|
|
6
|
-
contantType?: "application/json" | "application/json; odata=nometadata" | "application/xml";
|
|
7
|
-
}
|
|
8
|
-
export function getAxiosConfigBearer(token: string, options?: axiosConfigOptions) {
|
|
9
|
-
return getAxiosConfig(`Bearer ${token}`, options);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function getAxiosConfig(token?: string, options?: axiosConfigOptions) {
|
|
13
|
-
//allow self sign ssl certificates
|
|
14
|
-
globalAgent.options.rejectUnauthorized = false;
|
|
15
|
-
const config: AxiosRequestConfig<any> = {
|
|
16
|
-
httpAgent: new Agent({
|
|
17
|
-
rejectUnauthorized: false
|
|
18
|
-
}),
|
|
19
|
-
headers: {}
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
if (!isNullOrEmptyString(token))
|
|
23
|
-
config.headers!.Authorization = token;
|
|
24
|
-
|
|
25
|
-
if (options) {
|
|
26
|
-
if (!isNullOrEmptyString(options.contantType)) {
|
|
27
|
-
config.headers!["Content-Type"] = options.contantType;
|
|
28
|
-
config.headers!["Accept"] = options.contantType;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return config;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function getAxiosErrorData(error: AxiosError) {
|
|
35
|
-
let code = error.code || "Unknown";
|
|
36
|
-
let errorMessage = error.message || "Unspecified error";
|
|
37
|
-
if (error && error.response && error.response.data && error.response.data["odata.error"]) {
|
|
38
|
-
let errorData: { code: string; message: { value: string; }; } = error.response.data["odata.error"];
|
|
39
|
-
if (errorData.message && errorData.message.value)
|
|
40
|
-
errorMessage = errorData.message.value;
|
|
41
|
-
if (errorData && errorData.code)
|
|
42
|
-
code = errorData.code;
|
|
43
|
-
}
|
|
44
|
-
return { code: code, message: errorMessage };
|
|
45
|
-
}
|
package/src/exports-index.ts
DELETED
package/src/get-with-cache.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { IDictionary, isNullOrUndefined } from "@kwiz/common";
|
|
2
|
-
|
|
3
|
-
const $$cache: IDictionary<{ expires: Date; value: any }> = {};
|
|
4
|
-
|
|
5
|
-
export async function getWithCache<T>(worker: () => Promise<{ success: boolean; value: T }>, info: {
|
|
6
|
-
/** seconds */
|
|
7
|
-
successCacheDuration: number;
|
|
8
|
-
/** seconds */
|
|
9
|
-
failedCacheDuration?: number;
|
|
10
|
-
/** must be unique for your call! function name, and parameters */
|
|
11
|
-
cacheKey: string;
|
|
12
|
-
forceRefresh?: boolean;
|
|
13
|
-
}): Promise<T> {
|
|
14
|
-
const now = new Date();
|
|
15
|
-
//purge old values
|
|
16
|
-
Object.keys($$cache).forEach(key => {
|
|
17
|
-
if ($$cache[key].expires < now) delete $$cache[key];
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
let cached = info.forceRefresh ? null : $$cache[info.cacheKey];
|
|
21
|
-
|
|
22
|
-
if (isNullOrUndefined(cached)) {
|
|
23
|
-
const result = await worker();
|
|
24
|
-
if (result.success) {
|
|
25
|
-
$$cache[info.cacheKey] = {
|
|
26
|
-
expires: new Date(new Date().getTime() + info.successCacheDuration * 1000),
|
|
27
|
-
value: result.value
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
else if (info.failedCacheDuration > 0) {
|
|
31
|
-
$$cache[info.cacheKey] = {
|
|
32
|
-
expires: new Date(new Date().getTime() + info.failedCacheDuration * 1000),
|
|
33
|
-
value: result.value
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return $$cache[info.cacheKey].value;
|
|
38
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './graph';
|
package/src/graph/graph.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { AuthContextType, ITenantInfo, isNullOrUndefined } from "@kwiz/common";
|
|
2
|
-
import { GetMSALToken } from "../auth/msal";
|
|
3
|
-
import { getAxiosConfigBearer } from "../axios";
|
|
4
|
-
|
|
5
|
-
/** "https://graph.microsoft.com" */
|
|
6
|
-
export const graphScope = "https://graph.microsoft.com";
|
|
7
|
-
|
|
8
|
-
var auth: AuthContextType = null;
|
|
9
|
-
export function ConfigureGraphAuth(config?: AuthContextType) {
|
|
10
|
-
auth = config;
|
|
11
|
-
}
|
|
12
|
-
export async function getAxiosConfigGraph(tenantInfo: ITenantInfo, clearCache?: boolean) {
|
|
13
|
-
if (isNullOrUndefined(auth)) throw Error("Call ConfigureGraphAuth first");
|
|
14
|
-
|
|
15
|
-
// secret or certificate supported
|
|
16
|
-
let token = await GetMSALToken(tenantInfo, graphScope, auth, clearCache);
|
|
17
|
-
return getAxiosConfigBearer(token);
|
|
18
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./exports-index";
|
package/src/storage/common.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { isNullOrEmptyString } from "@kwiz/common";
|
|
2
|
-
import axios from "axios";
|
|
3
|
-
|
|
4
|
-
export async function IsAzuriteRunning() {
|
|
5
|
-
let responseServer = "";
|
|
6
|
-
try {
|
|
7
|
-
//make a request for http://127.0.0.1:10000/ expect response headers "Server" to contain "Azurite"
|
|
8
|
-
const result = await axios.get("http://127.0.0.1:10000/");
|
|
9
|
-
responseServer = result.headers.server;
|
|
10
|
-
} catch (e) {
|
|
11
|
-
responseServer = e.response && e.response.headers && e.response.headers.server;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return isNullOrEmptyString(responseServer) ? false : responseServer.toLowerCase().indexOf("azurite") >= 0;
|
|
15
|
-
}
|
package/src/storage/odata.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { isNullOrEmptyArray, isNullOrEmptyString, isNullOrUndefined } from "@kwiz/common";
|
|
2
|
-
|
|
3
|
-
export enum ODataOperators {
|
|
4
|
-
equal = "eq",
|
|
5
|
-
notEqual = "ne",
|
|
6
|
-
greater = "gt",
|
|
7
|
-
greaterOrEqual = "ge",
|
|
8
|
-
less = "lt",
|
|
9
|
-
lessOrEqual = "le",
|
|
10
|
-
contains = "in",
|
|
11
|
-
startswith = "startswith"
|
|
12
|
-
}
|
|
13
|
-
export enum ODataJoinOperators {
|
|
14
|
-
and = "and",
|
|
15
|
-
or = "or"
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface IOdataFilter<DataType> {
|
|
19
|
-
property: keyof DataType;
|
|
20
|
-
operator: ODataOperators;
|
|
21
|
-
value: string | Date | number | boolean;
|
|
22
|
-
not?: boolean;
|
|
23
|
-
}
|
|
24
|
-
export interface IOdataFilterStatement<DataType> {
|
|
25
|
-
filters: (IOdataFilter<DataType> | IOdataFilterStatement<DataType>)[];
|
|
26
|
-
/** default: or */
|
|
27
|
-
join?: ODataJoinOperators;
|
|
28
|
-
not?: boolean;
|
|
29
|
-
}
|
|
30
|
-
function isIOdataFilter<DataType>(f: IOdataFilter<DataType> | IOdataFilterStatement<DataType>): f is IOdataFilter<DataType> {
|
|
31
|
-
if (typeof (f as IOdataFilter<DataType>).property === "string")
|
|
32
|
-
return true;
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
function isIOdataFilterStatement<DataType>(f: IOdataFilter<DataType> | IOdataFilterStatement<DataType>): f is IOdataFilter<DataType> {
|
|
36
|
-
if (typeof (f as IOdataFilterStatement<DataType>).filters === "object" && Array.isArray((f as IOdataFilterStatement<DataType>).filters))
|
|
37
|
-
return true;
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
enum TableStorageKnownColumns {
|
|
42
|
-
partitionKey = "PartitionKey",
|
|
43
|
-
rowKey = "RowKey"
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function getPropNameForFilter(prop: string | number | symbol) {
|
|
47
|
-
return TableStorageKnownColumns[prop] || prop as string;
|
|
48
|
-
}
|
|
49
|
-
function getOdataFilterStatement<DataType>(filter: IOdataFilter<DataType>) {
|
|
50
|
-
let filterValue = isNullOrUndefined(filter.value)
|
|
51
|
-
? `null`
|
|
52
|
-
: typeof filter.value === "string"
|
|
53
|
-
? `'${filter.value.replace(/'/g, "''")}'`
|
|
54
|
-
: filter.value instanceof Date
|
|
55
|
-
? `'${filter.value.toISOString()}'`
|
|
56
|
-
: `${filter.value}`;
|
|
57
|
-
|
|
58
|
-
if (filter.operator === ODataOperators.startswith)
|
|
59
|
-
return `${filter.not ? 'not ' : ''}${filter.operator}(${getPropNameForFilter(filter.property)}, ${filterValue})`;
|
|
60
|
-
return `${filter.not ? 'not ' : ''}${getPropNameForFilter(filter.property)} ${filter.operator} ${filterValue}`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function getOdataFilter<DataType>(statement: IOdataFilterStatement<DataType>) {
|
|
64
|
-
if (isNullOrUndefined(statement) || isNullOrEmptyArray(statement.filters)) return "";
|
|
65
|
-
if (isNullOrEmptyString(statement.join)) statement.join = ODataJoinOperators.or;
|
|
66
|
-
|
|
67
|
-
let filterStatements: string[] = [];
|
|
68
|
-
statement.filters.forEach(filter => {
|
|
69
|
-
if (isIOdataFilter(filter)) {
|
|
70
|
-
filterStatements.push(getOdataFilterStatement<DataType>(filter));
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
let subStatement = getOdataFilter(filter);
|
|
74
|
-
if (!isNullOrEmptyString(subStatement))
|
|
75
|
-
filterStatements.push(subStatement);
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
if (filterStatements.length === 0) return "";
|
|
79
|
-
|
|
80
|
-
let result = "";
|
|
81
|
-
if (filterStatements.length === 1) result = filterStatements[0];
|
|
82
|
-
else if (filterStatements.length > 1) {
|
|
83
|
-
result = `(${filterStatements.join(` ${statement.join} `)})`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return `${statement.not ? '(not ' : ''}${result}${statement.not ? ')' : ''}`;
|
|
87
|
-
}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import assert from 'assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import { IsAzuriteRunning } from "./common";
|
|
4
|
-
import { ODataOperators } from "./odata";
|
|
5
|
-
import { ConfigureTableStorage, Table, addItem, ensureTable, overflowColumnValue, restoreOverflowColumnValue } from "./table-storage";
|
|
6
|
-
|
|
7
|
-
test('testTableStorage', async t => {
|
|
8
|
-
if (!(await IsAzuriteRunning())) {
|
|
9
|
-
console.log('Skipping table-storage tests - azurite is not running.');
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
//configure storage
|
|
13
|
-
ConfigureTableStorage({ connectionString: "UseDevelopmentStorage=true" });
|
|
14
|
-
|
|
15
|
-
const tableName = "TestTable";
|
|
16
|
-
const table = new Table(tableName, {
|
|
17
|
-
getKeys: p => p,
|
|
18
|
-
transform: {
|
|
19
|
-
load: v => ({ ...v, schedule: JSON.parse(v.schedule) }),
|
|
20
|
-
save: v => ({ ...v, schedule: JSON.stringify(v.schedule) })
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
const partitionKey = "PK1";
|
|
24
|
-
const partitionKey2 = "PK2";
|
|
25
|
-
const rowKey1 = "0b8612bb-377d-4897-be8f-a532a1ad3e73|d2e32b93-c096-431c-8a79-fcce9a4d7bce";
|
|
26
|
-
const rowKey2 = "0b8612bb-377d-4897-be8f-a532a1ad3e73|cd24aa15-0d03-4892-bb46-af32e5fb9361";
|
|
27
|
-
//start fresh
|
|
28
|
-
let result = await table.delete();
|
|
29
|
-
//don't test this - it might have a table to delete and might not... depending if cleanup didn't work last run
|
|
30
|
-
//await t.test("Delete table", t => assert.strictEqual(result, true));
|
|
31
|
-
|
|
32
|
-
//provoke table name not allowed
|
|
33
|
-
result = await ensureTable("T2");
|
|
34
|
-
await t.test("Table name not allowed", t => assert.strictEqual(result, false));
|
|
35
|
-
|
|
36
|
-
//create table
|
|
37
|
-
result = await table.ensure();
|
|
38
|
-
await t.test("Create table", t => assert.strictEqual(result, true));
|
|
39
|
-
|
|
40
|
-
//provoke table already exists
|
|
41
|
-
result = await table.ensure();
|
|
42
|
-
await t.test("Ensure existing table", t => assert.strictEqual(result, true));
|
|
43
|
-
|
|
44
|
-
//add item
|
|
45
|
-
result = await table.addItem({
|
|
46
|
-
partitionKey: partitionKey,
|
|
47
|
-
rowKey: rowKey1,
|
|
48
|
-
list: "1bd32599-375e-4c98-9e1c-ce9dd2f2e17d",
|
|
49
|
-
action: "6b964dd7-6e62-42f6-89e4-5e79d6b6cbb0",
|
|
50
|
-
nextUTC: "2023010105",
|
|
51
|
-
schedule: { days: 4, hours: [1, 2, 3] }
|
|
52
|
-
});
|
|
53
|
-
await t.test("Create item 1", t => assert.strictEqual(result, true));
|
|
54
|
-
|
|
55
|
-
result = await table.addItem({
|
|
56
|
-
partitionKey: partitionKey,
|
|
57
|
-
rowKey: rowKey2,
|
|
58
|
-
list: "1bd32599-375e-4c98-9e1c-ce9dd2f2e17d",
|
|
59
|
-
action: "fab4094b-c000-4ff4-bb52-0f8538b2bc49",
|
|
60
|
-
nextUTC: "2023010105",
|
|
61
|
-
schedule: { days: 4, hours: [1, 2, 3] }
|
|
62
|
-
});
|
|
63
|
-
await t.test("Create item 2", t => assert.strictEqual(result, true));
|
|
64
|
-
|
|
65
|
-
result = await table.addItem({
|
|
66
|
-
partitionKey: partitionKey2,
|
|
67
|
-
rowKey: rowKey1,
|
|
68
|
-
list: "b98211e2-809a-4761-b928-559eddc28dfc",
|
|
69
|
-
action: "d20196e5-feb9-4202-bd03-718b49bf029e",
|
|
70
|
-
nextUTC: "2023010105",
|
|
71
|
-
schedule: { days: 4, hours: [1, 2, 3] }
|
|
72
|
-
});
|
|
73
|
-
await t.test("Create item in second partition", t => assert.strictEqual(result, true));
|
|
74
|
-
|
|
75
|
-
//provoke add second item with same key
|
|
76
|
-
result = await table.addItem({
|
|
77
|
-
partitionKey: partitionKey,
|
|
78
|
-
rowKey: rowKey1,
|
|
79
|
-
list: "1bd32599-375e-4c98-9e1c-ce9dd2f2e17d",
|
|
80
|
-
action: "6b964dd7-6e62-42f6-89e4-5e79d6b6cbb0",
|
|
81
|
-
nextUTC: "2023010105",
|
|
82
|
-
schedule: { days: 4, hours: [1, 2, 3] }
|
|
83
|
-
});
|
|
84
|
-
await t.test("Create item with existing key", t => assert.strictEqual(result, false));
|
|
85
|
-
|
|
86
|
-
//provoke add second item with same key
|
|
87
|
-
result = await table.upsertItem({
|
|
88
|
-
partitionKey: partitionKey,
|
|
89
|
-
rowKey: rowKey1,
|
|
90
|
-
list: "1bd32599-375e-4c98-9e1c-ce9dd2f2e17d",
|
|
91
|
-
action: "6b964dd7-6e62-42f6-89e4-5e79d6b6cbb0",
|
|
92
|
-
nextUTC: "2023020202",
|
|
93
|
-
schedule: { days: 4, hours: [1, 2, 3] }
|
|
94
|
-
});
|
|
95
|
-
await t.test("Update item 1", t => assert.strictEqual(result, true));
|
|
96
|
-
|
|
97
|
-
//provoke add item to table that does not exist
|
|
98
|
-
result = await addItem(tableName + "DoeNotExist", {
|
|
99
|
-
partitionKey: partitionKey,
|
|
100
|
-
rowKey: rowKey1,
|
|
101
|
-
list: "1bd32599-375e-4c98-9e1c-ce9dd2f2e17d",
|
|
102
|
-
action: "6b964dd7-6e62-42f6-89e4-5e79d6b6cbb0",
|
|
103
|
-
nextUTC: "2023020202",
|
|
104
|
-
schedule: { days: 4, hours: [1, 2, 3] }
|
|
105
|
-
});
|
|
106
|
-
await t.test("Create item in table that does not exist", t => assert.strictEqual(result, false));
|
|
107
|
-
|
|
108
|
-
let allItems = await table.getItems();
|
|
109
|
-
await t.test("Get items", t => assert.strictEqual(allItems.length, 3));
|
|
110
|
-
|
|
111
|
-
//find items by partition
|
|
112
|
-
let partitionItems = await table.getItems({
|
|
113
|
-
filterStatment: {
|
|
114
|
-
filters: [
|
|
115
|
-
{
|
|
116
|
-
property: "partitionKey",
|
|
117
|
-
operator: ODataOperators.equal,
|
|
118
|
-
value: partitionKey
|
|
119
|
-
}
|
|
120
|
-
]
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
await t.test("Get items with filter", t => assert.strictEqual(partitionItems.length, 2));
|
|
125
|
-
await t.test("Update values", t => assert.strictEqual(partitionItems[1].nextUTC, "2023020202"));
|
|
126
|
-
|
|
127
|
-
let deleteResult = await table.deleteItem({ partitionKey: partitionKey, rowKey: rowKey1 });
|
|
128
|
-
await t.test("delete item", t => assert.strictEqual(deleteResult, true));
|
|
129
|
-
deleteResult = await table.deleteItem({ partitionKey: partitionKey, rowKey: rowKey1 });
|
|
130
|
-
await t.test("delete item that was already deleted", t => assert.strictEqual(deleteResult, true));
|
|
131
|
-
|
|
132
|
-
//cleanup
|
|
133
|
-
result = await table.delete();
|
|
134
|
-
await t.test("Delete table", t => assert.strictEqual(result, true));
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
test('testTableStorage', async t => {
|
|
139
|
-
let item = { title: "test1", test: "abc123def456" };
|
|
140
|
-
let withOverflow = overflowColumnValue(item, "test", 3);
|
|
141
|
-
await t.test("Test overflow column", t => assert.strictEqual(JSON.stringify(withOverflow), JSON.stringify({
|
|
142
|
-
title: "test1",
|
|
143
|
-
test: "abc",
|
|
144
|
-
test2: "123",
|
|
145
|
-
test3: "def",
|
|
146
|
-
test4: "456"
|
|
147
|
-
})));
|
|
148
|
-
|
|
149
|
-
await t.test("Test restore overflow column", t => assert.strictEqual(JSON.stringify(restoreOverflowColumnValue(withOverflow, "test")), JSON.stringify(item)));
|
|
150
|
-
});
|
|
@@ -1,385 +0,0 @@
|
|
|
1
|
-
//https://www.npmjs.com/package/@azure/storage-blob
|
|
2
|
-
//https://www.npmjs.com/package/@azure/storage-queue
|
|
3
|
-
//https://www.npmjs.com/package/@azure/storage-file-share
|
|
4
|
-
//https://www.npmjs.com/package/@azure/data-tables
|
|
5
|
-
//https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite?tabs=visual-studio-code
|
|
6
|
-
import { FullOperationResponse } from "@azure/core-client";
|
|
7
|
-
import { ListTableEntitiesOptions, TableClient, TableServiceClient, UpdateMode, odata } from "@azure/data-tables";
|
|
8
|
-
import { IDictionary, isNotEmptyArray, isNotEmptyString, isNullOrEmptyString, isNullOrUndefined, isNumber, splitString } from "@kwiz/common";
|
|
9
|
-
import { IOdataFilterStatement, getOdataFilter } from "./odata";
|
|
10
|
-
|
|
11
|
-
var connectionString: string = null;
|
|
12
|
-
export function ConfigureTableStorage(config: { connectionString: string; }) {
|
|
13
|
-
connectionString = config.connectionString;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function getTableService() {
|
|
17
|
-
if (isNullOrEmptyString(connectionString)) throw Error("Call ConfigureTableStorage first");
|
|
18
|
-
return TableServiceClient.fromConnectionString(connectionString);
|
|
19
|
-
}
|
|
20
|
-
function getTableClient(tableName: string) {
|
|
21
|
-
if (isNullOrEmptyString(connectionString)) throw Error("Call ConfigureTableStorage first");
|
|
22
|
-
return TableClient.fromConnectionString(connectionString, tableName);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface IODataError {
|
|
26
|
-
odataError: {
|
|
27
|
-
code: string | "TableAlreadyExists" | "EntityAlreadyExists" | "TableNotFound" | "",
|
|
28
|
-
message: {
|
|
29
|
-
lang: string | "en-US",
|
|
30
|
-
value: string
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
export type TableOperationResult<T = never> = { success: true; result?: T; errorCode?: string; } | { success: false; result?: T; errorCode: string; };
|
|
35
|
-
|
|
36
|
-
type TableEntityBase = {
|
|
37
|
-
/**
|
|
38
|
-
* The PartitionKey property of the entity.
|
|
39
|
-
* Does not allow / \ # ? any control characted (like \n)
|
|
40
|
-
*/
|
|
41
|
-
partitionKey: string;
|
|
42
|
-
/**
|
|
43
|
-
* The RowKey property of the entity.
|
|
44
|
-
*/
|
|
45
|
-
rowKey: string;
|
|
46
|
-
}
|
|
47
|
-
//correctly limits the column types...
|
|
48
|
-
export type TableEntityType<DataType extends TableEntityBase> = {
|
|
49
|
-
[P in keyof DataType]: string | boolean | Date | number;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export async function findTable(tableName: string): Promise<TableOperationResult> {
|
|
53
|
-
const tableService = getTableService();
|
|
54
|
-
|
|
55
|
-
let success = false;
|
|
56
|
-
try {
|
|
57
|
-
const tables = tableService.listTables({
|
|
58
|
-
queryOptions: {
|
|
59
|
-
//Tag function - read more https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
|
|
60
|
-
filter: odata`TableName eq ${tableName}`
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
for await (const table of tables) {
|
|
64
|
-
success = true;
|
|
65
|
-
}
|
|
66
|
-
} catch (e) {
|
|
67
|
-
//console.log(e);
|
|
68
|
-
}
|
|
69
|
-
return { success, errorCode: success ? undefined : "Not found" };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export async function listTables() {
|
|
73
|
-
const tableService = getTableService();
|
|
74
|
-
|
|
75
|
-
let arr: string[] = [];
|
|
76
|
-
try {
|
|
77
|
-
const tables = tableService.listTables();
|
|
78
|
-
for await (const table of tables) {
|
|
79
|
-
arr.push(table.name);
|
|
80
|
-
}
|
|
81
|
-
} catch (e) {
|
|
82
|
-
//console.log(e);
|
|
83
|
-
}
|
|
84
|
-
return arr;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// async function ensureTableObsolete(name: string) {
|
|
88
|
-
// const tableService = getTableService();
|
|
89
|
-
// //create table
|
|
90
|
-
// // If the table 'newTable' already exists, createTable doesn't throw
|
|
91
|
-
// let success = false;
|
|
92
|
-
// try {
|
|
93
|
-
// await tableService.createTable(name, {
|
|
94
|
-
// onResponse: raw => {
|
|
95
|
-
// let error = isError(raw);
|
|
96
|
-
// success = error.isError !== true;
|
|
97
|
-
// if (error.isError) console.log(error.message);
|
|
98
|
-
// }
|
|
99
|
-
// });
|
|
100
|
-
// }
|
|
101
|
-
// catch (e) {
|
|
102
|
-
// success = false;
|
|
103
|
-
// }
|
|
104
|
-
|
|
105
|
-
// return success;
|
|
106
|
-
// }
|
|
107
|
-
|
|
108
|
-
export async function ensureTable(tableName: string): Promise<TableOperationResult> {
|
|
109
|
-
const table = getTableClient(tableName);
|
|
110
|
-
let success = false;
|
|
111
|
-
let errorCode: string;
|
|
112
|
-
try {
|
|
113
|
-
await table.createTable({
|
|
114
|
-
onResponse: raw => {
|
|
115
|
-
let error = isError(raw);
|
|
116
|
-
success = error.isError !== true || error.errorCode === "TableAlreadyExists";
|
|
117
|
-
if (error.isError) {
|
|
118
|
-
console.error(error.errorCode);
|
|
119
|
-
errorCode = error.errorCode;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
} catch (e) {
|
|
124
|
-
//console.error(e);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return { success, errorCode };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export async function deleteTable(tableName: string): Promise<TableOperationResult> {
|
|
131
|
-
const table = getTableClient(tableName);
|
|
132
|
-
let success = false;
|
|
133
|
-
let errorCode: string;
|
|
134
|
-
try {
|
|
135
|
-
await table.deleteTable({
|
|
136
|
-
onResponse: raw => {
|
|
137
|
-
let error = isError(raw);
|
|
138
|
-
success = error.isError !== true;
|
|
139
|
-
if (error.isError) {
|
|
140
|
-
console.error(error.errorCode);
|
|
141
|
-
errorCode = error.errorCode;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
} catch (e) {
|
|
146
|
-
//console.log(e);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return { success, errorCode };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
//https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/tables/data-tables/samples/v13/typescript/src/queryEntities.ts
|
|
153
|
-
export async function getItems<DataType extends TableEntityBase & TableEntityType<DataType>>(tableName: string, options?: {
|
|
154
|
-
filterStatment?: IOdataFilterStatement<DataType>;
|
|
155
|
-
postFilter?: (item: DataType) => boolean;
|
|
156
|
-
}): Promise<TableOperationResult<DataType[]>> {
|
|
157
|
-
const table = getTableClient(tableName);
|
|
158
|
-
let success = true;
|
|
159
|
-
let errorCode: string;
|
|
160
|
-
let result: DataType[] = [];
|
|
161
|
-
try {
|
|
162
|
-
let o: ListTableEntitiesOptions;
|
|
163
|
-
if (options) {
|
|
164
|
-
if (!isNullOrUndefined(options.filterStatment)) {
|
|
165
|
-
let filterStatment = getOdataFilter(options.filterStatment);
|
|
166
|
-
if (!isNullOrEmptyString(filterStatment))
|
|
167
|
-
o = {
|
|
168
|
-
queryOptions: {
|
|
169
|
-
filter: filterStatment
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
let items = table.listEntities<DataType>(o);
|
|
175
|
-
for await (const item of items) {
|
|
176
|
-
//console.dir(item);
|
|
177
|
-
if (!options || typeof options.postFilter !== "function" || options.postFilter(item))
|
|
178
|
-
result.push(item as DataType);
|
|
179
|
-
}
|
|
180
|
-
} catch (e) {
|
|
181
|
-
//console.log(e);
|
|
182
|
-
success = false;
|
|
183
|
-
errorCode = "Could not get items";
|
|
184
|
-
}
|
|
185
|
-
return { success, errorCode, result };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export async function addItem<DataType extends TableEntityBase & TableEntityType<DataType>>(tableName: string, item: DataType): Promise<TableOperationResult> {
|
|
189
|
-
const table = getTableClient(tableName);
|
|
190
|
-
let success = false;
|
|
191
|
-
let errorCode: string;
|
|
192
|
-
try {
|
|
193
|
-
let result = await table.createEntity(item, {
|
|
194
|
-
onResponse: raw => {
|
|
195
|
-
let error = isError(raw);
|
|
196
|
-
success = error.isError !== true;
|
|
197
|
-
if (error.isError) {
|
|
198
|
-
console.error(error.errorCode);
|
|
199
|
-
errorCode = error.errorCode;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
//console.log(result);
|
|
204
|
-
success = true;
|
|
205
|
-
} catch (e) { success = false; }
|
|
206
|
-
return { success, errorCode };
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export async function deleteItem(tableName: string, partitionKey: string, rowKey: string): Promise<TableOperationResult> {
|
|
210
|
-
const table = getTableClient(tableName);
|
|
211
|
-
let success = false;
|
|
212
|
-
let errorCode: string;
|
|
213
|
-
|
|
214
|
-
try {
|
|
215
|
-
let result = await table.deleteEntity(partitionKey, rowKey, {
|
|
216
|
-
onResponse: raw => {
|
|
217
|
-
let error = isError(raw);
|
|
218
|
-
//error not found is still a success for delete action
|
|
219
|
-
success = error.isError !== true || error.errorCode === "ResourceNotFound";
|
|
220
|
-
if (error.isError) {
|
|
221
|
-
console.error(error.errorCode);
|
|
222
|
-
errorCode = error.errorCode;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
//console.log(result);
|
|
227
|
-
success = true;
|
|
228
|
-
} catch (e) {
|
|
229
|
-
//success = false;
|
|
230
|
-
}
|
|
231
|
-
return { success, errorCode };
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
export async function upsertItem<DataType extends TableEntityBase & TableEntityType<DataType>>(tableName: string, item: DataType, options?: {
|
|
235
|
-
mode?: UpdateMode
|
|
236
|
-
}): Promise<TableOperationResult> {
|
|
237
|
-
const table = getTableClient(tableName);
|
|
238
|
-
let success = false;
|
|
239
|
-
let errorCode: string;
|
|
240
|
-
try {
|
|
241
|
-
let result = await table.upsertEntity(item, options?.mode || "Replace", {
|
|
242
|
-
onResponse: raw => {
|
|
243
|
-
let error = isError(raw);
|
|
244
|
-
success = error.isError !== true;
|
|
245
|
-
if (error.isError) {
|
|
246
|
-
console.error(error.errorCode);
|
|
247
|
-
errorCode = error.errorCode;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
//console.log(result);
|
|
252
|
-
success = true;
|
|
253
|
-
} catch (e) {
|
|
254
|
-
console.error(e);
|
|
255
|
-
success = false;
|
|
256
|
-
}
|
|
257
|
-
return { success, errorCode };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
export class Table<KeysType extends TableEntityBase,
|
|
261
|
-
GetKeysParam,
|
|
262
|
-
SavedRow extends KeysType & TableEntityType<SavedRow>,
|
|
263
|
-
ParsedRow = SavedRow> {
|
|
264
|
-
private tableName: string;
|
|
265
|
-
private transform: {
|
|
266
|
-
save: (parsed: ParsedRow, table: Table<KeysType, GetKeysParam, SavedRow, ParsedRow>) => SavedRow;
|
|
267
|
-
load: (saved: SavedRow, table: Table<KeysType, GetKeysParam, SavedRow, ParsedRow>) => ParsedRow;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
public getKeys: (p: GetKeysParam) => KeysType = null;
|
|
271
|
-
|
|
272
|
-
/** If your type contains complex values, provide a transforer to serialize/deserialize those complex columns */
|
|
273
|
-
public constructor(tableName: string, options: {
|
|
274
|
-
getKeys: (p: GetKeysParam) => KeysType,
|
|
275
|
-
transform?: {
|
|
276
|
-
save: (parsed: ParsedRow, table: Table<KeysType, GetKeysParam, SavedRow, ParsedRow>) => SavedRow;
|
|
277
|
-
load: (saved: SavedRow, table: Table<KeysType, GetKeysParam, SavedRow, ParsedRow>) => ParsedRow;
|
|
278
|
-
}
|
|
279
|
-
}) {
|
|
280
|
-
this.tableName = tableName;
|
|
281
|
-
this.getKeys = options.getKeys;
|
|
282
|
-
this.transform = options.transform || {
|
|
283
|
-
save: v => v as any as SavedRow,
|
|
284
|
-
load: v => v as any as ParsedRow
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
public delete() {
|
|
288
|
-
return deleteTable(this.tableName);
|
|
289
|
-
}
|
|
290
|
-
public ensure() {
|
|
291
|
-
return ensureTable(this.tableName);
|
|
292
|
-
}
|
|
293
|
-
public async getItems(options?: {
|
|
294
|
-
filterStatment?: IOdataFilterStatement<SavedRow>;
|
|
295
|
-
postFilter?: (item: SavedRow) => boolean;
|
|
296
|
-
}) {
|
|
297
|
-
let itemsResult = await getItems<SavedRow>(this.tableName, options);
|
|
298
|
-
return { ...itemsResult, result: itemsResult.result.map(i => this.transform.load(i, this)) };
|
|
299
|
-
}
|
|
300
|
-
public async addItem(item: ParsedRow) {
|
|
301
|
-
await this.ensure();
|
|
302
|
-
|
|
303
|
-
return addItem<SavedRow>(this.tableName, this.transform.save(item, this));
|
|
304
|
-
}
|
|
305
|
-
public async upsertItem(item: ParsedRow, options?: {
|
|
306
|
-
mode?: UpdateMode
|
|
307
|
-
}) {
|
|
308
|
-
await this.ensure();
|
|
309
|
-
|
|
310
|
-
return upsertItem<SavedRow>(this.tableName, this.transform.save(item, this), options);
|
|
311
|
-
}
|
|
312
|
-
public deleteItemByKey(param: GetKeysParam) {
|
|
313
|
-
let keys = this.getKeys(param);
|
|
314
|
-
return this.deleteItem(keys);
|
|
315
|
-
}
|
|
316
|
-
public deleteItem(item: KeysType) {
|
|
317
|
-
return deleteItem(this.tableName, item.partitionKey, item.rowKey);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function isError(raw: FullOperationResponse) {
|
|
322
|
-
let isError = false;
|
|
323
|
-
let errorCode: string = null;
|
|
324
|
-
|
|
325
|
-
//201 - table created successfully
|
|
326
|
-
//204 - entity created successfully
|
|
327
|
-
//409 - table/entity already exists
|
|
328
|
-
//400 - table name not allowed / Property value too large
|
|
329
|
-
//404 - TableNotFound when adding item
|
|
330
|
-
if (raw.status >= 400) {
|
|
331
|
-
isError = true;
|
|
332
|
-
let error = raw.parsedBody as IODataError;
|
|
333
|
-
errorCode = error && error.odataError && !isNullOrEmptyString(error.odataError.code)
|
|
334
|
-
? error.odataError.code
|
|
335
|
-
: `Unknown error`;//for some errors like table name too short, error code is empty
|
|
336
|
-
}
|
|
337
|
-
else {
|
|
338
|
-
isError = false;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
return { isError, errorCode };
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
export const tableColumnLengthLimit = 31999;
|
|
345
|
-
export function overflowColumnValue<T = IDictionary<any>>(item: T, longProps: string | string[], overrideLimit?: number): T {
|
|
346
|
-
let limit = isNumber(overrideLimit) && overrideLimit > 0 ? overrideLimit : tableColumnLengthLimit;
|
|
347
|
-
let columns: IDictionary<string> = {};
|
|
348
|
-
const properties = !Array.isArray(longProps) ? [longProps] : longProps;
|
|
349
|
-
properties.forEach(propertyName => {
|
|
350
|
-
let value = item[propertyName];
|
|
351
|
-
if (isNotEmptyString(value)) {
|
|
352
|
-
let splitValue = splitString(value, { maxLength: limit });
|
|
353
|
-
splitValue.forEach((v, i) => {
|
|
354
|
-
//form, form2, form3...
|
|
355
|
-
let key = i === 0 ? propertyName : `${propertyName}${i + 1}`;
|
|
356
|
-
columns[key] = v;
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
if (isNotEmptyArray(Object.keys(columns)))
|
|
362
|
-
item = { ...item, ...columns };
|
|
363
|
-
|
|
364
|
-
return item;
|
|
365
|
-
}
|
|
366
|
-
export function restoreOverflowColumnValue<T = IDictionary<any>>(item: T, longProps: string | string[]): T {
|
|
367
|
-
const properties = !Array.isArray(longProps) ? [longProps] : longProps;
|
|
368
|
-
properties.forEach(propertyName => {
|
|
369
|
-
let result: string = item[propertyName];
|
|
370
|
-
if (isNotEmptyString(result)) {
|
|
371
|
-
let startIndex = 2;
|
|
372
|
-
let key = `${propertyName}${startIndex}`;
|
|
373
|
-
//restore from overflow
|
|
374
|
-
while (isNotEmptyString(item[key])) {
|
|
375
|
-
result += item[key];
|
|
376
|
-
delete item[key];
|
|
377
|
-
|
|
378
|
-
startIndex++;
|
|
379
|
-
key = `${propertyName}${startIndex}`;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
item[propertyName] = result;
|
|
383
|
-
});
|
|
384
|
-
return item;
|
|
385
|
-
}
|