2021-11-04 15:54:35 +01:00
|
|
|
import {
|
|
|
|
Integration,
|
|
|
|
DatasourceFieldTypes,
|
|
|
|
QueryTypes,
|
|
|
|
SqlQuery,
|
2021-11-18 14:35:22 +01:00
|
|
|
QueryJson,
|
2021-11-23 14:27:38 +01:00
|
|
|
Operation,
|
2021-11-04 15:54:35 +01:00
|
|
|
} from "../definitions/datasource"
|
2021-11-18 14:35:22 +01:00
|
|
|
import {
|
|
|
|
finaliseExternalTables,
|
|
|
|
getSqlQuery,
|
|
|
|
buildExternalTableId,
|
2021-11-22 12:38:17 +01:00
|
|
|
convertSqlType,
|
2021-11-23 14:27:38 +01:00
|
|
|
SqlClients,
|
2021-11-18 14:35:22 +01:00
|
|
|
} from "./utils"
|
|
|
|
import oracledb, {
|
|
|
|
ExecuteOptions,
|
|
|
|
Result,
|
|
|
|
Connection,
|
|
|
|
ConnectionAttributes,
|
|
|
|
BindParameters,
|
|
|
|
} from "oracledb"
|
2021-11-05 14:56:54 +01:00
|
|
|
import Sql from "./base/sql"
|
2021-11-17 15:34:16 +01:00
|
|
|
import { Table } from "../definitions/common"
|
|
|
|
import { DatasourcePlus } from "./base/datasourcePlus"
|
|
|
|
import { FieldTypes } from "../constants"
|
2021-11-09 18:52:26 +01:00
|
|
|
|
2021-11-04 15:54:35 +01:00
|
|
|
module OracleModule {
|
2021-11-18 14:35:22 +01:00
|
|
|
oracledb.outFormat = oracledb.OUT_FORMAT_OBJECT
|
2021-11-09 18:52:26 +01:00
|
|
|
|
2021-11-04 15:54:35 +01:00
|
|
|
interface OracleConfig {
|
2021-11-05 14:56:54 +01:00
|
|
|
host: string
|
|
|
|
port: number
|
|
|
|
database: string
|
|
|
|
user: string
|
|
|
|
password: string
|
2021-11-04 15:54:35 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const SCHEMA: Integration = {
|
2021-11-08 23:08:47 +01:00
|
|
|
docs: "https://github.com/oracle/node-oracledb",
|
2021-11-17 15:34:16 +01:00
|
|
|
plus: true,
|
2021-11-04 15:54:35 +01:00
|
|
|
friendlyName: "Oracle",
|
2021-11-18 14:35:22 +01:00
|
|
|
description:
|
|
|
|
"Oracle Database is an object-relational database management system developed by Oracle Corporation",
|
2021-11-04 15:54:35 +01:00
|
|
|
datasource: {
|
2021-11-05 14:56:54 +01:00
|
|
|
host: {
|
|
|
|
type: DatasourceFieldTypes.STRING,
|
|
|
|
default: "localhost",
|
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
port: {
|
|
|
|
type: DatasourceFieldTypes.NUMBER,
|
|
|
|
required: true,
|
|
|
|
default: 1521,
|
|
|
|
},
|
|
|
|
database: {
|
|
|
|
type: DatasourceFieldTypes.STRING,
|
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
user: {
|
|
|
|
type: DatasourceFieldTypes.STRING,
|
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
password: {
|
|
|
|
type: DatasourceFieldTypes.PASSWORD,
|
|
|
|
required: true,
|
2021-11-18 14:35:22 +01:00
|
|
|
},
|
2021-11-04 15:54:35 +01:00
|
|
|
},
|
|
|
|
query: {
|
2021-11-05 14:56:54 +01:00
|
|
|
create: {
|
|
|
|
type: QueryTypes.SQL,
|
|
|
|
},
|
|
|
|
read: {
|
|
|
|
type: QueryTypes.SQL,
|
|
|
|
},
|
|
|
|
update: {
|
|
|
|
type: QueryTypes.SQL,
|
|
|
|
},
|
|
|
|
delete: {
|
|
|
|
type: QueryTypes.SQL,
|
|
|
|
},
|
2021-11-04 15:54:35 +01:00
|
|
|
},
|
|
|
|
}
|
2021-11-17 15:34:16 +01:00
|
|
|
|
2021-11-18 14:35:22 +01:00
|
|
|
const UNSUPPORTED_TYPES = ["BLOB", "CLOB", "NCLOB"]
|
2021-11-17 17:41:00 +01:00
|
|
|
|
2021-11-17 15:34:16 +01:00
|
|
|
/**
|
|
|
|
* Raw query response
|
|
|
|
*/
|
|
|
|
interface ColumnsResponse {
|
|
|
|
TABLE_NAME: string
|
|
|
|
COLUMN_NAME: string
|
|
|
|
DATA_TYPE: string
|
2021-11-18 11:49:53 +01:00
|
|
|
DATA_DEFAULT: string | null
|
2021-11-17 15:34:16 +01:00
|
|
|
COLUMN_ID: number
|
|
|
|
CONSTRAINT_NAME: string | null
|
|
|
|
CONSTRAINT_TYPE: string | null
|
|
|
|
R_CONSTRAINT_NAME: string | null
|
|
|
|
SEARCH_CONDITION: string | null
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* An oracle constraint
|
|
|
|
*/
|
|
|
|
interface OracleConstraint {
|
|
|
|
name: string
|
2021-11-18 14:35:22 +01:00
|
|
|
type: string
|
2021-11-17 15:34:16 +01:00
|
|
|
relatedConstraintName: string | null
|
|
|
|
searchCondition: string | null
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* An oracle column and it's related constraints
|
|
|
|
*/
|
|
|
|
interface OracleColumn {
|
|
|
|
name: string
|
|
|
|
type: string
|
2021-11-18 11:49:53 +01:00
|
|
|
default: string | null
|
2021-11-17 15:34:16 +01:00
|
|
|
id: number
|
2021-11-18 14:35:22 +01:00
|
|
|
constraints: { [key: string]: OracleConstraint }
|
2021-11-17 15:34:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* An oracle table and it's related columns
|
|
|
|
*/
|
|
|
|
interface OracleTable {
|
|
|
|
name: string
|
2021-11-18 14:35:22 +01:00
|
|
|
columns: { [key: string]: OracleColumn }
|
2021-11-17 15:34:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const OracleContraintTypes = {
|
|
|
|
PRIMARY: "P",
|
|
|
|
NOT_NULL_OR_CHECK: "C",
|
|
|
|
FOREIGN_KEY: "R",
|
2021-11-18 14:35:22 +01:00
|
|
|
UNIQUE: "U",
|
2021-11-17 15:34:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
class OracleIntegration extends Sql implements DatasourcePlus {
|
2021-11-04 15:54:35 +01:00
|
|
|
private readonly config: OracleConfig
|
|
|
|
|
2021-11-17 15:34:16 +01:00
|
|
|
public tables: Record<string, Table> = {}
|
|
|
|
public schemaErrors: Record<string, string> = {}
|
|
|
|
|
|
|
|
private readonly COLUMNS_SQL = `
|
|
|
|
SELECT
|
|
|
|
tabs.table_name,
|
|
|
|
cols.column_name,
|
|
|
|
cols.data_type,
|
2021-11-18 11:49:53 +01:00
|
|
|
cols.data_default,
|
2021-11-17 15:34:16 +01:00
|
|
|
cols.column_id,
|
|
|
|
cons.constraint_name,
|
|
|
|
cons.constraint_type,
|
|
|
|
cons.r_constraint_name,
|
|
|
|
cons.search_condition
|
|
|
|
FROM
|
|
|
|
user_tables tabs
|
|
|
|
JOIN
|
|
|
|
user_tab_columns cols
|
|
|
|
ON tabs.table_name = cols.table_name
|
|
|
|
LEFT JOIN
|
|
|
|
user_cons_columns col_cons
|
|
|
|
ON cols.column_name = col_cons.column_name
|
|
|
|
AND cols.table_name = col_cons.table_name
|
|
|
|
LEFT JOIN
|
|
|
|
user_constraints cons
|
|
|
|
ON col_cons.constraint_name = cons.constraint_name
|
|
|
|
AND cons.table_name = cols.table_name
|
|
|
|
WHERE
|
|
|
|
(cons.status = 'ENABLED'
|
|
|
|
OR cons.status IS NULL)
|
|
|
|
`
|
2021-11-04 15:54:35 +01:00
|
|
|
constructor(config: OracleConfig) {
|
2021-11-23 14:27:38 +01:00
|
|
|
super(SqlClients.ORACLE)
|
2021-11-05 14:56:54 +01:00
|
|
|
this.config = config
|
|
|
|
}
|
|
|
|
|
2021-11-17 15:34:16 +01:00
|
|
|
/**
|
2021-11-18 14:35:22 +01:00
|
|
|
* Map the flat tabular columns and constraints data into a nested object
|
2021-11-17 15:34:16 +01:00
|
|
|
*/
|
2021-11-18 14:35:22 +01:00
|
|
|
private mapColumns(result: Result<ColumnsResponse>): {
|
|
|
|
[key: string]: OracleTable
|
|
|
|
} {
|
2021-11-17 15:34:16 +01:00
|
|
|
const oracleTables: { [key: string]: OracleTable } = {}
|
|
|
|
|
|
|
|
if (result.rows) {
|
|
|
|
result.rows.forEach(row => {
|
|
|
|
const tableName = row.TABLE_NAME
|
|
|
|
const columnName = row.COLUMN_NAME
|
|
|
|
const dataType = row.DATA_TYPE
|
2021-11-18 11:49:53 +01:00
|
|
|
const dataDefault = row.DATA_DEFAULT
|
2021-11-17 15:34:16 +01:00
|
|
|
const columnId = row.COLUMN_ID
|
|
|
|
const constraintName = row.CONSTRAINT_NAME
|
|
|
|
const constraintType = row.CONSTRAINT_TYPE
|
|
|
|
const relatedConstraintName = row.R_CONSTRAINT_NAME
|
|
|
|
const searchCondition = row.SEARCH_CONDITION
|
2021-11-08 23:08:47 +01:00
|
|
|
|
2021-11-17 15:34:16 +01:00
|
|
|
let table = oracleTables[tableName]
|
|
|
|
if (!table) {
|
|
|
|
table = {
|
|
|
|
name: tableName,
|
2021-11-18 14:35:22 +01:00
|
|
|
columns: {},
|
2021-11-17 15:34:16 +01:00
|
|
|
}
|
|
|
|
oracleTables[tableName] = table
|
|
|
|
}
|
|
|
|
|
|
|
|
let column = table.columns[columnName]
|
|
|
|
if (!column) {
|
|
|
|
column = {
|
|
|
|
name: columnName,
|
|
|
|
type: dataType,
|
2021-11-18 14:35:22 +01:00
|
|
|
default: dataDefault,
|
2021-11-17 15:34:16 +01:00
|
|
|
id: columnId,
|
2021-11-18 14:35:22 +01:00
|
|
|
constraints: {},
|
2021-11-17 15:34:16 +01:00
|
|
|
}
|
|
|
|
table.columns[columnName] = column
|
|
|
|
}
|
|
|
|
|
|
|
|
if (constraintName && constraintType) {
|
|
|
|
let constraint = column.constraints[constraintName]
|
|
|
|
if (!constraint) {
|
|
|
|
constraint = {
|
|
|
|
name: constraintName,
|
|
|
|
type: constraintType,
|
|
|
|
relatedConstraintName: relatedConstraintName,
|
2021-11-18 14:35:22 +01:00
|
|
|
searchCondition: searchCondition,
|
2021-11-17 15:34:16 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
column.constraints[constraintName] = constraint
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return oracleTables
|
|
|
|
}
|
|
|
|
|
2021-11-17 17:41:00 +01:00
|
|
|
private isSupportedColumn(column: OracleColumn) {
|
|
|
|
if (UNSUPPORTED_TYPES.includes(column.type)) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2021-11-18 11:49:53 +01:00
|
|
|
private isAutoColumn(column: OracleColumn) {
|
|
|
|
if (column.default && column.default.toLowerCase().includes("nextval")) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2021-11-18 14:35:22 +01:00
|
|
|
/**
|
|
|
|
* No native boolean in oracle. Best we can do is to check if a manual 1 or 0 number constraint has been set up
|
|
|
|
* This matches the default behaviour for generating DDL used in knex.
|
|
|
|
*/
|
|
|
|
private isBooleanType(column: OracleColumn): boolean {
|
|
|
|
if (
|
|
|
|
column.type.toLowerCase() === "number" &&
|
|
|
|
Object.values(column.constraints).filter(c => {
|
|
|
|
if (
|
|
|
|
c.type === OracleContraintTypes.NOT_NULL_OR_CHECK &&
|
|
|
|
c.searchCondition
|
|
|
|
) {
|
|
|
|
const condition = c.searchCondition
|
|
|
|
.replace(/\s/g, "") // remove spaces
|
|
|
|
.replace(/[']+/g, "") // remove quotes
|
|
|
|
if (
|
|
|
|
condition.includes("in(0,1)") ||
|
|
|
|
condition.includes("in(1,0)")
|
|
|
|
) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}).length > 0
|
|
|
|
) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
private internalConvertType(column: OracleColumn): string {
|
|
|
|
if (this.isBooleanType(column)) {
|
|
|
|
return FieldTypes.BOOLEAN
|
|
|
|
}
|
|
|
|
|
2021-11-22 12:38:17 +01:00
|
|
|
return convertSqlType(column.type)
|
2021-11-18 14:35:22 +01:00
|
|
|
}
|
|
|
|
|
2021-11-17 15:34:16 +01:00
|
|
|
/**
|
|
|
|
* Fetches the tables from the oracle table and assigns them to the datasource.
|
|
|
|
* @param {*} datasourceId - datasourceId to fetch
|
|
|
|
* @param entities - the tables that are to be built
|
2021-11-18 14:35:22 +01:00
|
|
|
*/
|
2021-11-17 15:34:16 +01:00
|
|
|
async buildSchema(datasourceId: string, entities: Record<string, Table>) {
|
2021-11-18 14:35:22 +01:00
|
|
|
const columnsResponse = await this.internalQuery<ColumnsResponse>({
|
|
|
|
sql: this.COLUMNS_SQL,
|
|
|
|
})
|
2021-11-17 15:34:16 +01:00
|
|
|
const oracleTables = this.mapColumns(columnsResponse)
|
|
|
|
|
|
|
|
const tables: { [key: string]: Table } = {}
|
|
|
|
|
|
|
|
// iterate each table
|
|
|
|
Object.values(oracleTables).forEach(oracleTable => {
|
|
|
|
let table = tables[oracleTable.name]
|
|
|
|
if (!table) {
|
|
|
|
table = {
|
|
|
|
_id: buildExternalTableId(datasourceId, oracleTable.name),
|
|
|
|
primary: [],
|
|
|
|
name: oracleTable.name,
|
|
|
|
schema: {},
|
|
|
|
}
|
|
|
|
tables[oracleTable.name] = table
|
|
|
|
}
|
|
|
|
|
|
|
|
// iterate each column on the table
|
|
|
|
Object.values(oracleTable.columns)
|
2021-11-18 14:35:22 +01:00
|
|
|
// remove columns that we can't read / save
|
|
|
|
.filter(oracleColumn => this.isSupportedColumn(oracleColumn))
|
|
|
|
// match the order of the columns in the db
|
|
|
|
.sort((c1, c2) => c1.id - c2.id)
|
|
|
|
.forEach(oracleColumn => {
|
|
|
|
const columnName = oracleColumn.name
|
|
|
|
let fieldSchema = table.schema[columnName]
|
|
|
|
if (!fieldSchema) {
|
|
|
|
fieldSchema = {
|
|
|
|
autocolumn: this.isAutoColumn(oracleColumn),
|
|
|
|
name: columnName,
|
|
|
|
type: this.internalConvertType(oracleColumn),
|
|
|
|
}
|
|
|
|
table.schema[columnName] = fieldSchema
|
2021-11-17 15:34:16 +01:00
|
|
|
}
|
|
|
|
|
2021-11-18 14:35:22 +01:00
|
|
|
// iterate each constraint on the column
|
|
|
|
Object.values(oracleColumn.constraints).forEach(
|
|
|
|
oracleConstraint => {
|
|
|
|
if (oracleConstraint.type === OracleContraintTypes.PRIMARY) {
|
|
|
|
table.primary!.push(columnName)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
2021-11-17 15:34:16 +01:00
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
const final = finaliseExternalTables(tables, entities)
|
|
|
|
this.tables = final.tables
|
|
|
|
this.schemaErrors = final.errors
|
|
|
|
}
|
|
|
|
|
2021-11-23 14:27:38 +01:00
|
|
|
/**
|
|
|
|
* Knex default returning behaviour does not work with oracle
|
|
|
|
* Manually add the behaviour for the return column
|
|
|
|
*/
|
2021-11-25 18:14:07 +01:00
|
|
|
private addReturning(
|
|
|
|
query: SqlQuery,
|
|
|
|
bindings: BindParameters,
|
|
|
|
returnColumn: string
|
|
|
|
) {
|
2021-11-23 14:27:38 +01:00
|
|
|
if (bindings instanceof Array) {
|
|
|
|
bindings.push({ dir: oracledb.BIND_OUT })
|
2021-11-25 18:14:07 +01:00
|
|
|
query.sql =
|
|
|
|
query.sql + ` returning \"${returnColumn}\" into :${bindings.length}`
|
2021-11-23 14:27:38 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-25 18:14:07 +01:00
|
|
|
private async internalQuery<T>(
|
|
|
|
query: SqlQuery,
|
|
|
|
returnColum?: string,
|
|
|
|
operation?: string
|
|
|
|
): Promise<Result<T>> {
|
2021-11-18 14:35:22 +01:00
|
|
|
let connection
|
|
|
|
try {
|
|
|
|
connection = await this.getConnection()
|
2021-11-17 15:34:16 +01:00
|
|
|
|
|
|
|
const options: ExecuteOptions = { autoCommit: true }
|
|
|
|
const bindings: BindParameters = query.bindings || []
|
2021-11-23 14:27:38 +01:00
|
|
|
|
2021-11-25 18:14:07 +01:00
|
|
|
if (
|
|
|
|
returnColum &&
|
|
|
|
(operation === Operation.CREATE || operation === Operation.UPDATE)
|
|
|
|
) {
|
2021-11-23 14:27:38 +01:00
|
|
|
this.addReturning(query, bindings, returnColum)
|
|
|
|
}
|
|
|
|
|
2021-11-18 14:35:22 +01:00
|
|
|
const result: Result<T> = await connection.execute<T>(
|
|
|
|
query.sql,
|
|
|
|
bindings,
|
|
|
|
options
|
|
|
|
)
|
2021-11-08 23:08:47 +01:00
|
|
|
|
|
|
|
return result
|
|
|
|
} finally {
|
2021-11-18 14:35:22 +01:00
|
|
|
if (connection) {
|
|
|
|
try {
|
|
|
|
await connection.close()
|
|
|
|
} catch (err) {
|
|
|
|
console.error(err)
|
|
|
|
}
|
|
|
|
}
|
2021-11-08 23:08:47 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private getConnection = async (): Promise<Connection> => {
|
2021-11-05 14:56:54 +01:00
|
|
|
//connectString : "(DESCRIPTION =(ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1521))(CONNECT_DATA =(SID= ORCL)))"
|
2021-11-18 14:35:22 +01:00
|
|
|
const connectString = `${this.config.host}:${this.config.port || 1521}/${
|
|
|
|
this.config.database
|
|
|
|
}`
|
2021-11-08 23:08:47 +01:00
|
|
|
const attributes: ConnectionAttributes = {
|
2021-11-05 14:56:54 +01:00
|
|
|
user: this.config.user,
|
|
|
|
password: this.config.user,
|
2021-11-08 23:08:47 +01:00
|
|
|
connectString,
|
2021-11-05 14:56:54 +01:00
|
|
|
}
|
2021-11-18 14:35:22 +01:00
|
|
|
return oracledb.getConnection(attributes)
|
2021-11-04 15:54:35 +01:00
|
|
|
}
|
|
|
|
|
2021-11-22 12:38:17 +01:00
|
|
|
async create(query: SqlQuery | string): Promise<any[]> {
|
|
|
|
const response = await this.internalQuery<any>(getSqlQuery(query))
|
2021-11-18 14:35:22 +01:00
|
|
|
return response.rows && response.rows.length
|
|
|
|
? response.rows
|
|
|
|
: [{ created: true }]
|
2021-11-04 15:54:35 +01:00
|
|
|
}
|
|
|
|
|
2021-11-22 12:38:17 +01:00
|
|
|
async read(query: SqlQuery | string): Promise<any[]> {
|
|
|
|
const response = await this.internalQuery<any>(getSqlQuery(query))
|
|
|
|
return response.rows ? response.rows : []
|
2021-11-08 23:08:47 +01:00
|
|
|
}
|
2021-11-04 15:54:35 +01:00
|
|
|
|
2021-11-25 18:14:07 +01:00
|
|
|
async update(query: SqlQuery | string): Promise<any[]> {
|
2021-11-17 15:34:16 +01:00
|
|
|
const response = await this.internalQuery(getSqlQuery(query))
|
2021-11-18 14:35:22 +01:00
|
|
|
return response.rows && response.rows.length
|
|
|
|
? response.rows
|
|
|
|
: [{ updated: true }]
|
2021-11-08 23:08:47 +01:00
|
|
|
}
|
2021-11-04 15:54:35 +01:00
|
|
|
|
2021-11-25 18:14:07 +01:00
|
|
|
async delete(query: SqlQuery | string): Promise<any[]> {
|
2021-11-17 15:34:16 +01:00
|
|
|
const response = await this.internalQuery(getSqlQuery(query))
|
2021-11-18 14:35:22 +01:00
|
|
|
return response.rows && response.rows.length
|
|
|
|
? response.rows
|
|
|
|
: [{ deleted: true }]
|
2021-11-04 15:54:35 +01:00
|
|
|
}
|
2021-11-17 15:34:16 +01:00
|
|
|
|
|
|
|
async query(json: QueryJson) {
|
2021-11-23 14:27:38 +01:00
|
|
|
const primaryKeys = json.meta!.table!.primary
|
|
|
|
const primaryKey = primaryKeys ? primaryKeys[0] : undefined
|
2021-11-25 18:14:07 +01:00
|
|
|
const queryFn = (query: any, operation: string) =>
|
|
|
|
this.internalQuery(query, primaryKey, operation)
|
|
|
|
const processFn = (response: any) => (response.rows ? response.rows : [])
|
2021-11-23 14:27:38 +01:00
|
|
|
const output = await this.queryWithReturning(json, queryFn, processFn)
|
|
|
|
return output
|
2021-11-17 15:34:16 +01:00
|
|
|
}
|
2021-11-04 15:54:35 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
schema: SCHEMA,
|
|
|
|
integration: OracleIntegration,
|
|
|
|
}
|
|
|
|
}
|