@kwiz/node 1.0.8 → 1.0.10

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.
@@ -0,0 +1 @@
1
+ {"filename":"C:\\Repos\\@kwiz\\node\\__azurite_db_blob__.json","collections":[{"name":"$SERVICES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{},"constraints":null,"uniqueNames":["accountName"],"transforms":{},"objType":"$SERVICES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$CONTAINERS_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"accountName":{"name":"accountName","dirty":false,"values":[]},"name":{"name":"name","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$CONTAINERS_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$BLOBS_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"accountName":{"name":"accountName","dirty":false,"values":[]},"containerName":{"name":"containerName","dirty":false,"values":[]},"name":{"name":"name","dirty":false,"values":[]},"snapshot":{"name":"snapshot","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$BLOBS_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$BLOCKS_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"accountName":{"name":"accountName","dirty":false,"values":[]},"containerName":{"name":"containerName","dirty":false,"values":[]},"blobName":{"name":"blobName","dirty":false,"values":[]},"name":{"name":"name","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$BLOCKS_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]}],"databaseVersion":1.5,"engineVersion":1.5,"autosave":true,"autosaveInterval":5000,"autosaveHandle":null,"throttledSaves":true,"options":{"persistenceMethod":"fs","autosave":true,"autosaveInterval":5000,"serializationMethod":"normal","destructureDelimiter":"$<\n"},"persistenceMethod":"fs","persistenceAdapter":null,"verbose":false,"events":{"init":[null],"loaded":[],"flushChanges":[],"close":[],"changes":[],"warning":[]},"ENV":"NODEJS"}
@@ -0,0 +1 @@
1
+ {"filename":"C:\\Repos\\@kwiz\\node\\__azurite_db_blob_extent__.json","collections":[{"name":"$EXTENTS_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"id":{"name":"id","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$EXTENTS_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]}],"databaseVersion":1.5,"engineVersion":1.5,"autosave":true,"autosaveInterval":5000,"autosaveHandle":null,"throttledSaves":true,"options":{"persistenceMethod":"fs","autosave":true,"autosaveInterval":5000,"serializationMethod":"normal","destructureDelimiter":"$<\n"},"persistenceMethod":"fs","persistenceAdapter":null,"verbose":false,"events":{"init":[null],"loaded":[],"flushChanges":[],"close":[],"changes":[],"warning":[]},"ENV":"NODEJS"}
@@ -0,0 +1 @@
1
+ {"filename":"C:\\Repos\\@kwiz\\node\\__azurite_db_queue__.json","collections":[{"name":"$SERVICES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{},"constraints":null,"uniqueNames":["accountName"],"transforms":{},"objType":"$SERVICES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$QUEUES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"accountName":{"name":"accountName","dirty":false,"values":[]},"name":{"name":"name","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$QUEUES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$MESSAGES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"accountName":{"name":"accountName","dirty":false,"values":[]},"queueName":{"name":"queueName","dirty":false,"values":[]},"messageId":{"name":"messageId","dirty":false,"values":[]},"visibleTime":{"name":"visibleTime","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$MESSAGES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]}],"databaseVersion":1.5,"engineVersion":1.5,"autosave":true,"autosaveInterval":5000,"autosaveHandle":null,"throttledSaves":true,"options":{"persistenceMethod":"fs","autosave":true,"autosaveInterval":5000,"serializationMethod":"normal","destructureDelimiter":"$<\n"},"persistenceMethod":"fs","persistenceAdapter":null,"verbose":false,"events":{"init":[null],"loaded":[],"flushChanges":[],"close":[],"changes":[],"warning":[]},"ENV":"NODEJS"}
@@ -0,0 +1 @@
1
+ {"filename":"C:\\Repos\\@kwiz\\node\\__azurite_db_queue_extent__.json","collections":[{"name":"$EXTENTS_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"id":{"name":"id","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$EXTENTS_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]}],"databaseVersion":1.5,"engineVersion":1.5,"autosave":true,"autosaveInterval":5000,"autosaveHandle":null,"throttledSaves":true,"options":{"persistenceMethod":"fs","autosave":true,"autosaveInterval":5000,"serializationMethod":"normal","destructureDelimiter":"$<\n"},"persistenceMethod":"fs","persistenceAdapter":null,"verbose":false,"events":{"init":[null],"loaded":[],"flushChanges":[],"close":[],"changes":[],"warning":[]},"ENV":"NODEJS"}
@@ -0,0 +1 @@
1
+ {"filename":"C:\\Repos\\@kwiz\\node\\__azurite_db_table__.json","collections":[{"name":"$TABLES_COLLECTION$","data":[],"idIndex":[],"binaryIndices":{"account":{"name":"account","dirty":false,"values":[]},"table":{"name":"table","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$TABLES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":5,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$SERVICES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{},"constraints":null,"uniqueNames":["accountName"],"transforms":{},"objType":"$SERVICES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]}],"databaseVersion":1.5,"engineVersion":1.5,"autosave":true,"autosaveInterval":5000,"autosaveHandle":null,"throttledSaves":true,"options":{"persistenceMethod":"fs","autosave":true,"autosaveInterval":5000,"serializationMethod":"normal","destructureDelimiter":"$<\n"},"persistenceMethod":"fs","persistenceAdapter":null,"verbose":false,"events":{"init":[null],"loaded":[],"flushChanges":[],"close":[],"changes":[],"warning":[]},"ENV":"NODEJS"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kwiz/node",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "KWIZ utilities and helpers for node applications",
5
5
  "module": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -24,7 +24,9 @@
24
24
  "npm-publish": "npm publish --access public",
25
25
  "reset-repo": "git fetch origin && git reset --hard origin/main",
26
26
  "link-local-common": "npm link @kwiz/common",
27
- "reindex-project": "cti create ./src -i _dependencies -w -b -n -o exports-index.ts && node fix-folder-imports.js"
27
+ "reindex-project": "cti create ./src -i _dependencies -w -b -n -o exports-index.ts && node fix-folder-imports.js",
28
+ "install-packages": "npm install azurite -g && npm ci",
29
+ "startDevStorage": "azurite -s -l"
28
30
  },
29
31
  "repository": {
30
32
  "type": "git",
@@ -64,6 +66,7 @@
64
66
  "typescript": "^5.3.3"
65
67
  },
66
68
  "dependencies": {
69
+ "@azure/data-tables": "^13.2.2",
67
70
  "@azure/msal-node": "^2.6.4",
68
71
  "@kwiz/common": "^1.0.10",
69
72
  "axios": "^1.6.7",
@@ -1,4 +1,5 @@
1
1
  export * from './auth/exports-index';
2
2
  export * from './graph/exports-index';
3
3
  export * from './spo/exports-index';
4
+ export * from './storage/exports-index';
4
5
  export * from './axios';
@@ -0,0 +1,2 @@
1
+ export * from './odata';
2
+ export * from './table-storage';
@@ -0,0 +1,87 @@
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
+ ? `datetime'${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
+ }
@@ -0,0 +1,129 @@
1
+ import assert from 'assert/strict';
2
+ import test from 'node:test';
3
+ import { ODataOperators } from "./odata";
4
+ import { ConfigureTableStorage, Table, addItem, ensureTable } from "./table-storage";
5
+
6
+ test('testTableStorage', async t => {
7
+ //configure storage
8
+ ConfigureTableStorage({ connectionString: "UseDevelopmentStorage=true" });
9
+
10
+ const tableName = "TestTable";
11
+ const table = new Table(tableName, {
12
+ getKeys: p => p,
13
+ transform: {
14
+ load: v => ({ ...v, schedule: JSON.parse(v.schedule) }),
15
+ save: v => ({ ...v, schedule: JSON.stringify(v.schedule) })
16
+ }
17
+ });
18
+ const partitionKey = "PK1";
19
+ const partitionKey2 = "PK2";
20
+ const rowKey1 = "0b8612bb-377d-4897-be8f-a532a1ad3e73|d2e32b93-c096-431c-8a79-fcce9a4d7bce";
21
+ const rowKey2 = "0b8612bb-377d-4897-be8f-a532a1ad3e73|cd24aa15-0d03-4892-bb46-af32e5fb9361";
22
+ //start fresh
23
+ let result = await table.delete();
24
+ await t.test("Delete table", t => assert.strictEqual(result, true));
25
+
26
+ //provoke table name not allowed
27
+ result = await ensureTable("T2");
28
+ await t.test("Table name not allowed", t => assert.strictEqual(result, false));
29
+
30
+ //create table
31
+ result = await table.ensure();
32
+ await t.test("Create table", t => assert.strictEqual(result, true));
33
+
34
+ //provoke table already exists
35
+ result = await table.ensure();
36
+ await t.test("Ensure existing table", t => assert.strictEqual(result, true));
37
+
38
+ //add item
39
+ result = await table.addItem({
40
+ partitionKey: partitionKey,
41
+ rowKey: rowKey1,
42
+ list: "1bd32599-375e-4c98-9e1c-ce9dd2f2e17d",
43
+ action: "6b964dd7-6e62-42f6-89e4-5e79d6b6cbb0",
44
+ nextUTC: "2023010105",
45
+ schedule: { days: 4, hours: [1, 2, 3] }
46
+ });
47
+ await t.test("Create item 1", t => assert.strictEqual(result, true));
48
+
49
+ result = await table.addItem({
50
+ partitionKey: partitionKey,
51
+ rowKey: rowKey2,
52
+ list: "1bd32599-375e-4c98-9e1c-ce9dd2f2e17d",
53
+ action: "fab4094b-c000-4ff4-bb52-0f8538b2bc49",
54
+ nextUTC: "2023010105",
55
+ schedule: { days: 4, hours: [1, 2, 3] }
56
+ });
57
+ await t.test("Create item 2", t => assert.strictEqual(result, true));
58
+
59
+ result = await table.addItem({
60
+ partitionKey: partitionKey2,
61
+ rowKey: rowKey1,
62
+ list: "b98211e2-809a-4761-b928-559eddc28dfc",
63
+ action: "d20196e5-feb9-4202-bd03-718b49bf029e",
64
+ nextUTC: "2023010105",
65
+ schedule: { days: 4, hours: [1, 2, 3] }
66
+ });
67
+ await t.test("Create item in second partition", t => assert.strictEqual(result, true));
68
+
69
+ //provoke add second item with same key
70
+ result = await table.addItem({
71
+ partitionKey: partitionKey,
72
+ rowKey: rowKey1,
73
+ list: "1bd32599-375e-4c98-9e1c-ce9dd2f2e17d",
74
+ action: "6b964dd7-6e62-42f6-89e4-5e79d6b6cbb0",
75
+ nextUTC: "2023010105",
76
+ schedule: { days: 4, hours: [1, 2, 3] }
77
+ });
78
+ await t.test("Create item with existing key", t => assert.strictEqual(result, false));
79
+
80
+ //provoke add second item with same key
81
+ result = await table.upsertItem({
82
+ partitionKey: partitionKey,
83
+ rowKey: rowKey1,
84
+ list: "1bd32599-375e-4c98-9e1c-ce9dd2f2e17d",
85
+ action: "6b964dd7-6e62-42f6-89e4-5e79d6b6cbb0",
86
+ nextUTC: "2023020202",
87
+ schedule: { days: 4, hours: [1, 2, 3] }
88
+ });
89
+ await t.test("Update item 1", t => assert.strictEqual(result, true));
90
+
91
+ //provoke add item to table that does not exist
92
+ result = await addItem(tableName + "DoeNotExist", {
93
+ partitionKey: partitionKey,
94
+ rowKey: rowKey1,
95
+ list: "1bd32599-375e-4c98-9e1c-ce9dd2f2e17d",
96
+ action: "6b964dd7-6e62-42f6-89e4-5e79d6b6cbb0",
97
+ nextUTC: "2023020202",
98
+ schedule: { days: 4, hours: [1, 2, 3] }
99
+ });
100
+ await t.test("Create item in table that does not exist", t => assert.strictEqual(result, false));
101
+
102
+ let allItems = await table.getItems();
103
+ await t.test("Get items", t => assert.strictEqual(allItems.length, 3));
104
+
105
+ //find items by partition
106
+ let partitionItems = await table.getItems({
107
+ filterStatment: {
108
+ filters: [
109
+ {
110
+ property: "partitionKey",
111
+ operator: ODataOperators.equal,
112
+ value: partitionKey
113
+ }
114
+ ]
115
+ }
116
+ });
117
+
118
+ await t.test("Get items with filter", t => assert.strictEqual(partitionItems.length, 2));
119
+ await t.test("Update values", t => assert.strictEqual(partitionItems[1].nextUTC, "2023020202"));
120
+
121
+ let deleteResult = await table.deleteItem({ partitionKey: partitionKey, rowKey: rowKey1 });
122
+ await t.test("delete item", t => assert.strictEqual(deleteResult, true));
123
+ deleteResult = await table.deleteItem({ partitionKey: partitionKey, rowKey: rowKey1 });
124
+ await t.test("delete item that was already deleted", t => assert.strictEqual(deleteResult, true));
125
+
126
+ //cleanup
127
+ result = await table.delete();
128
+ await t.test("Delete table", t => assert.strictEqual(result, true));
129
+ });
@@ -0,0 +1,314 @@
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 { isNullOrEmptyString, isNullOrUndefined } 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
+
35
+ type TableEntityBase = {
36
+ /**
37
+ * The PartitionKey property of the entity.
38
+ * Does not allow / \ # ? any control characted (like \n)
39
+ */
40
+ partitionKey: string;
41
+ /**
42
+ * The RowKey property of the entity.
43
+ */
44
+ rowKey: string;
45
+ }
46
+ //correctly limits the column types...
47
+ export type TableEntityType<DataType extends TableEntityBase> = {
48
+ [P in keyof DataType]: string | boolean | Date | number;
49
+ };
50
+
51
+ export async function findTable(tableName: string) {
52
+ const tableService = getTableService();
53
+
54
+ let found = false;
55
+ try {
56
+ const tables = tableService.listTables({
57
+ queryOptions: {
58
+ //Tag function - read more https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
59
+ filter: odata`TableName eq ${tableName}`
60
+ }
61
+ });
62
+ for await (const table of tables) {
63
+ found = true;
64
+ }
65
+ } catch (e) {
66
+ //console.log(e);
67
+ }
68
+ return found;
69
+ }
70
+
71
+ export async function listTables() {
72
+ const tableService = getTableService();
73
+
74
+ let arr: string[] = [];
75
+ try {
76
+ const tables = tableService.listTables();
77
+ for await (const table of tables) {
78
+ arr.push(table.name);
79
+ }
80
+ } catch (e) {
81
+ //console.log(e);
82
+ }
83
+ return arr;
84
+ }
85
+
86
+ // async function ensureTableObsolete(name: string) {
87
+ // const tableService = getTableService();
88
+ // //create table
89
+ // // If the table 'newTable' already exists, createTable doesn't throw
90
+ // let success = false;
91
+ // try {
92
+ // await tableService.createTable(name, {
93
+ // onResponse: raw => {
94
+ // let error = isError(raw);
95
+ // success = error.isError !== true;
96
+ // if (error.isError) console.log(error.message);
97
+ // }
98
+ // });
99
+ // }
100
+ // catch (e) {
101
+ // success = false;
102
+ // }
103
+
104
+ // return success;
105
+ // }
106
+
107
+ export async function ensureTable(tableName: string) {
108
+ const table = getTableClient(tableName);
109
+ let success = false;
110
+ try {
111
+ await table.createTable({
112
+ onResponse: raw => {
113
+ let error = isError(raw);
114
+ success = error.isError !== true || error.message === "TableAlreadyExists";
115
+ //if (error.isError) console.error(error.message);
116
+ }
117
+ });
118
+ } catch (e) {
119
+ //console.error(e);
120
+ }
121
+
122
+ return success;
123
+ }
124
+
125
+ export async function deleteTable(tableName: string) {
126
+ const table = getTableClient(tableName);
127
+ let success = false;
128
+ try {
129
+ await table.deleteTable({
130
+ onResponse: raw => {
131
+ let error = isError(raw);
132
+ success = error.isError !== true;
133
+ //if (error.isError) console.log(error.message);
134
+ }
135
+ });
136
+ } catch (e) {
137
+ //console.log(e);
138
+ }
139
+
140
+ return success;
141
+ }
142
+
143
+ //https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/tables/data-tables/samples/v13/typescript/src/queryEntities.ts
144
+ export async function getItems<DataType extends TableEntityBase & TableEntityType<DataType>>(tableName: string, options?: {
145
+ filterStatment?: IOdataFilterStatement<DataType>;
146
+ postFilter?: (item: DataType) => boolean;
147
+ }) {
148
+ const table = getTableClient(tableName);
149
+ let result: DataType[] = [];
150
+ try {
151
+ let o: ListTableEntitiesOptions;
152
+ if (options) {
153
+ if (!isNullOrUndefined(options.filterStatment)) {
154
+ let filterStatment = getOdataFilter(options.filterStatment);
155
+ if (!isNullOrEmptyString(filterStatment))
156
+ o = {
157
+ queryOptions: {
158
+ filter: filterStatment
159
+ }
160
+ };
161
+ }
162
+ }
163
+ let items = table.listEntities<DataType>(o);
164
+ for await (const item of items) {
165
+ //console.dir(item);
166
+ if (!options || typeof options.postFilter !== "function" || options.postFilter(item))
167
+ result.push(item as DataType);
168
+ }
169
+ } catch (e) {
170
+ //console.log(e);
171
+ }
172
+ return result;
173
+ }
174
+
175
+ export async function addItem<DataType extends TableEntityBase & TableEntityType<DataType>>(tableName: string, item: DataType) {
176
+ const table = getTableClient(tableName);
177
+ let success = false;
178
+ try {
179
+ let result = await table.createEntity(item, {
180
+ onResponse: raw => {
181
+ let error = isError(raw);
182
+ success = error.isError !== true;
183
+ //if (error.isError) console.log(error.message);
184
+ }
185
+ });
186
+ //console.log(result);
187
+ success = true;
188
+ } catch (e) { success = false; }
189
+ return success;
190
+ }
191
+
192
+ export async function deleteItem(tableName: string, partitionKey: string, rowKey: string) {
193
+ const table = getTableClient(tableName);
194
+ let success = false;
195
+ try {
196
+ let result = await table.deleteEntity(partitionKey, rowKey, {
197
+ onResponse: raw => {
198
+ let error = isError(raw);
199
+ success = error.isError !== true || error.message === "ResourceNotFound";
200
+ }
201
+ });
202
+ //console.log(result);
203
+ success = true;
204
+ } catch (e) {
205
+ //success = false;
206
+ }
207
+ return success;
208
+ }
209
+
210
+ export async function upsertItem<DataType extends TableEntityBase & TableEntityType<DataType>>(tableName: string, item: DataType, options?: {
211
+ mode?: UpdateMode
212
+ }) {
213
+ const table = getTableClient(tableName);
214
+ let success = false;
215
+ try {
216
+ let result = await table.upsertEntity(item, options?.mode || "Replace", {
217
+ onResponse: raw => {
218
+ let error = isError(raw);
219
+ success = error.isError !== true;
220
+ //if (error.isError) console.log(error.message);
221
+ }
222
+ });
223
+ //console.log(result);
224
+ success = true;
225
+ } catch (e) {
226
+ console.error(e);
227
+ success = false;
228
+ }
229
+ return success;
230
+ }
231
+
232
+ export class Table<KeysType extends TableEntityBase,
233
+ GetKeysParam,
234
+ SavedRow extends KeysType & TableEntityType<SavedRow>,
235
+ ParsedRow = SavedRow>{
236
+ private tableName: string;
237
+ private transform: {
238
+ save: (parsed: ParsedRow, table: Table<KeysType, GetKeysParam, SavedRow, ParsedRow>) => SavedRow;
239
+ load: (saved: SavedRow, table: Table<KeysType, GetKeysParam, SavedRow, ParsedRow>) => ParsedRow;
240
+ }
241
+
242
+ public getKeys: (p: GetKeysParam) => KeysType = null;
243
+
244
+ /** If your type contains complex values, provide a transforer to serialize/deserialize those complex columns */
245
+ public constructor(tableName: string, options: {
246
+ getKeys: (p: GetKeysParam) => KeysType,
247
+ transform?: {
248
+ save: (parsed: ParsedRow, table: Table<KeysType, GetKeysParam, SavedRow, ParsedRow>) => SavedRow;
249
+ load: (saved: SavedRow, table: Table<KeysType, GetKeysParam, SavedRow, ParsedRow>) => ParsedRow;
250
+ }
251
+ }) {
252
+ this.tableName = tableName;
253
+ this.getKeys = options.getKeys;
254
+ this.transform = options.transform || {
255
+ save: v => v as any as SavedRow,
256
+ load: v => v as any as ParsedRow
257
+ };
258
+ }
259
+ public delete() {
260
+ return deleteTable(this.tableName);
261
+ }
262
+ public ensure() {
263
+ return ensureTable(this.tableName);
264
+ }
265
+ public async getItems(options?: {
266
+ filterStatment?: IOdataFilterStatement<SavedRow>;
267
+ postFilter?: (item: SavedRow) => boolean;
268
+ }) {
269
+ let items = await getItems<SavedRow>(this.tableName, options);
270
+ return items.map(i => this.transform.load(i, this));
271
+ }
272
+ public async addItem(item: ParsedRow) {
273
+ await this.ensure();
274
+
275
+ return addItem<SavedRow>(this.tableName, this.transform.save(item, this));
276
+ }
277
+ public async upsertItem(item: ParsedRow, options?: {
278
+ mode?: UpdateMode
279
+ }) {
280
+ await this.ensure();
281
+
282
+ return upsertItem<SavedRow>(this.tableName, this.transform.save(item, this), options);
283
+ }
284
+ public deleteItemByKey(param: GetKeysParam) {
285
+ let keys = this.getKeys(param);
286
+ return this.deleteItem(keys);
287
+ }
288
+ public deleteItem(item: KeysType) {
289
+ return deleteItem(this.tableName, item.partitionKey, item.rowKey);
290
+ }
291
+ }
292
+
293
+ function isError(raw: FullOperationResponse) {
294
+ let isError = false;
295
+ let message: string = null;
296
+
297
+ //201 - table created successfully
298
+ //204 - entity created successfully
299
+ //409 - table/entity already exists
300
+ //400 - table name not allowed
301
+ //404 - TableNotFound when adding item
302
+ if (raw.status >= 400) {
303
+ isError = true;
304
+ let error = raw.parsedBody as IODataError;
305
+ message = error && error.odataError && !isNullOrEmptyString(error.odataError.code)
306
+ ? error.odataError.code
307
+ : `Unknown error`;//for some errors like table name too short, error code is empty
308
+ }
309
+ else {
310
+ isError = false;
311
+ }
312
+
313
+ return { isError, message };
314
+ }