Merge branch 'main' into audit-logging
This commit is contained in:
@@ -8,7 +8,7 @@
|
|||||||
"mocha": true
|
"mocha": true
|
||||||
},
|
},
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2018
|
"ecmaVersion": 2020
|
||||||
},
|
},
|
||||||
"globals": {
|
"globals": {
|
||||||
"SELECT": true,
|
"SELECT": true,
|
||||||
|
|||||||
2
.github/workflows/node.js.yml
vendored
2
.github/workflows/node.js.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [12.x, 14.x]
|
node-version: [16.x, 14.x, 12.x]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
// Incorporate pre-build extensions from...
|
|
||||||
using from '@capire/common';
|
|
||||||
24
bookshop/db/init.js
Normal file
24
bookshop/db/init.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* In order to keep basic bookshop sample as simple as possible, we don't add
|
||||||
|
* reuse dependencies. This db/init.js ensures we still have a minimum set of
|
||||||
|
* currencies, if not obtained through @capire/common.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = async (db)=>{
|
||||||
|
|
||||||
|
const has_common = db.model.definitions['sap.common.Currencies'].elements.numcode
|
||||||
|
if (has_common) return
|
||||||
|
|
||||||
|
const already_filled = await db.exists('sap.common.Currencies',{code:'EUR'})
|
||||||
|
if (already_filled) return
|
||||||
|
|
||||||
|
await INSERT.into ('sap.common.Currencies') .columns (
|
||||||
|
'code','symbol','name'
|
||||||
|
) .rows (
|
||||||
|
[ 'EUR','€','Euro' ],
|
||||||
|
[ 'USD','$','US Dollar' ],
|
||||||
|
[ 'GBP','£','British Pound' ],
|
||||||
|
[ 'ILS','₪','Shekel' ],
|
||||||
|
[ 'JPY','¥','Yen' ],
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "A simple self-contained bookshop service.",
|
"description": "A simple self-contained bookshop service.",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capire/common": "*",
|
|
||||||
"@sap/cds": "^5.0.4",
|
"@sap/cds": "^5.0.4",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"passport": "0.4.1"
|
"passport": "0.4.1"
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ class CatalogService extends cds.ApplicationService { init(){
|
|||||||
this.on ('submitOrder', async req => {
|
this.on ('submitOrder', async req => {
|
||||||
const {book,quantity} = req.data
|
const {book,quantity} = req.data
|
||||||
if (quantity < 1) return req.reject (400,`quantity has to be 1 or more`)
|
if (quantity < 1) return req.reject (400,`quantity has to be 1 or more`)
|
||||||
let {stock} = await SELECT `stock` .from (Books,book)
|
let b = await SELECT `stock` .from (Books,book)
|
||||||
|
if (!b) return req.error (404,`Book #${book} doesn't exist`)
|
||||||
|
let {stock} = b
|
||||||
if (quantity > stock) return req.reject (409,`${quantity} exceeds stock for book #${book}`)
|
if (quantity > stock) return req.reject (409,`${quantity} exceeds stock for book #${book}`)
|
||||||
await UPDATE (Books,book) .with ({ stock: stock -= quantity })
|
await UPDATE (Books,book) .with ({ stock: stock -= quantity })
|
||||||
await this.emit ('OrderedBook', { book, quantity, buyer:req.user.id })
|
await this.emit ('OrderedBook', { book, quantity, buyer:req.user.id })
|
||||||
|
|||||||
@@ -33,3 +33,6 @@ using from '@capire/orders/app/fiori';
|
|||||||
|
|
||||||
// Add data browser
|
// Add data browser
|
||||||
using from '@capire/data-viewer';
|
using from '@capire/data-viewer';
|
||||||
|
|
||||||
|
// Incorporate pre-build extensions from...
|
||||||
|
using from '@capire/common';
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ extend sap.common.Currencies with {
|
|||||||
* annotate sap.common.Countries with @cds.persistence.skip:false;
|
* annotate sap.common.Countries with @cds.persistence.skip:false;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
context sap.common_countries {
|
context sap.common.countries {
|
||||||
|
|
||||||
extend sap.common.Countries {
|
extend sap.common.Countries {
|
||||||
regions : Composition of many Regions on regions._parent = $self.code;
|
regions : Composition of many Regions on regions._parent = $self.code;
|
||||||
|
|||||||
@@ -2,12 +2,17 @@
|
|||||||
const GET = (url) => axios.get('/-data'+url)
|
const GET = (url) => axios.get('/-data'+url)
|
||||||
const storageGet = (key, def) => localStorage.getItem('data-viewer:'+key) || def
|
const storageGet = (key, def) => localStorage.getItem('data-viewer:'+key) || def
|
||||||
const storageSet = (key, val) => localStorage.setItem('data-viewer:'+key, val)
|
const storageSet = (key, val) => localStorage.setItem('data-viewer:'+key, val)
|
||||||
|
const columnKeysFirst = (c1, c2) => {
|
||||||
|
if (c1.isKey && !c2.isKey) return -1
|
||||||
|
if (!c1.isKey && c2.isKey) return 1
|
||||||
|
if (c1.isKey && c2.isKey) return c1.name.localeCompare(c2.name)
|
||||||
|
return 0 // retain natural order of normal columns
|
||||||
|
}
|
||||||
|
|
||||||
const viewer = new Vue ({
|
const vue = Vue.createApp ({
|
||||||
|
|
||||||
el:'#app',
|
data() { return {
|
||||||
|
error: undefined,
|
||||||
data: {
|
|
||||||
dataSource: storageGet('data-source', 'db'),
|
dataSource: storageGet('data-source', 'db'),
|
||||||
skip: storageGet('skip', 0),
|
skip: storageGet('skip', 0),
|
||||||
top: storageGet('top', 20),
|
top: storageGet('top', 20),
|
||||||
@@ -15,58 +20,100 @@ const viewer = new Vue ({
|
|||||||
entities: [],
|
entities: [],
|
||||||
columns: [],
|
columns: [],
|
||||||
data: [],
|
data: [],
|
||||||
rowDetails: undefined,
|
rowDetails: {},
|
||||||
},
|
rowKey: storageGet('rowKey')
|
||||||
|
}},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
dataSource: (v) => { storageSet('data-source', v); viewer.fetchEntities() },
|
dataSource: (v) => { storageSet('data-source', v); vue.fetchEntities() },
|
||||||
skip: (v) => { storageSet('skip', v); if (viewer.entity) viewer.fetchData() },
|
skip: (v) => { storageSet('skip', v); if (vue.entity) vue.fetchData() },
|
||||||
top: (v) => { storageSet('top', v); if (viewer.entity) viewer.fetchData() },
|
top: (v) => { storageSet('top', v); if (vue.entity) vue.fetchData() },
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
async fetchEntities () {
|
async fetchEntities () {
|
||||||
let url = `/Entities`
|
let url = `/Entities`
|
||||||
if (viewer.dataSource === 'db') url += `?dataSource=db`
|
if (vue.dataSource === 'db') url += `?dataSource=db`
|
||||||
const {data} = await GET(url)
|
const {data} = await GET(url)
|
||||||
viewer.entities = data.value
|
vue.entities = data.value
|
||||||
const entity = viewer.entity && viewer.entities.find(e => e.name === viewer.entity.name)
|
vue.entities.forEach(entity => entity.columns.sort(columnKeysFirst))
|
||||||
|
const entity = vue.entity && vue.entities.find(e => e.name === vue.entity.name)
|
||||||
if (entity) { // restore selection from previous fetch
|
if (entity) { // restore selection from previous fetch
|
||||||
viewer.columns = entity.columns
|
vue.columns = entity.columns
|
||||||
await viewer.fetchData(entity)
|
await vue.fetchData(entity)
|
||||||
} else {
|
} else {
|
||||||
viewer.entity = undefined
|
vue.entity = undefined
|
||||||
viewer.columns = []
|
vue.columns = []
|
||||||
viewer.data = []
|
vue.data = []
|
||||||
viewer.rowDetails = {}
|
vue.rowDetails = {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async inspectEntity (eve) {
|
async inspectEntity (eve) {
|
||||||
const entity = viewer.entity = viewer.entities [eve.currentTarget.rowIndex-1]
|
const entity = vue.entity = vue.entities [eve.currentTarget.rowIndex-1]
|
||||||
storageSet('entity', JSON.stringify(entity))
|
storageSet('entity', JSON.stringify(entity))
|
||||||
viewer.columns = viewer.entities.find(e => e.name === entity.name).columns
|
vue.columns = vue.entities.find(e => e.name === entity.name).columns
|
||||||
return await this.fetchData()
|
return await this.fetchData()
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetchData () {
|
async fetchData () {
|
||||||
let url = `/Data?entity=${viewer.entity.name}&$skip=${viewer.skip}&$top=${viewer.top}`
|
let url = `/Data?entity=${vue.entity.name}&$skip=${vue.skip}&$top=${vue.top}`
|
||||||
if (viewer.dataSource === 'db') url += `&dataSource=db`
|
if (vue.dataSource === 'db') url += `&dataSource=db`
|
||||||
|
|
||||||
|
try {
|
||||||
const {data} = await GET(url)
|
const {data} = await GET(url)
|
||||||
viewer.data = data.value.map(d => d.record.map(r => r.data))
|
// sort data along column order
|
||||||
viewer.rowDetails = undefined
|
const columnIndexes = {}
|
||||||
|
vue.columns.forEach((col, i) => columnIndexes[col.name] = i)
|
||||||
|
vue.data = data.value.map(d => d.record
|
||||||
|
.sort((r1, r2) => columnIndexes[r1.column] - columnIndexes[r2.column])
|
||||||
|
.map(r => r.data)
|
||||||
|
)
|
||||||
|
const row = vue.data.find(data => vue._makeRowKey(data) === vue.rowKey)
|
||||||
|
if (row) vue._setRowDetails(row)
|
||||||
|
else vue.rowDetails = {}
|
||||||
|
vue.error = undefined
|
||||||
|
} catch (err) {
|
||||||
|
vue.data = []
|
||||||
|
vue.rowDetails = {}
|
||||||
|
if (err.response?.data?.error) {
|
||||||
|
vue.error = err.response.data.error
|
||||||
|
} else {
|
||||||
|
vue.error = { code:err.code, message:err.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
inspectRow (eve) {
|
inspectRow (eve) {
|
||||||
viewer.rowDetails = {}
|
vue.rowDetails = {}
|
||||||
const selectedRow = eve.currentTarget.rowIndex-1
|
const selectedRow = eve.currentTarget.rowIndex-1
|
||||||
viewer.data[selectedRow].forEach((line, colIndex) => {
|
vue.rowKey = vue._makeRowKey(vue.data[selectedRow])
|
||||||
viewer.rowDetails[viewer.columns[colIndex].name] = line
|
storageSet('rowKey', vue.rowKey)
|
||||||
|
vue._setRowDetails(vue.data[selectedRow])
|
||||||
|
},
|
||||||
|
|
||||||
|
_setRowDetails(row) {
|
||||||
|
vue.rowDetails = {}
|
||||||
|
row.forEach((line, colIndex) => {
|
||||||
|
vue.rowDetails[vue.columns[colIndex].name] = line
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_makeRowKey(row) {
|
||||||
|
// to identify a row, build a key string out of all key columns' values
|
||||||
|
return row
|
||||||
|
.filter((_, colIndex) => vue.columns[colIndex] && vue.columns[colIndex].isKey)
|
||||||
|
.reduce(((prev, next) => prev += next), '')
|
||||||
|
},
|
||||||
|
|
||||||
|
isActiveRow(row) {
|
||||||
|
return vue._makeRowKey(row) === vue.rowKey
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.mount('#app')
|
||||||
|
|
||||||
viewer.fetchEntities()
|
vue.fetchEntities()
|
||||||
|
|||||||
@@ -5,28 +5,36 @@
|
|||||||
<title>Data Browser</title>
|
<title>Data Browser</title>
|
||||||
<link rel="stylesheet" href="https://unpkg.com/primitive-ui/dist/css/main.css">
|
<link rel="stylesheet" href="https://unpkg.com/primitive-ui/dist/css/main.css">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
|
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
|
||||||
|
<script src="app.js" defer></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
th { position: sticky; top:0; z-index: 2; background-color: white; }
|
||||||
|
.noscroll { overflow: hidden; }
|
||||||
.hovering tr:hover td { background: #ebeefc; cursor: pointer; }
|
.hovering tr:hover td { background: #ebeefc; cursor: pointer; }
|
||||||
.highlight { background: #ebeefc; }
|
.highlight { background: #ebeefc !important; }
|
||||||
.rating-stars { color:teal }
|
.rating-stars { color:teal }
|
||||||
.succeeded { color:teal }
|
.succeeded { color:teal }
|
||||||
.failed { color:red }
|
.failed { color:red }
|
||||||
.condensed { max-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.condensed { max-width: 100px; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.key { font-weight: bold }
|
.key { font-weight: bold }
|
||||||
.not-key { font-weight: lighter;}
|
.not-key { font-weight: lighter;}
|
||||||
.with-sidebar { display: flex; flex-wrap: wrap; gap: 1rem; }
|
.with-sidebar { display: flex; flex-wrap: wrap; gap: 1rem; }
|
||||||
.sidebar { flex-basis: 20rem; flex-grow: 1; }
|
.sidebar { flex-basis: 20rem; flex-grow: 1; }
|
||||||
.not-sidebar { flex-basis: 0; flex-grow: 999; min-inline-size: 50%;}
|
.sidebar-main { height: 100vh; overflow-y: scroll; }
|
||||||
|
.not-sidebar { flex-basis: 0; flex-grow: 999; min-inline-size: 50%; align-items: stretch;}
|
||||||
|
.not-sidebar-main { max-height: 40vh; overflow-y: scroll; }
|
||||||
|
.not-sidebar-sub { max-height: 40vh; overflow-y: scroll; }
|
||||||
.horizontal label { display: inline; }
|
.horizontal label { display: inline; }
|
||||||
.horizontal input { width: initial; display: inline; }
|
.horizontal input { width: initial; display: inline; }
|
||||||
|
.error { color: red; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body class="noscroll">
|
||||||
<div id='app' class="container">
|
<div id='app' class="full-container">
|
||||||
|
|
||||||
<h1> {{ document.title }}{{ entity ? ' – ' + entity.name : '' }}</h1>
|
<h1>Data Browser – {{ entity ? entity.name : '' }}</h1>
|
||||||
|
|
||||||
<div class="with-sidebar">
|
<div class="with-sidebar">
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
@@ -37,15 +45,17 @@
|
|||||||
<input type="radio" id="dataSource-srv" value="service" v-model="dataSource">
|
<input type="radio" id="dataSource-srv" value="service" v-model="dataSource">
|
||||||
<label for="dataSource-srv">Service</label>
|
<label for="dataSource-srv">Service</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="sidebar-main">
|
||||||
<table id='entities' class="hovering">
|
<table id='entities' class="hovering">
|
||||||
<thead>
|
<thead>
|
||||||
<th>Entity Name</th>
|
<th>Entity Name</th>
|
||||||
</thead>
|
</thead>
|
||||||
<tr v-for="e in entities" v-bind:id="e.name" v-on:click="inspectEntity" :class="{'highlight': (entity && e.name === entity.name)}">
|
<tr v-for="e in entities" :key="e.name" @click="inspectEntity" :class="{'highlight': (entity && e.name === entity.name)}">
|
||||||
<td>{{ e.name }}</td>
|
<td>{{ e.name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="not-sidebar">
|
<div class="not-sidebar">
|
||||||
<div class="horizontal">
|
<div class="horizontal">
|
||||||
@@ -54,18 +64,21 @@
|
|||||||
<label for="top">Top:</label>
|
<label for="top">Top:</label>
|
||||||
<input id="top" v-model.lazy="top" title="No. of entries to read" type="number" min="0">
|
<input id="top" v-model.lazy="top" title="No. of entries to read" type="number" min="0">
|
||||||
</div>
|
</div>
|
||||||
<div v-if="entity">
|
<div v-if="data" class="not-sidebar-main">
|
||||||
<table id='data' class="hovering striped-table condensed">
|
<table id='data' class="hovering striped-table condensed">
|
||||||
<thead>
|
<thead>
|
||||||
<th v-for="col in columns" v-bind:title="col.type" :class="[col.isKey ? 'key' : 'not-key']">{{ col.name }} </th>
|
<th v-for="col in columns" :title="col.type" :class="[col.isKey ? 'key' : 'not-key']">{{ col.name }} </th>
|
||||||
</thead>
|
</thead>
|
||||||
<tr v-for="(row, index) in data" v-on:click="inspectRow">
|
<tr v-for="row in data" @click="inspectRow" :class="{'highlight': isActiveRow(row)}">
|
||||||
<td v-for="d in row" v-bind:title="d">{{ d }}</td>
|
<td v-for="d in row" :title="d">{{ d }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="error" class="not-sidebar-main error">
|
||||||
<div v-if="rowDetails">
|
Error: {{ error.code ? error.code + ' – ' + error.message : error.message }}
|
||||||
|
</div>
|
||||||
|
<p></p>
|
||||||
|
<div v-if="rowDetails" class="not-sidebar-sub">
|
||||||
<table id='rowDetails'>
|
<table id='rowDetails'>
|
||||||
<tr v-for="(key, value) in rowDetails" >
|
<tr v-for="(key, value) in rowDetails" >
|
||||||
<td class="key">{{ value }}</td>
|
<td class="key">{{ value }}</td>
|
||||||
@@ -79,5 +92,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
<script src="app.js"></script>
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Exposes data + entity metadata
|
* Exposes data + entity metadata
|
||||||
*/
|
*/
|
||||||
//@requires:'admin'
|
@requires:'authenticated-user'
|
||||||
service DataService @( path:'-data' ) {
|
service DataService @( path:'-data' ) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class DataService extends cds.ApplicationService { init(){
|
|||||||
if (!entity) return req.reject(404, 'No such entity: ' + entityName)
|
if (!entity) return req.reject(404, 'No such entity: ' + entityName)
|
||||||
|
|
||||||
const query = SELECT.from(entity)
|
const query = SELECT.from(entity)
|
||||||
query.SELECT.limit = req.query.SELECT.limit // use $skip / $top from request
|
query.SELECT.limit = req.query.SELECT.limit // forward $skip / $top
|
||||||
|
|
||||||
const dataSource = findDataSource(dataSourceName, entityName)
|
const dataSource = findDataSource(dataSourceName, entityName)
|
||||||
const res = await dataSource.run(query)
|
const res = await dataSource.run(query)
|
||||||
|
|||||||
@@ -64,8 +64,9 @@ annotate AdminService.Books.texts with @(
|
|||||||
|
|
||||||
// Add Value Help for Locales
|
// Add Value Help for Locales
|
||||||
annotate AdminService.Books.texts {
|
annotate AdminService.Books.texts {
|
||||||
locale @ValueList:{entity:'Languages'};
|
locale @(
|
||||||
locale @Common.ValueListWithFixedValues:true; //show as drop down, not a dialog
|
ValueList.entity:'Languages', Common.ValueListWithFixedValues, //show as drop down, not a dialog
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// In addition we need to expose Languages through AdminService as a target for ValueList
|
// In addition we need to expose Languages through AdminService as a target for ValueList
|
||||||
using { sap } from '@sap/cds/common';
|
using { sap } from '@sap/cds/common';
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
"start": "cds serve srv/world.cds",
|
"start": "cds serve srv/world.cds",
|
||||||
"start:ts": "cds-ts serve srv/world.cds"
|
"start:ts": "cds-ts serve srv/world.cds"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sap/cds": "^5.0.4"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^27.0.2",
|
"@types/jest": "^27.0.2",
|
||||||
"@types/node": "^16.11.6",
|
"@types/node": "^16.11.6",
|
||||||
|
|||||||
3671
package-lock.json
generated
3671
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -21,10 +21,7 @@
|
|||||||
"chai": "^4.3.4",
|
"chai": "^4.3.4",
|
||||||
"chai-as-promised": "^7.1.1",
|
"chai-as-promised": "^7.1.1",
|
||||||
"chai-subset": "^1.6.0",
|
"chai-subset": "^1.6.0",
|
||||||
"sqlite3": "^5"
|
"sqlite3": "npm:@mendix/sqlite3@^5"
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"tar": "^4.4.18"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"cleanup": "rm -rf node_modules && rm -rf */node_modules && rm -rf */*/node_modules",
|
"cleanup": "rm -rf node_modules && rm -rf */node_modules && rm -rf */*/node_modules",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
subject;rating;reviewer;title;text
|
subject;rating;reviewer;title;text
|
||||||
201;5;adam;Intriguing;Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
|
201;5;bob;Intriguing;Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
|
||||||
201;4;bob;Fascinating;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Id diam maecenas ultricies mi eget mauris pharetra et. Risus at ultrices mi tempus imperdiet nulla malesuada pellentesque. Pulvinar mattis nunc sed blandit libero. Facilisis magna etiam tempor orci eu. Nec sagittis aliquam malesuada bibendum arcu. Eu consequat ac felis donec. Ultricies tristique nulla aliquet enim tortor at auctor urna nunc. Tortor posuere ac ut consequat semper viverra nam libero. Amet nisl suscipit adipiscing bibendum est ultricies integer quis auctor. Scelerisque purus semper eget duis at tellus. Elementum tempus egestas sed sed risus pretium. Arcu dictum varius duis at. Amet luctus venenatis lectus magna fringilla urna. Eget velit aliquet sagittis id consectetur purus ut faucibus. Vitae auctor eu augue ut lectus. Fermentum iaculis eu non diam phasellus vestibulum.
|
201;4;bob;Fascinating;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Id diam maecenas ultricies mi eget mauris pharetra et. Risus at ultrices mi tempus imperdiet nulla malesuada pellentesque. Pulvinar mattis nunc sed blandit libero. Facilisis magna etiam tempor orci eu. Nec sagittis aliquam malesuada bibendum arcu. Eu consequat ac felis donec. Ultricies tristique nulla aliquet enim tortor at auctor urna nunc. Tortor posuere ac ut consequat semper viverra nam libero. Amet nisl suscipit adipiscing bibendum est ultricies integer quis auctor. Scelerisque purus semper eget duis at tellus. Elementum tempus egestas sed sed risus pretium. Arcu dictum varius duis at. Amet luctus venenatis lectus magna fringilla urna. Eget velit aliquet sagittis id consectetur purus ut faucibus. Vitae auctor eu augue ut lectus. Fermentum iaculis eu non diam phasellus vestibulum.
|
||||||
207;2;bob;What is this?;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Libero justo laoreet sit amet cursus sit amet dictum. Nunc faucibus a pellentesque sit. Dis parturient montes nascetur ridiculus mus mauris vitae ultricies. Enim nunc faucibus a pellentesque. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien. Cras ornare arcu dui vivamus. Facilisi etiam dignissim diam quis enim lobortis. Et molestie ac feugiat sed. Urna neque viverra justo nec ultrices dui. Ullamcorper a lacus vestibulum sed arcu non. Volutpat ac tincidunt vitae semper quis. Dignissim sodales ut eu sem. Feugiat in fermentum posuere urna nec. At augue eget arcu dictum varius.
|
207;2;bob;What is this?;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Libero justo laoreet sit amet cursus sit amet dictum. Nunc faucibus a pellentesque sit. Dis parturient montes nascetur ridiculus mus mauris vitae ultricies. Enim nunc faucibus a pellentesque. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien. Cras ornare arcu dui vivamus. Facilisi etiam dignissim diam quis enim lobortis. Et molestie ac feugiat sed. Urna neque viverra justo nec ultrices dui. Ullamcorper a lacus vestibulum sed arcu non. Volutpat ac tincidunt vitae semper quis. Dignissim sodales ut eu sem. Feugiat in fermentum posuere urna nec. At augue eget arcu dictum varius.
|
||||||
251;3;bob;It's dark...;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Suscipit tellus mauris a diam. Velit aliquet sagittis id consectetur purus ut. Viverra adipiscing at in tellus integer. Vitae elementum curabitur vitae nunc. Mattis ullamcorper velit sed ullamcorper morbi. Diam quis enim lobortis scelerisque. Auctor neque vitae tempus quam pellentesque nec nam aliquam. Semper auctor neque vitae tempus. Quis eleifend quam adipiscing vitae proin. Neque convallis a cras semper auctor neque vitae. Imperdiet massa tincidunt nunc pulvinar sapien et ligula. Sit amet consectetur adipiscing elit ut aliquam purus. Pretium quam vulputate dignissim suspendisse.
|
251;3;bob;It's dark...;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Suscipit tellus mauris a diam. Velit aliquet sagittis id consectetur purus ut. Viverra adipiscing at in tellus integer. Vitae elementum curabitur vitae nunc. Mattis ullamcorper velit sed ullamcorper morbi. Diam quis enim lobortis scelerisque. Auctor neque vitae tempus quam pellentesque nec nam aliquam. Semper auctor neque vitae tempus. Quis eleifend quam adipiscing vitae proin. Neque convallis a cras semper auctor neque vitae. Imperdiet massa tincidunt nunc pulvinar sapien et ligula. Sit amet consectetur adipiscing elit ut aliquam purus. Pretium quam vulputate dignissim suspendisse.
|
||||||
|
@@ -32,7 +32,6 @@ entity Likes {
|
|||||||
|
|
||||||
// Auto-fill reviewers and review dates
|
// Auto-fill reviewers and review dates
|
||||||
annotate Reviews with {
|
annotate Reviews with {
|
||||||
reviewer @cds.on.insert:$user;
|
reviewer @cds.on:{insert:$user};
|
||||||
date @cds.on.insert:$now;
|
date @cds.on:{insert:$now,update:$now};
|
||||||
date @cds.on.update:$now;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user