@naanlang/naan 1.0.2 → 1.0.3

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.
Files changed (35) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +3 -3
  3. package/bin/index.js +2 -2
  4. package/dist/env_web.js +149 -20
  5. package/dist/naan.min.js +5 -5
  6. package/frameworks/browser/sworker.js +309 -91
  7. package/frameworks/browser/terminals.nlg +22 -9
  8. package/frameworks/browser/workers.nlg +19 -11
  9. package/frameworks/client/apiclient.nlg +17 -4
  10. package/frameworks/client/psm_client.nlg +116 -53
  11. package/frameworks/common/common.nlg +12 -2
  12. package/frameworks/node/apiserver.nlg +25 -13
  13. package/frameworks/node/filesystem.nlg +173 -30
  14. package/frameworks/node/psm_server.nlg +95 -32
  15. package/frameworks/project/build.nlg +40 -23
  16. package/frameworks/project/projects.nlg +47 -26
  17. package/frameworks/running/debugnub.nlg +2 -5
  18. package/frameworks/storage/csv.nlg +120 -0
  19. package/frameworks/storage/dbt_pouch.nlg +41 -25
  20. package/frameworks/storage/psm.nlg +108 -28
  21. package/frameworks/storage/psm_dbtables.nlg +7 -3
  22. package/frameworks/storage/resources.nlg +30 -2
  23. package/lib/browser/env_web.js +147 -18
  24. package/lib/browser/env_webworker.js +1 -1
  25. package/lib/browser/require.js +1 -1
  26. package/lib/core/naanlib.js +5 -5
  27. package/package.json +1 -1
  28. package/plugins/serviceAws/aws_cloudwatchlogs.nlg +1 -1
  29. package/plugins/serviceAws/aws_dynamo.nlg +625 -141
  30. package/plugins/serviceAws/aws_dynextra.nlg +294 -0
  31. package/plugins/serviceAws/dbt_aws.nlg +31 -11
  32. package/plugins/serviceAws/psm_aws.nlg +42 -26
  33. package/plugins/serviceAws/serviceAws.nlg +1 -0
  34. package/plugins/serviceGitHub/psm_github.nlg +41 -25
  35. package/plugins/serviceGitLab/psm_gitlab.nlg +41 -25
@@ -0,0 +1,294 @@
1
+ /*
2
+ * aws_dynextra.nlg
3
+ * serviceAws
4
+ *
5
+ * Extra DynamoDB services for AWS.
6
+ *
7
+ * column positioning: // // !
8
+ *
9
+ * Copyright (c) 2022 by Richard C. Zulch
10
+ *
11
+ */
12
+
13
+
14
+ /*
15
+ * dynexBatchGetRecords
16
+ *
17
+ * Retrieve records from one or more tables using arrays of keys. The results are put back in a
18
+ * dictionary with the same shape as the source. The result is a standard `(error, data) tuple.
19
+ * The request/response layout is as follows:
20
+ * [
21
+ * {
22
+ * table: <DynaTable object> // table to act upon
23
+ * keys: [<hash-range tuple>] // records to get
24
+ * data: [<retrieved records>] // received records in reply
25
+ * },
26
+ * { <next-table> }
27
+ * ]
28
+ * The operations are processed in parallel batches, and the final result is a standard result
29
+ * tuple `(error, result). An error is only reported if we are unable to complete all operations.
30
+ *
31
+ */
32
+
33
+ function dynexBatchGetRecords(dyna, request,
34
+ local index, titem, tname, error, batches, activity, params, batchsize, primary) {
35
+ if !dyna.aws
36
+ return (list(Error("DynaTable.batchGetRecords: not logged into AWS")))
37
+ request = new(request) // allow local modifications
38
+ index = { }
39
+ for titem in request { // preflight all tables
40
+ tname = titem.table.tablename
41
+ if index[tname]
42
+ return (Error("DynaTable.batchGetRecords: duplicate table in request:", tname))
43
+ `(error) = titem.table.info()
44
+ if error
45
+ return (Error("DynaTable.batchGetRecords: table", tname, ":", error))
46
+ titem.data = []
47
+ index[tname] = titem
48
+ }
49
+ batches = []
50
+ activity = true
51
+ while activity { // loop until requests empty
52
+ params = {
53
+ RequestItems: { }
54
+ }
55
+ batchsize = 0
56
+ while :fillbatch activity { // loop until batch full
57
+ activity = false
58
+ for `(tname, titem) in index {
59
+ if titem.keys.length > 0 {
60
+ primary = titem.table.makeDynKey(titem.keys.unshift())
61
+ if params.RequestItems[tname].Keys
62
+ params.RequestItems[tname].Keys.push(primary)
63
+ else
64
+ params.RequestItems[tname] = {
65
+ Keys: [primary]
66
+ }
67
+ activity = true
68
+ if ++batchsize >= 100
69
+ break :fillbatch // our batch is full
70
+ } else
71
+ item.keys = undefined // clear for return result
72
+ }
73
+ }
74
+ if batchsize > 0
75
+ batches.push(params)
76
+ }
77
+ asyncArray(batches, 10, closure bgr(params, local delayer, pending, err, response, table) {
78
+ delayer = dyna.conv.genBackoffDelayer()
79
+ loop {
80
+ pending = new(nonce)
81
+ dyna.aws.batchGetItem(params, function(err, data) {
82
+ if err
83
+ debuglog("aws.batchGetItem failed", err)
84
+ pending.signal(err, data)
85
+ })
86
+ `(err, response) = pending.wait()
87
+ for `(tname, table) in response.Responses
88
+ for titem in table
89
+ index[tname].data.push(dyna.conv.recordDynToNaan(titem))
90
+ if response.UnprocessedKeys
91
+ params.RequestItems = response.UnprocessedKeys // still more to do
92
+ else
93
+ break // total success or failure
94
+ if delayer() > dyna.timeout {
95
+ if !error // too slow
96
+ error = err
97
+ break
98
+ }
99
+ }
100
+ }).wait()
101
+ if error
102
+ list(error)
103
+ else
104
+ list(false, request)
105
+ };
106
+
107
+
108
+ /*
109
+ * dynexBatchWriteRecords
110
+ *
111
+ * Put arrays of records into one or more tables, replacing or deleting any existing records with
112
+ * the same keys. This returns arrays of results coresponding to the input records, where each
113
+ * is a standard `(error, data) tuple. The request layout is as follows:
114
+ * [
115
+ * {
116
+ * table: <DynaTable object> // table to act upon
117
+ * delete: [<key tuples/records>] // records to delete (needing only keys)
118
+ * put: [<records>] // records to put
119
+ * },
120
+ * { <next-table> }
121
+ * ]
122
+ * The operations are processed in parallel batches, and the final result is a standard result
123
+ * tuple `(error, result). An error is only reported if we are unable to complete all operations.
124
+ *
125
+ */
126
+
127
+ closure dynexBatchWriteRecords(dyna, request, local batches, error, activity) {
128
+ if !dyna.aws
129
+ return (list(Error("DynaTable.batchWriteRecords: not logged into AWS")))
130
+
131
+ //
132
+ // addOp
133
+ // Add a put or delete operation to the parameters
134
+ //
135
+ function addOp(params, table, del, record, local request) {
136
+ if del { // deleting a record
137
+ if tuple(record)
138
+ record = table.makeDynKey(record) // caller provided hash/range key pair
139
+ else
140
+ request = { DeleteRequest: { Key: dyna.conv.recordNaanToDyn(record) } }
141
+ }
142
+ else // putting a record
143
+ request = { PutRequest: { Item: dyna.conv.recordNaanToDyn(record) } }
144
+ if params.RequestItems[table.tablename]
145
+ params.RequestItems[table.tablename].push(request)
146
+ else
147
+ params.RequestItems[table.tablename] = [request]
148
+ }
149
+
150
+ // transform into batches
151
+ //
152
+ // This is tricky because it wants each batch to use multiple tables for maximum parallelism,
153
+ // just in case AWS isn't already optimizing this. It takes a request from each table and
154
+ // combines them into a single batch repeatedly, until the batch is full or the tables in the
155
+ // request have no more entries.
156
+ //
157
+ request = new(request) // allow local modifications
158
+ batches = []
159
+ activity = true
160
+ while activity
161
+ let (params, batchsize, table) {
162
+ params = {
163
+ RequestItems: { }
164
+ }
165
+ batchsize = 0
166
+ while :fillbatch activity { // loop until no activity or batch full
167
+ activity = false
168
+ for table in request {
169
+ if table.delete.length > 0
170
+ addOp(params, table, true, table.delete.unshift())
171
+ else if table.put.length > 0
172
+ addOp(params, table, false, table.put.unshift())
173
+ else
174
+ continue
175
+ activity = true
176
+ if ++batchsize >= 25
177
+ break :fillbatch // our batch is full
178
+ }
179
+ }
180
+ if batchsize > 0
181
+ batches.push(params)
182
+ } ()
183
+ asyncArray(batches, 10, closure bwr(params, local delayer, pending, err, response) {
184
+ delayer = dyna.conv.genBackoffDelayer()
185
+ loop {
186
+ pending = new(nonce)
187
+ dyna.aws.batchWriteItem(params, function(err, data) {
188
+ if err
189
+ debuglog("aws.batchWriteItem failed", err)
190
+ pending.signal(err, data)
191
+ })
192
+ `(err, response) = pending.wait()
193
+ if response.UnprocessedKeys
194
+ params.RequestItems = response.UnprocessedKeys // still more to do
195
+ else
196
+ break // total success or failure
197
+ if delayer() > dyna.timeout {
198
+ if !error // too slow
199
+ error = err
200
+ break
201
+ }
202
+ }
203
+ }).wait()
204
+ if error
205
+ list(error)
206
+ else
207
+ list(false, { ok: true })
208
+ };
209
+
210
+
211
+
212
+ /*
213
+ * dynexTransactGetRecords
214
+ *
215
+ * Retrieve records from one or more tables using arrays of keys as a single transaction. The
216
+ * results are put back in an array with the same shape as the source. The request/response
217
+ * layout is as follows:
218
+ * [
219
+ * {
220
+ * table: <DynaTable object> // table to act upon
221
+ * keys: [<hash-range tuple>] // records to get
222
+ * data: [<retrieved records>] // received records in reply
223
+ * },
224
+ * { <next-table> }
225
+ * ]
226
+ * This returns a standard result tuple `(error, result). An error is only reported if we are
227
+ * unable to complete all operations.
228
+ *
229
+ */
230
+
231
+ closure dynexTransactGetRecords(dyna, request,
232
+ local index, titem, tname, error, params, pending, response) {
233
+ if !dyna.aws
234
+ return (list(Error("DynaTable.transactGetRecords: not logged into AWS")))
235
+ request = new(request) // allow local modifications
236
+ index = { }
237
+ for titem in request { // preflight all tables
238
+ tname = titem.table.tablename
239
+ if index[tname]
240
+ return (Error("DynaTable.batchGetRecords: duplicate table in request:", tname))
241
+ `(error) = titem.table.info()
242
+ if error
243
+ return (Error("DynaTable.batchGetRecords: table", tname, ":", error))
244
+ titem.data = []
245
+ index[tname] = titem
246
+ }
247
+ params = {
248
+ TransactItems: []
249
+ }
250
+ for `(tname, titem) in index {
251
+ while titem.keys.length > 0 {
252
+ params.TransactItems.push({
253
+ Get: {
254
+ TableName: tname
255
+ Key: makeDynKey(titem.keys.unshift())
256
+ }
257
+ })
258
+ }
259
+ titem.keys = undefined
260
+ }
261
+ pending = new(nonce)
262
+ dyna.aws.transactGetItems(params, function(err, data) {
263
+ if err
264
+ debuglog("aws.transactGetItems failed", err)
265
+ pending.signal(err, data)
266
+ })
267
+ `(error, response) = pending.wait()
268
+ if error
269
+ list(error)
270
+ else {
271
+ for titem in response.Responses {
272
+ tname = titem.Item.*.0
273
+ index[tname].data.push(dyna.conv.recordDynToNaan(titem.Item[tname]))
274
+ }
275
+ list(false, request)
276
+ }
277
+ };
278
+
279
+
280
+ /*
281
+ * dynexInit
282
+ *
283
+ * Initialize the component.
284
+ *
285
+ */
286
+
287
+ function dynexInit(local manifest) {
288
+ manifest = `(dynexBatchGetRecords, dynexBatchWriteRecords, dynexTransactGetRecords, dynexInit)
289
+
290
+ Naan.module.build(module.id, "aws_dynextra", function(modobj, compobj) {
291
+ compobj.manifest = manifest
292
+ require("./serviceAws.nlg")
293
+ })
294
+ } ();
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * column positioning: // // !
8
8
  *
9
- * Copyright (c) 2020-2021 by Richard C. Zulch
9
+ * Copyright (c) 2020-2022 by Richard C. Zulch
10
10
  *
11
11
  */
12
12
 
@@ -259,6 +259,8 @@ closure dawsBucket(s3, local bucket) {
259
259
  listOne()
260
260
  }
261
261
 
262
+ // finis
263
+
262
264
  list(false, bucket)
263
265
 
264
266
  };
@@ -502,7 +504,10 @@ closure dawsTable(database, path, local table) {
502
504
 
503
505
  // head
504
506
  //
505
- // Return the header of the specified record, i.e. everything except the content.
507
+ // Return the header of the specified record, i.e. everything except the content. This operates
508
+ // on directories (with names ending in slash) differently because AWS does not support calling
509
+ // headObject upon them. We simply want to confirm that the directory exists, so we list objects
510
+ // that have the name without the slash and we get a common prefix if it exists.
506
511
 
507
512
  table.head = closure head(key, callback, local id) {
508
513
  if !callback
@@ -510,15 +515,30 @@ closure dawsTable(database, path, local table) {
510
515
  id = makeID(key)
511
516
  if !string(id)
512
517
  return (asyncResult(callback, id)) // error from makeID
513
- database.bucket.get(id, true, function(error, resp) {
514
- if error
515
- error = Error("head failed", error, id, { status: error.statusCode })
516
- else {
517
- resp.key = key
518
- resp = s3resp2doc(resp)
519
- resp._id = id }
520
- callback(error, resp)
521
- })
518
+ if id.slice(-1) == "/"
519
+ database.bucket.list(id.slice(0,-1), true, function blcb(error, resp) {
520
+ if error
521
+ error = Error("head failed", error, id, { status: error.statusCode })
522
+ else if id != resp.CommonPrefixes.0.Prefix {
523
+ error = Error("head not found", id, resp.CommonPrefixes.0.Prefix, { status: 404 })
524
+ resp = false
525
+ }
526
+ else {
527
+ resp.key = key
528
+ resp = s3resp2doc(resp)
529
+ resp._id = id }
530
+ callback(error, resp)
531
+ })
532
+ else
533
+ database.bucket.get(id, true, function bgcb(error, resp) {
534
+ if error
535
+ error = Error("head failed", error, id, { status: error.statusCode })
536
+ else {
537
+ resp.key = key
538
+ resp = s3resp2doc(resp)
539
+ resp._id = id }
540
+ callback(error, resp)
541
+ })
522
542
  }
523
543
 
524
544
  // copy
@@ -50,36 +50,26 @@ closure psmaConnector(psm, local connClassID, connector, watch) {
50
50
  key: "bucketName" }
51
51
  ]
52
52
  }
53
-
54
- // hashResourceID
55
- //
56
- // Make a stable hash based on the resource credentials that we can persist or share between
57
- // different domains accessing the same resource.
58
- connector.hashResourceID = function hashResourceID(resource, local ident) {
59
- ident = resource.region.concat(resource.region, resource.bucketName)
60
- if resource.label
61
- ident = ident.concat(resource.label)
62
- HashSHA256(ident)
63
- }
64
53
 
65
54
  // findResource
66
55
  //
67
- // Find a resource with the specified contents, returning the resID or false. The loose criteria
68
- // is: if added, would the specified contents be redundant to an existing resource?
56
+ // Find a resource with the specified contents, returning the resID or false.
69
57
  connector.findResource = function findResource(resource, local resID, creds) {
70
58
  for resID in listResources().1 {
71
59
  creds = access(resID).1
72
- if resource.region == creds.region && resource.bucketName == creds.bucketName
73
- && (!resource.label || resource.label == creds.name)
60
+ if ((!resource.region || resource.region == creds.region)
61
+ && (!resource.bucketName || resource.bucketName == creds.bucketName)
62
+ && (!resource.label || resource.label == creds.name))
74
63
  return (resID)
75
64
  }
76
65
  false
77
66
  }
78
67
 
79
- // addResource
68
+ // writeResource
80
69
  //
81
- // Add credentials for a named resource. The label is a string. The resource is a dictionary
82
- // comprising these keys:
70
+ // Add or update credentials for a named resource. If no resID is specified this adds a new
71
+ // resource; otherwise it updates an existing one. The resource is a dictionary comprising these
72
+ // keys:
83
73
  // label - [optional] label we use for this resource
84
74
  // keyID - AWS IAM keyID
85
75
  // keySecret - AWS IAM keySecret
@@ -87,10 +77,13 @@ closure psmaConnector(psm, local connClassID, connector, watch) {
87
77
  // bucketName - AWS bucketName
88
78
  // hidden - [optional] don't enumerate to user
89
79
  // locked - [optional] don't allow user delete
90
- // The return value is (error, result) tuple. Error, if non-false, is a dictionary of field
91
- // keys and error diagnostic strings. The special key "label" refers to the label string.
92
- connector.addResource = function addResource(resource, local label, errors, creds, error, resID, data) {
93
- function badField(str) { !string(str) || str.trim().length == 0 }
80
+ // The return value is an (error, resID) tuple. Error, if non-false, is a dictionary of field
81
+ // keys and error diagnostic strings.
82
+ connector.writeResource = function writeResource(resource, resID,
83
+ local creds, errors, error, data, change) {
84
+ // badField - test for a bad string
85
+ function badField(str) { !string(str) || str.trim().length == 0 }
86
+
94
87
  creds = {
95
88
  keyID: resource.keyID,
96
89
  keySecret: resource.keySecret,
@@ -104,6 +97,8 @@ closure psmaConnector(psm, local connClassID, connector, watch) {
104
97
  creds.label = resource.region.concat("-", resource.bucketName)
105
98
  if badField(creds.label)
106
99
  errors.label = "invalid label"
100
+ else if !resID && findResource({ label: creds.label })
101
+ errors.label = "duplicate name"
107
102
  if badField(resource.keyID)
108
103
  errors.keyID = "required field"
109
104
  if badField(resource.keySecret)
@@ -119,14 +114,35 @@ closure psmaConnector(psm, local connClassID, connector, watch) {
119
114
  creds.hidden = resource.hidden
120
115
  if resource.locked
121
116
  creds.locked = resource.locked
122
- resID = hashResourceID(resource)
123
- `(error, data) = connector.vault.addResource(resID, creds)
117
+ if resID {
118
+ `(error, data) = connector.vault.updateResource(resID, creds)
119
+ change = { changed: [resID] }
120
+ } else {
121
+ resID = UUID()
122
+ `(error, data) = connector.vault.addResource(resID, creds)
123
+ change = { added: [resID] }
124
+ }
124
125
  if data {
125
- watch.notify(connClassID, { added: [resID] })
126
- data = resID }
126
+ watch.notify(connClassID, change)
127
+ data = resID
128
+ }
127
129
  }
128
130
  list(error, data)
129
131
  }
132
+
133
+ // addResource
134
+ //
135
+ // Add a new resource, returning a standard error tuple per writeResource.
136
+ connector.addResource = function addResource(resource) {
137
+ writeResource(resource)
138
+ }
139
+
140
+ // updateResource
141
+ //
142
+ // Update an existing resource, returning a standard error tuple per writeResource.
143
+ connector.updateResource = function updateResource(resID, resource) {
144
+ writeResource(resource, resID)
145
+ }
130
146
 
131
147
  // getResource
132
148
  //
@@ -24,6 +24,7 @@ function sawsInit(local manifest) {
24
24
  Naan.module.build(module.id, "serviceAws", function(modobj, compobj) {
25
25
  compobj.manifest = manifest
26
26
  require("frameworks/common").LiveImport()
27
+ require("./aws_utils.nlg")
27
28
  function sawsPostload() {
28
29
  if js.g
29
30
  awsSDK = js.r("aws-sdk")
@@ -45,41 +45,34 @@ closure phubConnector(psm, local connClassID, connector, watch) {
45
45
 
46
46
  // findResource
47
47
  //
48
- // Find a resource with the specified contents, returning the resID or false. The loose criteria
49
- // is: if added, would the specified contents be redundant to an existing resource?
48
+ // Find a resource with the specified contents, returning the resID or false.
50
49
  connector.findResource = function findResource(resource, local resID, creds) {
51
50
  for resID in listResources().1 {
52
51
  creds = access(resID).1
53
- if resource.userName == creds.userName
52
+ if ((!resource.label || resource.label == creds.label)
53
+ && (!resource.userName || resource.userName == creds.userName))
54
54
  return (resID)
55
55
  }
56
56
  false
57
57
  }
58
-
59
- // hashResourceID
60
- //
61
- // Make a stable hash based on the resource credentials that we can persist or share between
62
- // different domains accessing the same resource.
63
- connector.hashResourceID = function hashResourceID(resource, local ident) {
64
- ident = resource.userName
65
- if resource.label
66
- ident = ident.concat(resource.label)
67
- HashSHA256(ident)
68
- }
69
58
 
70
- // addResource
59
+ // writeResource
71
60
  //
72
- // Add credentials for a named resource. The label is a string. The resource is a dictionary
73
- // comprising these keys:
61
+ // Add or update credentials for a named resource. If no resID is specified this adds a new
62
+ // resource; otherwise it updates an existing one. The resource is a dictionary comprising these
63
+ // keys:
74
64
  // label - [optional] label we use for this resource
75
65
  // userName - GitHub user name
76
66
  // userPat - GitHUB Personal Access Token
77
67
  // hidden - [optional] don't enumerate to user
78
68
  // locked - [optional] don't allow user delete
79
- // The return value is (error, result) tuple. Error, if non-false, is a dictionary of field
80
- // keys and error diagnostic strings. The special key "label" refers to the label string.
81
- connector.addResource = function addResource(resource, local label, errors, creds, error, resID, data) {
82
- function badField(str) { !string(str) || str.trim().length == 0 }
69
+ // The return value is an (error, resID) tuple. Error, if non-false, is a dictionary of field
70
+ // keys and error diagnostic strings.
71
+ connector.writeResource = function writeResource(resource, resID,
72
+ local label, errors, creds, error, data) {
73
+ // badField - test for a bad string
74
+ function badField(str) { !string(str) || str.trim().length == 0 }
75
+
83
76
  creds = {
84
77
  userName: resource.userName,
85
78
  userPat: resource.userPat
@@ -91,6 +84,8 @@ closure phubConnector(psm, local connClassID, connector, watch) {
91
84
  creds.label = resource.region.concat("-", resource.bucketName)
92
85
  if badField(creds.label)
93
86
  errors.label = "invalid label"
87
+ else if !resID && findResource({ label: creds.label })
88
+ errors.label = "duplicate name"
94
89
  if badField(resource.userName)
95
90
  errors.userName = "required field"
96
91
  if badField(resource.userPat)
@@ -102,15 +97,36 @@ closure phubConnector(psm, local connClassID, connector, watch) {
102
97
  creds.hidden = resource.hidden
103
98
  if resource.locked
104
99
  creds.locked = resource.locked
105
- resID = hashResourceID(resource)
106
- `(error, data) = connector.vault.addResource(resID, creds)
100
+ if resID {
101
+ `(error, data) = connector.vault.updateResource(resID, creds)
102
+ change = { changed: [resID] }
103
+ } else {
104
+ resID = UUID()
105
+ `(error, data) = connector.vault.addResource(resID, creds)
106
+ change = { added: [resID] }
107
+ }
107
108
  if data {
108
- watch.notify(connClassID, { added: [resID] })
109
- data = resID }
109
+ watch.notify(connClassID, change)
110
+ data = resID
111
+ }
110
112
  }
111
113
  list(error, data)
112
114
  }
113
115
 
116
+ // addResource
117
+ //
118
+ // Add a new resource, returning a standard error tuple per writeResource.
119
+ connector.addResource = function addResource(resource) {
120
+ writeResource(resource)
121
+ }
122
+
123
+ // updateResource
124
+ //
125
+ // Update an existing resource, returning a standard error tuple per writeResource.
126
+ connector.updateResource = function updateResource(resID, resource) {
127
+ writeResource(resource, resID)
128
+ }
129
+
114
130
  // deleteResource
115
131
  //
116
132
  // Delete credentials for a resource