@sap/cds 9.0.4 → 9.1.0
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/CHANGELOG.md +25 -0
- package/lib/compile/for/lean_drafts.js +29 -7
- package/lib/dbs/cds-deploy.js +5 -3
- package/lib/env/cds-requires.js +1 -1
- package/lib/env/defaults.js +0 -11
- package/lib/env/schemas/cds-rc.js +214 -6
- package/lib/req/request.js +1 -1
- package/lib/req/validate.js +1 -2
- package/lib/srv/middlewares/auth/xssec.js +1 -1
- package/lib/utils/inflect.js +2 -2
- package/lib/utils/tar.js +60 -23
- package/libx/_runtime/common/generic/crud.js +1 -3
- package/libx/_runtime/common/generic/input.js +2 -2
- package/libx/_runtime/common/generic/temporal.js +0 -6
- package/libx/_runtime/fiori/lean-draft.js +487 -141
- package/libx/_runtime/remote/utils/client.js +1 -0
- package/libx/odata/ODataAdapter.js +47 -43
- package/libx/odata/middleware/batch.js +0 -1
- package/libx/odata/middleware/error.js +7 -0
- package/libx/odata/middleware/operation.js +15 -21
- package/libx/odata/parse/afterburner.js +22 -8
- package/libx/odata/parse/grammar.peggy +182 -133
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/index.js +0 -35
- package/libx/odata/utils/metadata.js +34 -1
- package/libx/odata/utils/odataBind.js +2 -1
- package/libx/odata/utils/result.js +22 -20
- package/libx/queue/index.js +5 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@
|
|
|
4
4
|
- The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
5
5
|
- This project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## Version 9.1.0 - 2025-06-30
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- CDS config schema validations for `cds.requires.auth.tenants`, `cds.cdsc`, `cds.query`, `cds.log`, `cds.server`
|
|
12
|
+
- Queue option `targetPrefix` to prefix `target` value of `cds.outbox.Messages` entries for microservice isolation
|
|
13
|
+
- Basic support for CRUD for hierarchy entities
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Reduced the amount of SELECT nesting the OData adapter does for `$apply` queries.
|
|
18
|
+
- Better error messages for unresolved parent associations in hierarchy requests
|
|
19
|
+
- Enabled updated behavior of `draftActivate` to move updates to fields of draft enabled entities with type `cds.LargeBinary` from draft to active table on the database level, with feature flag `cds.env.fiori.move_media_data_in_db`.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Copies of `cds.context` with `locale`
|
|
24
|
+
- Support for relative paths in `@odata.bind`
|
|
25
|
+
- `cds build` on Windows OS - fixed cli tar usage for resources.tgz
|
|
26
|
+
- Actions and functions with scalar return types use same `@odata.context` calculation as other return types, fixing e.g. `cds.odata.contextAbsoluteUrl` not being respected
|
|
27
|
+
- Improve content-type and content-length handling in OData adapter
|
|
28
|
+
- Parsing incorrect function parameters
|
|
29
|
+
- `cds deploy --dry` no longer tries to load a DB adapter, so that it works w/o one installed.
|
|
30
|
+
- Fix `@mandatory` for actions and functions
|
|
31
|
+
|
|
7
32
|
## Version 9.0.4 - 2025-06-18
|
|
8
33
|
|
|
9
34
|
### Fixed
|
|
@@ -41,16 +41,14 @@ const { Draft } = cds.linked(`
|
|
|
41
41
|
}
|
|
42
42
|
`).definitions
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const draft = Object.create (active, {
|
|
44
|
+
function DraftEntity4(active, name = active.name + '.drafts') {
|
|
45
|
+
const draft = Object.create(active, {
|
|
48
46
|
name: { value: name }, // REVISIT: lots of things break if we do that!
|
|
49
47
|
elements: { value: { ...active.elements, ...Draft.elements }, enumerable: true },
|
|
50
48
|
actives: { value: active },
|
|
51
49
|
query: { value: undefined }, // to not inherit that from active
|
|
52
50
|
// drafts: { value: undefined }, // to not inherit that from active -> doesn't work yet as the coding in lean-draft.js uses .drafts to identify both active and draft entities
|
|
53
|
-
isDraft: { value: true }
|
|
51
|
+
isDraft: { value: true }
|
|
54
52
|
})
|
|
55
53
|
|
|
56
54
|
// for quoted names, we need to overwrite the cds.persistence.name of the derived, draft entity
|
|
@@ -60,7 +58,6 @@ function DraftEntity4 (active, name = active.name+'.drafts') {
|
|
|
60
58
|
return draft
|
|
61
59
|
}
|
|
62
60
|
|
|
63
|
-
|
|
64
61
|
module.exports = function cds_compile_for_lean_drafts(csn) {
|
|
65
62
|
function _redirect(assoc, target) {
|
|
66
63
|
assoc.target = target.name
|
|
@@ -153,7 +150,8 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
|
|
|
153
150
|
key === '@mandatory' ||
|
|
154
151
|
key === '@Common.FieldControl' && newEl[key]?.['#'] === 'Mandatory' ||
|
|
155
152
|
// key === '@Core.Immutable': Not allowed via UI anyway -> okay to cleanse them in PATCH
|
|
156
|
-
|
|
153
|
+
// REVISIT: Remove feature flag dependency: If active, validation errors will be degraded to messages and stored in draft admin data
|
|
154
|
+
(!active._service?.entities.DraftAdministrativeData.elements.DraftMessages && key.startsWith('@assert')) ||
|
|
157
155
|
key.startsWith('@PersonalData')
|
|
158
156
|
)
|
|
159
157
|
newEl[key] = undefined
|
|
@@ -162,6 +160,23 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
|
|
|
162
160
|
draft.elements[each] = newEl
|
|
163
161
|
}
|
|
164
162
|
|
|
163
|
+
// For list-report hierarchies, there must not be deep deletes w.r.t. recursive compositions
|
|
164
|
+
// Therefore, they are degraded to associations.
|
|
165
|
+
// Note: For object-page hiearchies, deep delete on draft recursive compositions is still needed.
|
|
166
|
+
if (draft.elements.LimitedDescendantCount && draft['@Common.DraftRoot.ActivationAction']) {
|
|
167
|
+
for (const c in draft.compositions) {
|
|
168
|
+
const comp = draft.compositions[c]
|
|
169
|
+
if (comp.target === draft.name) {
|
|
170
|
+
// modify comp to assoc
|
|
171
|
+
comp.type = 'cds.Association'
|
|
172
|
+
comp.isComposition = false
|
|
173
|
+
comp.is = function(kind) { return kind === 'Association' }
|
|
174
|
+
delete draft.compositions[c]
|
|
175
|
+
draft.associations[c] = comp
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
165
180
|
return draft
|
|
166
181
|
}
|
|
167
182
|
|
|
@@ -173,6 +188,13 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
|
|
|
173
188
|
def.elements.HasActiveEntity.virtual = true
|
|
174
189
|
if (def.elements.DraftAdministrativeData_DraftUUID) def.elements.DraftAdministrativeData_DraftUUID.virtual = true
|
|
175
190
|
def.elements.DraftAdministrativeData.virtual = true
|
|
191
|
+
if (def.elements.LimitedDescendantCount) {
|
|
192
|
+
// for hierarchies: make sure recursive compositions are not part of the draft tree
|
|
193
|
+
for (const c in def.compositions) {
|
|
194
|
+
const comp = def.compositions[c]
|
|
195
|
+
if (comp.target === def.name) comp['@odata.draft.ignore'] = true
|
|
196
|
+
}
|
|
197
|
+
}
|
|
176
198
|
// will insert drafts entities, so that others can use `.drafts` even without incoming draft requests
|
|
177
199
|
addDraftEntity(def, csn)
|
|
178
200
|
}
|
package/lib/dbs/cds-deploy.js
CHANGED
|
@@ -20,6 +20,9 @@ const deploy = module.exports = function cds_deploy (model, options, csvs) {
|
|
|
20
20
|
if (o.mocked) deploy.include_external_entities_in(model)
|
|
21
21
|
else deploy.exclude_external_entities_in(model)
|
|
22
22
|
|
|
23
|
+
// dry deployment (output schema only)
|
|
24
|
+
if (o.dry) return await deploy.schema ({model, options:{}}, model, o)
|
|
25
|
+
|
|
23
26
|
// prepare db
|
|
24
27
|
if (!db.run) db = await cds.connect.to(db)
|
|
25
28
|
if (!cds.db) cds.db = cds.services.db = db
|
|
@@ -34,10 +37,9 @@ const deploy = module.exports = function cds_deploy (model, options, csvs) {
|
|
|
34
37
|
|
|
35
38
|
// deploy schema and initial data...
|
|
36
39
|
try {
|
|
37
|
-
|
|
38
|
-
await _run (async tx => {
|
|
40
|
+
await db.run (async tx => {
|
|
39
41
|
let any = await deploy.schema (tx, model, o)
|
|
40
|
-
if (
|
|
42
|
+
if (any || csvs) await deploy.data (tx, model, o, csvs, file => LOG?.(GREY, ' > init from', local(file), RESET))
|
|
41
43
|
})
|
|
42
44
|
LOG?.('/> successfully deployed to', descr, '\n')
|
|
43
45
|
} catch (e) {
|
package/lib/env/cds-requires.js
CHANGED
|
@@ -157,7 +157,7 @@ const _queue = {
|
|
|
157
157
|
storeLastError: true,
|
|
158
158
|
timeout: '1h',
|
|
159
159
|
legacyLocking: true,
|
|
160
|
-
ignoredContext: ['user', 'http', 'model', 'timestamp']
|
|
160
|
+
ignoredContext: ['user', 'http', 'model', 'timestamp', '_locale', '_features']
|
|
161
161
|
},
|
|
162
162
|
// legacy
|
|
163
163
|
"in-memory-outbox": "in-memory-queue",
|
package/lib/env/defaults.js
CHANGED
|
@@ -2,17 +2,6 @@ const production = process.env.NODE_ENV === 'production'
|
|
|
2
2
|
|
|
3
3
|
module.exports = {
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* For our own tests to replace hard-coded checks for CDS_ENV === 'better-sqlite'
|
|
7
|
-
* which don't work anymore with cds8 where that is the default.
|
|
8
|
-
*/
|
|
9
|
-
get _better_sqlite() {
|
|
10
|
-
if (process.env.CDS_ENV === 'better-sqlite') return true
|
|
11
|
-
let conf = this.requires.db || this.requires.kinds.sql
|
|
12
|
-
if (conf?.impl === '@cap-js/sqlite') return true
|
|
13
|
-
else return false
|
|
14
|
-
},
|
|
15
|
-
|
|
16
5
|
production,
|
|
17
6
|
|
|
18
7
|
requires: require('./cds-requires'),
|
|
@@ -81,23 +81,23 @@ module.exports = {
|
|
|
81
81
|
folders: {
|
|
82
82
|
type: 'object',
|
|
83
83
|
default: {},
|
|
84
|
-
description: '
|
|
84
|
+
description: 'Overrides for default folders. Only override if you don\'t want to use the defaults \'app/\', \'db/\', \'srv/\'.',
|
|
85
85
|
additionalProperties: true,
|
|
86
86
|
properties: {
|
|
87
87
|
app: {
|
|
88
88
|
type: 'string',
|
|
89
89
|
format: 'uri-reference',
|
|
90
|
-
description: '
|
|
90
|
+
description: 'Applications, e.g. UI5 or Fiori apps'
|
|
91
91
|
},
|
|
92
92
|
db: {
|
|
93
93
|
type: 'string',
|
|
94
94
|
format: 'uri-reference',
|
|
95
|
-
description: '
|
|
95
|
+
description: 'Database models and migrations'
|
|
96
96
|
},
|
|
97
97
|
srv: {
|
|
98
98
|
type: 'string',
|
|
99
99
|
format: 'uri-reference',
|
|
100
|
-
description: '
|
|
100
|
+
description: 'Services'
|
|
101
101
|
}
|
|
102
102
|
},
|
|
103
103
|
patternProperties: {
|
|
@@ -168,7 +168,7 @@ module.exports = {
|
|
|
168
168
|
properties: {
|
|
169
169
|
kind: {
|
|
170
170
|
type: 'string',
|
|
171
|
-
description: '
|
|
171
|
+
description: 'Authentication kind.',
|
|
172
172
|
anyOf: [
|
|
173
173
|
{
|
|
174
174
|
$ref: '#/$defs/authType'
|
|
@@ -181,6 +181,27 @@ module.exports = {
|
|
|
181
181
|
users: {
|
|
182
182
|
$ref: '#/$defs/mockUsers'
|
|
183
183
|
},
|
|
184
|
+
tenants: {
|
|
185
|
+
type: 'object',
|
|
186
|
+
description: 'List of tenants with their respective features.',
|
|
187
|
+
additionalProperties: true,
|
|
188
|
+
patternProperties: {
|
|
189
|
+
'.+': {
|
|
190
|
+
type: 'object',
|
|
191
|
+
additionalProperties: true,
|
|
192
|
+
properties: {
|
|
193
|
+
features: {
|
|
194
|
+
type: 'array',
|
|
195
|
+
description: 'List of feature toggles for this tenant.',
|
|
196
|
+
uniqueItems: true,
|
|
197
|
+
items: {
|
|
198
|
+
type: 'string'
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
},
|
|
184
205
|
credentials: {
|
|
185
206
|
type: 'object',
|
|
186
207
|
description: 'You can explicitly configure credentials, but this is overruled by VCAP_SERVICES if a matching entry is found therein.',
|
|
@@ -645,6 +666,193 @@ module.exports = {
|
|
|
645
666
|
]
|
|
646
667
|
}
|
|
647
668
|
}
|
|
669
|
+
},
|
|
670
|
+
cdsc: {
|
|
671
|
+
type: 'object',
|
|
672
|
+
default: {},
|
|
673
|
+
description: 'CDS compiler configuration options.',
|
|
674
|
+
additionalProperties: true,
|
|
675
|
+
properties: {
|
|
676
|
+
moduleLookupDirectories: {
|
|
677
|
+
type: 'array',
|
|
678
|
+
description: 'List of directories to search for modules.',
|
|
679
|
+
uniqueItems: true,
|
|
680
|
+
items: {
|
|
681
|
+
type: 'string',
|
|
682
|
+
format: 'uri-reference'
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
query: {
|
|
688
|
+
type: 'object',
|
|
689
|
+
default: {},
|
|
690
|
+
description: 'Query configuration options.',
|
|
691
|
+
additionalProperties: true,
|
|
692
|
+
properties: {
|
|
693
|
+
limit: {
|
|
694
|
+
type: 'object',
|
|
695
|
+
description: 'Default limit for queries.',
|
|
696
|
+
additionalProperties: true,
|
|
697
|
+
properties: {
|
|
698
|
+
default: {
|
|
699
|
+
type: 'integer',
|
|
700
|
+
description: 'Default number of results returned by a query.',
|
|
701
|
+
minimum: 1
|
|
702
|
+
},
|
|
703
|
+
max: {
|
|
704
|
+
type: 'integer',
|
|
705
|
+
description: 'Maximum number of results returned by a query.',
|
|
706
|
+
minimum: 1
|
|
707
|
+
},
|
|
708
|
+
reliablePaging: {
|
|
709
|
+
type: 'boolean',
|
|
710
|
+
description: 'Enable reliable paging for queries.'
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
},
|
|
716
|
+
log: {
|
|
717
|
+
type: 'object',
|
|
718
|
+
default: {},
|
|
719
|
+
description: 'Logging configuration options.',
|
|
720
|
+
additionalProperties: true,
|
|
721
|
+
properties: {
|
|
722
|
+
format: {
|
|
723
|
+
type: 'string',
|
|
724
|
+
description: 'Log format, either \'plain\' or \'json\'.',
|
|
725
|
+
enum: [
|
|
726
|
+
'plain',
|
|
727
|
+
'json'
|
|
728
|
+
]
|
|
729
|
+
},
|
|
730
|
+
levels: {
|
|
731
|
+
type: 'object',
|
|
732
|
+
description: 'Log levels for different components.',
|
|
733
|
+
additionalProperties: {
|
|
734
|
+
type: 'string'
|
|
735
|
+
},
|
|
736
|
+
properties: {
|
|
737
|
+
cds: {
|
|
738
|
+
type: 'string',
|
|
739
|
+
description: 'Server and common output.'
|
|
740
|
+
},
|
|
741
|
+
cli: {
|
|
742
|
+
type: 'string',
|
|
743
|
+
description: 'CLI output.'
|
|
744
|
+
},
|
|
745
|
+
build: {
|
|
746
|
+
type: 'string',
|
|
747
|
+
description: 'CDS build output.'
|
|
748
|
+
},
|
|
749
|
+
app: {
|
|
750
|
+
type: 'string',
|
|
751
|
+
description: 'Application Service.'
|
|
752
|
+
},
|
|
753
|
+
db: {
|
|
754
|
+
type: 'string',
|
|
755
|
+
description: 'Databases.'
|
|
756
|
+
},
|
|
757
|
+
sql: {
|
|
758
|
+
type: 'string',
|
|
759
|
+
description: 'SQL output.'
|
|
760
|
+
},
|
|
761
|
+
messaging: {
|
|
762
|
+
type: 'string',
|
|
763
|
+
description: 'Messaging Service.'
|
|
764
|
+
},
|
|
765
|
+
remote: {
|
|
766
|
+
type: 'string',
|
|
767
|
+
description: 'Remote Service.'
|
|
768
|
+
},
|
|
769
|
+
'audit-log': {
|
|
770
|
+
type: 'string',
|
|
771
|
+
description: 'AuditLog Service.'
|
|
772
|
+
},
|
|
773
|
+
odata: {
|
|
774
|
+
type: 'string',
|
|
775
|
+
description: 'OData Protocol Adapter.'
|
|
776
|
+
},
|
|
777
|
+
rest: {
|
|
778
|
+
type: 'string',
|
|
779
|
+
description: 'REST Protocol Adapter.'
|
|
780
|
+
},
|
|
781
|
+
graphql: {
|
|
782
|
+
type: 'string',
|
|
783
|
+
description: 'GraphQL Protocol Adapter.'
|
|
784
|
+
},
|
|
785
|
+
auth: {
|
|
786
|
+
type: 'string',
|
|
787
|
+
description: 'Authentication.'
|
|
788
|
+
},
|
|
789
|
+
deploy: {
|
|
790
|
+
type: 'string',
|
|
791
|
+
description: 'Database Deployment.'
|
|
792
|
+
},
|
|
793
|
+
mtx: {
|
|
794
|
+
type: 'string',
|
|
795
|
+
description: 'Multitenancy and Extensibility.'
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
},
|
|
799
|
+
mask_headers: {
|
|
800
|
+
type: 'array',
|
|
801
|
+
description: 'List of header patterns to mask in logs.',
|
|
802
|
+
uniqueItems: true,
|
|
803
|
+
items: {
|
|
804
|
+
type: 'string'
|
|
805
|
+
},
|
|
806
|
+
default: [
|
|
807
|
+
'/authorization/i',
|
|
808
|
+
'/cookie/i',
|
|
809
|
+
'/cert/i',
|
|
810
|
+
'/ssl/i'
|
|
811
|
+
]
|
|
812
|
+
},
|
|
813
|
+
als_custom_fields: {
|
|
814
|
+
type: 'object',
|
|
815
|
+
description:
|
|
816
|
+
`Custom fields for Application Logging Service (ALS) in Kibana's error rendering.
|
|
817
|
+
Key is the index in the log message array.`,
|
|
818
|
+
additionalProperties: {
|
|
819
|
+
type: 'integer'
|
|
820
|
+
}
|
|
821
|
+
},
|
|
822
|
+
cls_custom_fields: {
|
|
823
|
+
type: 'array',
|
|
824
|
+
description:
|
|
825
|
+
`Custom fields for CLS in Kibana's error rendering.
|
|
826
|
+
Each entry is a field name.`,
|
|
827
|
+
uniqueItems: true,
|
|
828
|
+
items: {
|
|
829
|
+
type: 'string'
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
},
|
|
834
|
+
server: {
|
|
835
|
+
type: 'object',
|
|
836
|
+
default: {},
|
|
837
|
+
description: 'Server configuration options.',
|
|
838
|
+
additionalProperties: true,
|
|
839
|
+
properties: {
|
|
840
|
+
port: {
|
|
841
|
+
type: 'integer',
|
|
842
|
+
description: 'Port number for the server to listen on.',
|
|
843
|
+
minimum: 1,
|
|
844
|
+
maximum: 65535
|
|
845
|
+
},
|
|
846
|
+
cors: {
|
|
847
|
+
type: 'boolean',
|
|
848
|
+
description: 'Enable CORS support.',
|
|
849
|
+
default: true
|
|
850
|
+
},
|
|
851
|
+
index: {
|
|
852
|
+
type: 'boolean',
|
|
853
|
+
description: 'Serve the default index page.'
|
|
854
|
+
}
|
|
855
|
+
}
|
|
648
856
|
}
|
|
649
857
|
}
|
|
650
858
|
},
|
|
@@ -909,4 +1117,4 @@ module.exports = {
|
|
|
909
1117
|
}
|
|
910
1118
|
}
|
|
911
1119
|
}
|
|
912
|
-
}
|
|
1120
|
+
}
|
package/lib/req/request.js
CHANGED
|
@@ -110,7 +110,7 @@ class Request extends require('./event') {
|
|
|
110
110
|
delete err.stack
|
|
111
111
|
throw err
|
|
112
112
|
}
|
|
113
|
-
let e = this.
|
|
113
|
+
let e = this._errors.add(4, ...args)
|
|
114
114
|
if (!('stack' in e)) Error.captureStackTrace (e = Object.assign(new Error,e), this.reject)
|
|
115
115
|
if (!('message' in e)) e.message = String (e.code || e.status)
|
|
116
116
|
throw e
|
package/lib/req/validate.js
CHANGED
|
@@ -242,8 +242,7 @@ class action extends struct {
|
|
|
242
242
|
super.validate (data, path, ctx, this.params || {})
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
|
|
246
|
-
_is_mandatory(e) { return e.notNull && !e.default } // params
|
|
245
|
+
_is_mandatory(e) { return e.notNull && !e.default || e._is_mandatory() } // params
|
|
247
246
|
}
|
|
248
247
|
|
|
249
248
|
/** Managed associations are struct-like, with foreign keys as elements to validate. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
try {
|
|
2
2
|
const xssec = require('@sap/xssec')
|
|
3
|
-
module.exports = xssec
|
|
3
|
+
module.exports = xssec
|
|
4
4
|
} catch (e) {
|
|
5
5
|
if (e.code === 'MODULE_NOT_FOUND') e.message = `Cannot find '@sap/xssec'. Make sure to install it with 'npm i @sap/xssec'\n` + e.message
|
|
6
6
|
throw e
|
package/lib/utils/inflect.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
this.singular4 = (dn,stripped) => {
|
|
3
3
|
let n = dn.name || dn; if (stripped) n = n.match(last)[0]
|
|
4
4
|
return dn['@singular'] || (
|
|
5
|
-
/species|news$/i.test(n) ? n :
|
|
5
|
+
/(species|news)$/i.test(n) ? n :
|
|
6
6
|
/ess$/.test(n) ? n : // Address
|
|
7
7
|
/ees$/.test(n) ? n.slice(0, -1) : // Employees --> Employee
|
|
8
8
|
/[sz]es$/.test(n) ? n.slice(0, -2) :
|
|
@@ -15,7 +15,7 @@ this.singular4 = (dn,stripped) => {
|
|
|
15
15
|
this.plural4 = (dn,stripped) => {
|
|
16
16
|
let n = dn.name || dn; if (stripped) n = n.match(last)[0]
|
|
17
17
|
return dn['@plural'] || (
|
|
18
|
-
/analysis|status|species|sheep|news$/i.test(n) ? n :
|
|
18
|
+
/(analysis|status|species|sheep|news)$/i.test(n) ? n :
|
|
19
19
|
/[^aeiou]y$/.test(n) ? n.slice(0,-1) + 'ies' :
|
|
20
20
|
/(s|x|z|ch|sh)$/.test(n) ? n + 'es' :
|
|
21
21
|
n + 's'
|
package/lib/utils/tar.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const { PassThrough } = require('stream')
|
|
1
2
|
const child_process = require('child_process')
|
|
2
3
|
const spawn = /\btar\b/.test(process.env.DEBUG) ? (cmd, args, options) => {
|
|
3
4
|
Error.captureStackTrace(spawn,spawn)
|
|
@@ -8,26 +9,36 @@ const spawn = /\btar\b/.test(process.env.DEBUG) ? (cmd, args, options) => {
|
|
|
8
9
|
const cds = require('../index'), { fs, path, mkdirp, exists, rimraf } = cds.utils
|
|
9
10
|
const _resolve = (...x) => path.resolve (cds.root,...x)
|
|
10
11
|
|
|
11
|
-
//
|
|
12
|
+
// ======= ONLY_FOR_WINDOWS ======
|
|
13
|
+
// This section contains logic relevant for Windows OS.
|
|
14
|
+
|
|
15
|
+
// tar does not work properly on Windows w/o this change
|
|
12
16
|
const win = path => {
|
|
13
17
|
if (!path) return path
|
|
14
18
|
if (typeof path === 'string') return path.replace('C:', '//localhost/c$').replace(/\\+/g, '/')
|
|
15
19
|
if (Array.isArray(path)) return path.map(el => win(el))
|
|
16
20
|
}
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
// spawn tar on Windows, using the cli version
|
|
23
|
+
const winSpawnDir = (dir, args) => {
|
|
24
|
+
if (args.some(arg => arg === '-f')) return spawn ('tar', ['c', '-C', win(dir), ...win(args)])
|
|
25
|
+
else return spawn ('tar', ['cf', '-', '-C', win(dir), ...win(args)])
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// copy a directory recursively on Windows, using fs.promises
|
|
29
|
+
async function winCopyDir(src, dest) {
|
|
19
30
|
if ((await fs.promises.stat(src)).isDirectory()) {
|
|
20
31
|
const entries = await fs.promises.readdir(src)
|
|
21
|
-
return Promise.all(entries.map(async each =>
|
|
32
|
+
return Promise.all(entries.map(async each => winCopyDir(path.join(src, each), path.join(dest, each))))
|
|
22
33
|
} else {
|
|
23
34
|
await fs.promises.mkdir(path.dirname(dest), { recursive: true })
|
|
24
35
|
return fs.promises.copyFile(src, dest)
|
|
25
36
|
}
|
|
26
37
|
}
|
|
27
38
|
|
|
28
|
-
//
|
|
29
|
-
// cli tar has a size limit on Windows
|
|
30
|
-
const
|
|
39
|
+
// copy resources containing files and folders to temp dir on Windows
|
|
40
|
+
// cli tar has a size limit on Windows
|
|
41
|
+
const winCreateTemp = async (root, resources) => {
|
|
31
42
|
// Asynchronously copies the entire content from src to dest.
|
|
32
43
|
const temp = await fs.promises.mkdtemp(`${fs.realpathSync(require('os').tmpdir())}${path.sep}tar-`)
|
|
33
44
|
for (let resource of resources) {
|
|
@@ -43,7 +54,7 @@ const createTemp = async (root, resources) => {
|
|
|
43
54
|
await fs.promises.cp(resource, destination, { recursive: true })
|
|
44
55
|
} else {
|
|
45
56
|
// node < 16
|
|
46
|
-
await
|
|
57
|
+
await winCopyDir(resource, destination)
|
|
47
58
|
}
|
|
48
59
|
}
|
|
49
60
|
}
|
|
@@ -51,6 +62,43 @@ const createTemp = async (root, resources) => {
|
|
|
51
62
|
return temp
|
|
52
63
|
}
|
|
53
64
|
|
|
65
|
+
// spawn tar on Windows, using a temp dir, which is copied from the original dir
|
|
66
|
+
// cli tar has a size limit on Windows
|
|
67
|
+
const winSpawnTempDir = (dir, args) => {
|
|
68
|
+
// Synchronous trick: use a PassThrough as placeholder
|
|
69
|
+
const stdout = new PassThrough()
|
|
70
|
+
const stderr = new PassThrough()
|
|
71
|
+
const c = {
|
|
72
|
+
stdout,
|
|
73
|
+
stderr,
|
|
74
|
+
on: (...a) => { stdout.on(...a); stderr.on(...a); return c },
|
|
75
|
+
once: (...a) => { stdout.once(...a); stderr.once(...a); return c },
|
|
76
|
+
kill: () => {},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// async copy, then swap streams/events
|
|
80
|
+
winCreateTemp(dir, args.shift()).then(tempPath => {
|
|
81
|
+
const real = winSpawnDir(tempPath, args)
|
|
82
|
+
real.stdout.pipe(stdout)
|
|
83
|
+
real.stderr && real.stderr.pipe(stderr)
|
|
84
|
+
const cleanup = () => exists(tempPath) && rimraf(tempPath)
|
|
85
|
+
real.on('close', (...ev) => {
|
|
86
|
+
stdout.emit('close', ...ev)
|
|
87
|
+
stderr.emit('close', ...ev)
|
|
88
|
+
cleanup()
|
|
89
|
+
})
|
|
90
|
+
real.on('error', (...ev) => {
|
|
91
|
+
stdout.emit('error', ...ev)
|
|
92
|
+
stderr.emit('error', ...ev)
|
|
93
|
+
cleanup()
|
|
94
|
+
})
|
|
95
|
+
c.kill = (...ev) => real.kill(...ev)
|
|
96
|
+
})
|
|
97
|
+
return c
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ====== END ONLY_FOR_WINDOWS ======
|
|
101
|
+
|
|
54
102
|
const tarInfo = async (info) => {
|
|
55
103
|
let cmd, param
|
|
56
104
|
if (info === 'version') {
|
|
@@ -118,19 +166,12 @@ exports.create = (dir='.', ...args) => {
|
|
|
118
166
|
if (typeof dir === 'string') dir = _resolve(dir)
|
|
119
167
|
if (Array.isArray(dir)) [ dir, ...args ] = [ cds.root, dir, ...args ]
|
|
120
168
|
|
|
121
|
-
let c
|
|
169
|
+
let c
|
|
122
170
|
args = args.filter(el => el)
|
|
123
|
-
if (process.platform === 'win32') {
|
|
124
|
-
const spawnDir = (dir, args) => {
|
|
125
|
-
if (args.some(arg => arg === '-f')) return spawn ('tar', ['c', '-C', win(dir), ...win(args)])
|
|
126
|
-
else return spawn ('tar', ['cf', '-', '-C', win(dir), ...win(args)])
|
|
127
|
-
}
|
|
171
|
+
if (process.platform === 'win32') {
|
|
128
172
|
args.push('.')
|
|
129
|
-
if (Array.isArray(args[0]))
|
|
130
|
-
|
|
131
|
-
} else {
|
|
132
|
-
c = spawnDir(dir, args)
|
|
133
|
-
}
|
|
173
|
+
if (Array.isArray(args[0])) c = winSpawnTempDir(dir, args)
|
|
174
|
+
else c = winSpawnDir(dir, args)
|
|
134
175
|
} else {
|
|
135
176
|
if (Array.isArray(args[0])) {
|
|
136
177
|
args.push (...args.shift().map (f => path.isAbsolute(f) ? path.relative(dir,f) : f))
|
|
@@ -153,11 +194,7 @@ exports.create = (dir='.', ...args) => {
|
|
|
153
194
|
c.stdout.on('data', d => data.push(d))
|
|
154
195
|
c.stderr.on('data', d => stderr += d)
|
|
155
196
|
c.on('close', code => code ? reject(new Error(stderr)) : resolve(Buffer.concat(data)))
|
|
156
|
-
c.on('error', reject)
|
|
157
|
-
if (process.platform === 'win32') {
|
|
158
|
-
c.on('close', () => temp && exists(temp) && rimraf(temp))
|
|
159
|
-
c.on('error', () => temp && exists(temp) && rimraf(temp))
|
|
160
|
-
}
|
|
197
|
+
c.on('error', reject)
|
|
161
198
|
},
|
|
162
199
|
|
|
163
200
|
/**
|
|
@@ -78,9 +78,7 @@ module.exports = cds.service.impl(function () {
|
|
|
78
78
|
if (await _targetEntityDoesNotExist(req)) req.reject(404) // REVISIT: add a reasonable error message
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
if (req.constructor.name in { ODataRequest: 1 }) req._.readAfterWrite = true
|
|
83
|
-
if (req.protocol?.match(/odata/)) req._.readAfterWrite = true //> REVISIT for noah
|
|
81
|
+
if (req.protocol?.match(/odata/)) req._.readAfterWrite = true
|
|
84
82
|
|
|
85
83
|
return req.data
|
|
86
84
|
})
|
|
@@ -251,7 +251,7 @@ async function validate_input(req) {
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
const errs = cds.validate(req.data, req.target, assertOptions)
|
|
254
|
-
if (errs) return req.
|
|
254
|
+
if (errs) return errs.forEach(err => req.error(err))
|
|
255
255
|
|
|
256
256
|
// -------------------------------------------------
|
|
257
257
|
// REVISIT: is the below still needed?
|
|
@@ -332,7 +332,7 @@ function validate_action(req) {
|
|
|
332
332
|
protocol: req.protocol
|
|
333
333
|
}
|
|
334
334
|
let errs = cds.validate(data, operation, assertOptions)
|
|
335
|
-
if (errs) return req.
|
|
335
|
+
if (errs) return errs.forEach(err => req.error(err))
|
|
336
336
|
|
|
337
337
|
// REVISIT: we still need the following because cds.validate doesn't check for @mandatory params (both flat and nested)
|
|
338
338
|
const errors = []
|
|
@@ -76,12 +76,6 @@ function handle_temporal_data(req) {
|
|
|
76
76
|
_getDateFromQueryOptions(_queryOptions['sap-valid-to'] ?? normalizeTimestamp('9999-12-31T23:59:59.9999999Z'))
|
|
77
77
|
)
|
|
78
78
|
}
|
|
79
|
-
|
|
80
|
-
// REVISIT: needed without okra
|
|
81
|
-
if (req.constructor.name !== 'ODataRequest') {
|
|
82
|
-
req._['VALID-FROM'] = _['VALID-FROM']
|
|
83
|
-
req._['VALID-TO'] = _['VALID-TO']
|
|
84
|
-
}
|
|
85
79
|
}
|
|
86
80
|
handle_temporal_data._initial = true
|
|
87
81
|
|