how can i access nested properties for CASL conditions - node.js

I can't seem to access the nested object with the condition rule.
i want a user to have access to delete an article if the article's comment has the same id as the user.
these are just some made up classes to test...
here is my code:
import { defineAbility, AbilityBuilder } from '#casl/ability';
import { Ability, AbilityClass, ExtractSubjectType, InferSubjects } from '#casl/ability';
export class Article {
static readonly modelName = "Article";
static readonly __typename = "Article";
constructor( id: number,
title: string,
content: string,
user: User,
comment: Comment) {
this.id = id
this.title = title
this.content = content
this.user = user
this.comment = comment
}
id: number
title: string
content: string
user: User
comment: Comment
}
export class User {
static readonly modelName = "User"
static readonly __typename = "User";
constructor (id: number,
name: string,
comment: Comment) {
this.id = id
this.name = name
this.comment = comment
}
id: number
name: string
comment: Comment
}
export class Comment {
static readonly modelName = "Comment"
static readonly __typename = "Comment";
constructor(id: number,
content: string,
authorId: number) {
this.id = id
this.content = content
this.authorId = authorId
}
id: number
content: string
authorId: number
}
type Action = 'create' | 'read' | 'update' | 'delete';
type Subjects = InferSubjects<typeof Article | typeof Comment| typeof User, true>;
export type AppAbility = Ability<[Action, Subjects]>;
export function createForUser(user: User) {
const { can, cannot, build } = new AbilityBuilder<
Ability<[Action, Subjects]>
>(Ability as AbilityClass<AppAbility>);
can('delete', Article, { comment: {id: user.comment.id}})
return build({
detectSubjectType: item => item.constructor as ExtractSubjectType<Subjects>
});
}
and im testing it with:
const comment = new Comment(0, 'a', 0)
const user = new User(1, 'sd', comment);
const article = new Article(2, 'sd', 'asd', user, comment)
const ability = createForUser(user);
console.log(ability.can('delete', article))// false
i saw somewhere that i need to do somthing like this:
can('delete', Article, { 'comment.id': user.comment.id})
but when i do is says 'Object literal may only specify known properties, and ''comment.id'' does not exist in type 'string[]'

You can find “Nested fields with dot notation” section on https://casl.js.org/v5/en/advanced/typescript useful. In short, when you use for notation to define conditions together with typescript, you need to create a custom type:
type FlatArticle = Article & {
'comment.id': Article['comment']['id']
};
can<FlatArticle>('read', Article, { 'comment.id': 1 });

Ended up solving it like this:
export enum Action {
Manage = 'manage',
Create = 'create',
Read = 'read',
Update = 'update',
Delete = 'delete',
}
export type Subject = InferSubjects<typeof Article |
typeof Comment |
typeof User |
'all';
export type AppAbility = PureAbility<[Action, Subject]>;
export const AppAbility = Ability as AbilityClass<AppAbility>;
and then defining rules like this:
createForUser(user: User) {
const { can, cannot, build } = new AbilityBuilder(AppAbility);
can(Action.Read, Article, { 'comment.id': 1 });
return build({
detectSubjectType: item => item.constructor as ExtractSubjectType<Subject>
});
}

Related

What's the right way to actually get this search functionality to work?

I have this app which displays a list of "coins" to the users . This list was parsed from an JSON API and I used Jetpack Compose for the UI. I implemented
Here is the code of the Jetpack composable list of "coins"
#Composable
fun CoinListScreen(
navController: NavController,
viewModel: CoinListViewModel = hiltViewModel(),
) {
val state = viewModel.state.value
Surface {
Box(modifier = Modifier.fillMaxSize()) {
Column {
androidx.compose.foundation.Image(painter = painterResource(id = R.drawable.ic_baseline_currency_bitcoin_24),
contentDescription = "BTC",
modifier = Modifier
.fillMaxWidth()
.align(CenterHorizontally)
.size(50.dp, 50.dp)
)
SearchBar(
hint = "Search..",
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
){
viewModel.searchCoinsList(it) **//here I'm calling my search function from the view model, inside my search bar**
}
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(state.coins) { coin ->
Spacer(modifier = Modifier.height(5.dp))
CoinListItem(
coin = coin,
onItemClick = {
navController.navigate(Screen.CoinDetailScreen.route + "/${coin.id}")
}
)
Divider()
}
}
}
if (state.error.isNotBlank()) {
Text(
text = state.error,
color = MaterialTheme.colors.error,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
.align(Alignment.Center)
)
}
if (state.isLoading) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
}
}
}
**//and this Is my composable search bar**
#Composable
fun SearchBar(
modifier: Modifier = Modifier,
hint: String = "",
onSearch: (String) -> Unit = {}
) {
var text by remember {
mutableStateOf("")
}
var isHint by remember {
mutableStateOf(hint != "")
}
Box(modifier = modifier){
BasicTextField(
value = text,
onValueChange = {
text = it
onSearch(it)
},
maxLines = 1,
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.shadow(5.dp, CircleShape)
.background(Color.White, CircleShape)
.padding(horizontal = 20.dp, vertical = 12.dp)
.onFocusChanged {
isHint = it.isFocused != true
}
)
if(isHint){
Text(
text = hint,
color = Color.LightGray,
modifier = Modifier.padding(horizontal = 20.dp, vertical = 12.dp)
)
}
}
}
and this is my view model, this is where I'm implementing the search function, this is where I'm lost, variables that I'm searching for are name, rank, and symbol from the Coin domain list
#HiltViewModel //injecting the use case
class CoinListViewModel #Inject constructor (
private val getCoinsUseCase: GetCoinsUseCase,
) : ViewModel() {
//vmstate Live Template, only the view model touches it
private val _state =
mutableStateOf(CoinListState())
val state: State<CoinListState> = _state
**//for search purposes , this is where I'm lost**
private var coinsList = mutableStateOf<List<Coin>>(listOf())
private var cachedCoinsList = listOf<Coin>()
private var isSearchStarting = true
private var isSearching = mutableStateOf(false)
init {
getCoins()
}
**//for search purposes , this is where I'm lost**
fun searchCoinsList(query: String){
val listToSearch = if(isSearchStarting){
coinsList.value
} else {
cachedCoinsList
}
viewModelScope.launch(Dispatchers.Default) {
if(query.isEmpty()){
coinsList.value = cachedCoinsList
isSearching.value = false
isSearchStarting = true
return#launch
}
val results = listToSearch.filter {
//val iterate: Int = coins.size
it.name.contains(query.trim(), ignoreCase = true) ||
(it.rank.toString() == query.trim()) ||
it.symbol.contains(query.trim(), ignoreCase = true)
}
if(isSearchStarting){
cachedCoinsList = coinsList.value
isSearchStarting = false
}
coinsList.value = results
isSearching.value = true
}
}
//function that calls our GetCoinsUseCase and puts the data inside the state object
//to display that in the UI
private fun getCoins() {
//overwrote the invoke function earlier for the use case which allows us to call the use case as a function
getCoinsUseCase().onEach { result ->
when (result) {
is Resource.SUCCESS -> {
_state.value =
CoinListState(coins = result.data ?: arrayListOf())
}
is Resource.ERROR -> {
_state.value =
CoinListState(
error = result.message ?: "An unexpected error occurred"
)
}
is Resource.LOADING -> {
_state.value = CoinListState(isLoading = true)
}
}
}.launchIn(viewModelScope)
}
}
CoinsListState data class used in view model
data class CoinListState(
val isLoading: Boolean = false,
val coins: ArrayList<Coin> = arrayListOf(),
val error: String = ""
)
this is my "GetCoinsUseCase" to get the coins
class GetCoinsUseCase #Inject constructor(
private val repository: CoinRepository
) {
// overwriting the operator fun invoke allows us to call the use case
//GetCoinsUseCase as if it was a function, and we return a flow because
// we want to emit states LOADING -> for progress bar, SUCCESS -> attach list of coins,
// and ERROR
operator fun invoke(): kotlinx.coroutines.flow.Flow<Resource<ArrayList<Coin>>> = flow {
try {
emit(Resource.LOADING<ArrayList<Coin>>())
//we mapped it to toCoin because we returning a list of coin, not coinDTO
val coins = repository.getCoins().map { it.toCoin() }
emit(Resource.SUCCESS<ArrayList<Coin>>(coins as ArrayList<Coin>))
}catch (e: HttpException){
emit(Resource.ERROR<ArrayList<Coin>>(e.localizedMessage ?: "An unexpected error occurred"))
}catch (e: IOException){
emit(Resource.ERROR<ArrayList<Coin>>("Couldn't reach server. Check connection"))
}
}
}
just the coin repository that is implemented in another place
interface CoinRepository {
//repository definitions
suspend fun getCoins() : ArrayList<CoinDTO>
suspend fun getCoinById(coinId: String) : CoinDetailDTO
}
This is my domain - Domain - only contains the data needed
data class Coin(
var id: String,
var isActive: Boolean,
var name: String,
var rank: Int,
var symbol: String
)
and this is how I'm mapping it
data class CoinDTO(
val id: String,
#SerializedName("is_active")
val isActive: Boolean,
#SerializedName("is_new")
val isNew: Boolean,
val name: String,
val rank: Int,
val symbol: String,
val type: String
)
fun CoinDTO.toCoin(): Coin {
return Coin(
id = id,
isActive = isActive,
name = name,
rank = rank,
symbol = symbol,
// logo = CoinDetailLogo(logo = String()).logo
)
}
Coin list item if needed for reference, this is what is displayed to the user in the list
#Composable
fun CoinListItem (
coin: Coin,
onItemClick: (Coin) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onItemClick(coin) }
.padding(20.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "${coin.rank}. ${coin.name} (${coin.symbol})",
style = MaterialTheme.typography.body1,
overflow = TextOverflow.Ellipsis
)
Text(
text = if(coin.isActive) "active" else "inactive",
color = if(coin.isActive) Color.Green else Color.Red,
fontStyle = FontStyle.Italic,
textAlign = TextAlign.End,
style = MaterialTheme.typography.body2,
modifier = Modifier.align(CenterVertically)
)
}
}
as well as the "Resource" generic for states
//UIStates
sealed class Resource<T>(val data: T? = null, val message: String? = null) {
class SUCCESS<T>(data: T) : Resource<T>(data)
class ERROR<T>(message: String, data: T? = null) : Resource<T>(data, message)
class LOADING<T>(data: T? = null) : Resource<T>(data)
}
again, given this info, how can I get the function searchCoinList in the view model to correctly view the searched data (name, rank, or symbol) when it is called in the CoinListScreen inside the Search Bar. Thank you so much
It seems like you want to implement a basic instant search functionality. It's pretty easy to achieve using Kotlin's StateFlow and its operators. Consider the following implementation with description:
// CoinListViewModel
private val queryFlow = MutableStateFlow("")
private val coinsList = mutableStateOf<List<Coin>>(listOf())
init {
queryFlow
.debounce(300) // filters out values that are followed by the newer values within the given timeout. The latest value is always emitted.
.filterNot { query -> userInput.isEmpty() } // filter the unwanted string like an empty string in this case to avoid the unnecessary network call.
.distinctUntilChanged() // to avoid duplicate network calls
.flowOn(Dispatchers.IO) // Changes the context where this flow is executed to Dispatchers.IO
.flatMapLatest { query -> // to avoid the network call results which are not needed more for displaying to the user
getCoinsUseCase(query).catch { emitAll(flowOf(emptyList())}
}
.onEach { coins: List<Coin> -> // go through each list of Coins
coinsList.value = coins
}
.launchIn(viewModelScope)
}
fun searchCoinsList(query: String) {
queryFlow.value = query
}

Nestjs + Swagger: How to add a custom option to #ApiProperty

Is it possible to add a custom option to #ApiProperty decorator?
import { ApiProperty } from '#nestjs/swagger';
class Animal {
#ApiProperty({
type: String,
description: 'animal name',
'x-description': 'some information' // how to add a cutom option 'x-description' ?
})
name: string;
}
I'm not sure i fully understand, but If you are talking about openapi's extensions. Take a look at this: https://github.com/nestjs/swagger/issues/195
Solution from https://github.com/nestjs/swagger/issues/195#issuecomment-526215840
import { ApiProperty } from '#nestjs/swagger';
type SwaggerDecoratorParams = Parameters<typeof ApiProperty>;
type SwaggerDecoratorMetadata = SwaggerDecoratorParams[0];
type Options = SwaggerDecoratorMetadata & DictionarySwaggerParameters;
type DictionarySwaggerParameters = { 'x-property'?: string };
export const ApiPropertyX = (params: Options) => {
return ApiProperty(params);
};

LitElement initialize State from Properties without default value

I'm trying out Lit and I'm having trouble figuring out how to initialize a state field from props. Here's a basic contrived pagination component:
export class Pagination extends BaseElement {
static styles = [BaseElement.styles];
#property({type: Number})
selected = 0;
#state()
_selected = this.selected;
#property({type: Number})
total!: number;
#property({type: Number})
pageSize: number = 10;
#state()
_numPages = Math.ceil(this.total / this.pageSize);
render() {
const numPages = Math.ceil(this.total / this.pageSize);
console.log(numPages, this._numPages, this._selected);
return html`
<ul class="pagination">
<li>${msg("Previous", {id: 'pagination.previous', desc: 'Previous button in pagination'})}</li>
${Array.from(Array(_numPages).keys()).map((e, i) => html`<li class=${this._selected === i ? "active" : ""}>${i + 1}</li>`)}
<li>${msg("Next", {id: 'pagination.next', desc: 'Previous button in pagination'})}</li>
</ul>
`;
}
}
This fails in render when using this._numPages (throws RangeError for the map because it's NaN), but is fine with numPages. It seems like if the state is using public properties that have defaults, it works, but otherwise it fails. I think this has to do with the fact that when the element is created it doesn't have props yet, so the initial first render doesn't have a value. But what is the right pattern to achieve this then in that case? The documentation here says "In some cases, internal reactive state may be initialized from public properties—for example, if there is a expensive transformation between the user-visible property and the internal state." (https://lit.dev/docs/components/properties/#public-properties-and-internal-state).
For completeness, here's a snippet of output of tsc:
import { __decorate } from "tslib";
import { html } from 'lit';
import { localized, msg } from '#lit/localize';
import { property } from 'lit/decorators/property.js';
import { state } from 'lit/decorators/state.js';
import { BaseElement } from '../BaseElement.js';
let Pagination = class Pagination extends BaseElement {
constructor() {
super(...arguments);
this.selected = 0;
this._selected = this.selected;
this.pageSize = 10;
this._numPages = Math.ceil(this.total / this.pageSize);
}
render() {
const numPages = Math.ceil(this.total / this.pageSize);
console.log(this._selected, this._numPages);
return html `
<ul class="pagination">
<li>${msg("Previous", { id: 'pagination.previous', desc: 'Previous button in pagination' })}</li>
${Array.from(Array(numPages).keys()).map((e, i) => html `<li class=${this.selected === i ? "active" : ""}>${i + 1}</li>`)}
<li>${msg("Next", { id: 'pagination.next', desc: 'Previous button in pagination' })}</li>
</ul>
`;
}
};
Pagination.styles = [BaseElement.styles];
__decorate([
property({ type: Number })
], Pagination.prototype, "selected", void 0);
__decorate([
state()
], Pagination.prototype, "_selected", void 0);
__decorate([
property({ type: Number })
], Pagination.prototype, "total", void 0);
__decorate([
property({ type: Number })
], Pagination.prototype, "pageSize", void 0);
__decorate([
state()
], Pagination.prototype, "_numPages", void 0);
Pagination = __decorate([
localized()
], Pagination);
export { Pagination };
The #property decorator only declares the property for any lit-element component. To initialize the property with an default value, you need to declare it in Constructor.
I am not clear on your issue, what i understand that _numPages is giving error. So you can declare and initialize it as follow.
Below is your modified code.
export class Pagination extends BaseElement {
static styles = [BaseElement.styles];
#property({type: Number})
selected = 0;
#state()
_selected = this.selected;
#property({type: Number})
total!: number;
#property({type: Number})
pageSize: number = 10;
#property({type: Number})
_numPages: number;
constructor() {
super();
this._numPages = 10
}
#state()
_numPages = Math.ceil(this.total / this.pageSize);
render() {
const numPages = Math.ceil(this.total / this.pageSize);
console.log(numPages, this._numPages, this._selected);
return html`
<ul class="pagination">
<li>${msg("Previous", {id: 'pagination.previous', desc: 'Previous button in pagination'})}</li>
${Array.from(Array(_numPages).keys()).map((e, i) => html`<li class=${this._selected === i ? "active" : ""}>${i + 1}</li>`)}
<li>${msg("Next", {id: 'pagination.next', desc: 'Previous button in pagination'})}</li>
</ul>
`;
}
}
Assigning the values in the class fields feels risky. I would need to review this, but I think this will not use the instance values so it might have unintended side-effects. I would wait until the instance is available (like the constructor), but, setting it in the constructor is also too soon as the property does not yet have the value from the component, rather it has the initial value from the class-field.
So we need to delay this even further.
I've solved this by using the firstUpdated lifecycle hook:
#customElement("my-component")
export class MyComponent extends LitElement {
#property()
myProperty: string = "";
#state()
_internalState: string = "";
override async firstUpdated() {
// avoid unnecessary calls by waiting for any pending updated to complete.
await this.updateComplete;
this._internalState = this.myProperty;
}
...
}

How to pass default parameters to the #Query class in Nest.Js?

I'm trying to pass the default parameters maxnodes=3 and addstats=false to the controller via the #Query parameter in Nest.Js.
The code works fine, but the default parameters are not used. When I pass on the query parameters the ones that are passed are shown, but if none are passed, the default values (3 and false) are not used.
How to fix that?
context.contructor.ts:
import { CreateContextQuery } from './context.query';
import { CreateContextDto } from './context.dto';
#Post('graph')
public async createGraphForContext(
#Body('context') contextData: CreateContextDto,
#Query()
contextQuery: CreateContextQuery,
) {
const before = Date.now();
const { context } = await this.contextService.createContext(contextData);
const graph = await this.contextService.getGraphOfContext(
context.id,
contextQuery.maxnodes,
contextQuery.addstats,
);
}
context.query.ts:
import { ApiProperty } from '#nestjs/swagger';
export class CreateContextQuery {
#ApiProperty({
description: 'Maximum number of nodes to show on the graph',
})
maxnodes;
#ApiProperty({
description: 'Include graph statistics',
})
addstats;
constructor(maxnodes = 3, addstats = false) {
this.maxnodes = maxnodes;
this.addstats = addstats;
}
}
So basically in your DTO, you can give default values.
export class CreateContextQuery {
#IsOptional()
#Type(() => Number)
#IsNumber()
#Min(0)
maxnodes?: number = 3;
#IsOptional()
#Type(() => Boolean)
#IsBoolean()
addstats?: boolean = false;
constructor(maxnodes = 3, addstats = false) {
this.maxnodes = maxnodes;
this.addstats = addstats;
}
}
// as you can see i am using validation too
And in your controller :
#Post('graph')
#UsePipes(new ValidationPipe({ transform: true }))
// you need to add this for tansformation
public async createGraphForContext(
#Body('context') contextData: CreateContextDto,
#Query()
contextQuery: CreateContextQuery,
) {
const before = Date.now();
const { context } = await this.contextService.createContext(contextData);
const graph = await this.contextService.getGraphOfContext(
context.id,
contextQuery.maxnodes,
contextQuery.addstats,
);
}
PS
Also if you want you can add custom decorators, in your case:
// add this decorator
export const GetContextQuery = createParamDecorator((_data: unknown, ctx: ExecutionContext): CreateContextDto => {
const request = ctx.switchToHttp().getRequest();
const query = request.query;
const maxnodes = parseInt(query.maxnodes) || 3;//default values here in case it fails to parse
const addstats = Boolean(query.addstats) || 0;
return { addstats, addstats };
});
and in your controller, you can call the decorator instead of #Query
just add your decorator #GetContextQuery() context: CreateContextDto, and now you do not need the UsePipes
What you receive in the query param is a plain object. You can achieve what you want putting a pipe in your query param and applying a class transform to instantiate the class.
Read this: https://docs.nestjs.com/pipes#providing-defaults
contextQuery isn't an instance of CreateContextQuery because, without any configuration, Nest won't call new CreateContextQuery any time. This is why you end up using pipes (read this https://docs.nestjs.com/techniques/validation#transform-payload-objects)

Support for Multilingual feature using Adaptive Cards in Microsoft Bot framework

I am testing the Multilingual Bot downloaded from Microsoft bot framework. While doing so, some of my content are not getting translated.
Image link
You can see the following code where I have inserted few lines that asks the user if there is anything I can help ? This gets translated in to tthe language selected by the User. But, the content in CardAction() object Title 'Yes' and 'No' are not getting translated.
How to handle such translations in the middleware ?
bool translate = userLanguage != TranslationSettings.DefaultLanguage;
if (IsLanguageChangeRequested(turnContext.Activity.Text))
{
await _accessors.LanguagePreference.SetAsync(turnContext, turnContext.Activity.Text);
var reply = turnContext.Activity.CreateReply($"Your current language code is: {turnContext.Activity.Text}");
await turnContext.SendActivityAsync(reply, cancellationToken);
await _accessors.UserState.SaveChangesAsync(turnContext, false, cancellationToken);
// This content is getting partially translated.
var newRply = turnContext.Activity.CreateReply("Is there anything else I can help you with?");
newRply.SuggestedActions = new SuggestedActions()
{
Actions = new List<CardAction>()
{
// The title is not getting translated
new CardAction() { Title = "Yes", Type = ActionTypes.PostBack, Value = Spanish },
// The title is not getting translated
new CardAction() { Title = "No", Type = ActionTypes.PostBack, Value = English },
},
};
await turnContext.SendActivityAsync(newRply);
}
else
{
var reply = turnContext.Activity.CreateReply("Choose your language:");
reply.SuggestedActions = new SuggestedActions()
{
Actions = new List<CardAction>()
{
new CardAction() { Title = "Español", Type = ActionTypes.PostBack, Value = Spanish },
new CardAction() { Title = "English", Type = ActionTypes.PostBack, Value = English },
},
};
await turnContext.SendActivityAsync(reply);
}
}
Expecting that string in the CardAction() should also be translated into the language chosen by the user.
I assume you are using Microsoft Translator class that comes with the Sample. From the same sample, I implemented a new class (MultilingualCardAction) by inheriting CardAction class.
This works for me but there may be better ways as well.
public class MultilingualCardAction : CardAction
{
private readonly MicrosoftTranslator _translator;
private string _language;
public MultilingualCardAction(string language)
{
_language = language;
_translator = new MicrosoftTranslator(<<YOUR TRANSLATION KEY>>);
}
public string cardTitle
{
get
{
return this.Title;
}
set
{
this.Title = getTranslatedText(value).Result;
}
}
async Task<string> getTranslatedText(string title)
{
return await _translator.TranslateAsync(title, _language);
}
}
Then I created the CardAction object this way.
var newRply = turnContext.Activity.CreateReply("Is there anything else I can help you with?");
newRply.SuggestedActions = new SuggestedActions()
{
Actions = new List<CardAction>()
{
new MultilingualCardAction('es') { cardTitle = "Yes", Type = ActionTypes.PostBack, Value = "Yes" },
new MultilingualCardAction('es') { cardTitle = "No, thanks!", Type = ActionTypes.PostBack, Value = "No" },
},
};
await turnContext.SendActivityAsync(newRply);
Refer the image below.

Resources