@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kwiz/node",
3
- "version": "1.0.23",
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.97",
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
@@ -1,3 +0,0 @@
1
- {
2
- "fileExtensions": ["js", "ts"]
3
- }
@@ -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
- }
@@ -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
- });
@@ -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
- }
@@ -1,2 +0,0 @@
1
- export * from './discovery';
2
- export * from './msal';
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
- }
@@ -1,6 +0,0 @@
1
- export * from './SPO/exports-index';
2
- export * from './auth/exports-index';
3
- export * from './graph/exports-index';
4
- export * from './storage/exports-index';
5
- export * from './axios';
6
- export * from './get-with-cache';
@@ -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';
@@ -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";
@@ -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
- }
@@ -1,3 +0,0 @@
1
- export * from './common';
2
- export * from './odata';
3
- export * from './table-storage';
@@ -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
- }