Skip to content

Commit

Permalink
Merge branch 'current' into allie/updating-docs-to-reflect-service-to…
Browse files Browse the repository at this point in the history
…ken-changes
  • Loading branch information
matthewshaver authored Sep 11, 2023
2 parents 9ed5f59 + 5d3cea5 commit 0c98632
Show file tree
Hide file tree
Showing 36 changed files with 4,404 additions and 869 deletions.
2 changes: 1 addition & 1 deletion contributing/single-sourcing-content.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ This component can be added directly to a markdown file in a similar way as othe
Both properties can be used together to set a range where the content should show. In the example below, this content will only show if the selected version is between **0.21** and **1.0**:

```markdown
<VersionBlock lastVersion="1.0">
<VersionBlock lastVersion="1.6">

Versioned content here

Expand Down
169 changes: 169 additions & 0 deletions website/api/get-discourse-comments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
const axios = require('axios')
require("dotenv").config();

const { DISCOURSE_DEVBLOG_API_KEY , DISCOURSE_USER_SYSTEM } = process.env
const DEVBLOG_PROD_URL = 'https://docs.getdbt.com/blog/'
const DEV_ENV = 'dev-'
const PREVIEW_ENV = 'deploy-preview-'

// Set API endpoint and headers
let discourse_endpoint = `https://discourse.getdbt.com`
let headers = {
'Accept': 'application/json',
'Api-Key': DISCOURSE_DEVBLOG_API_KEY,
'Api-Username': DISCOURSE_USER_SYSTEM,
}

async function getDiscourseComments(request, response) {
let topicId, comments, DISCOURSE_TOPIC_ID;

const blogUrl = await getBlogUrl(request)

if (blogUrl === DEVBLOG_PROD_URL) {
DISCOURSE_TOPIC_ID = 21
} else {
DISCOURSE_TOPIC_ID = 2
}

try {
const env =
blogUrl === DEVBLOG_PROD_URL
? ""
: blogUrl.includes("localhost")
? DEV_ENV
: PREVIEW_ENV;
const postTitle = `${env}${request.query.title}`;
const postSlug = request.query.slug;
const cleanSlug = cleanUrl(request.query.slug);
const externalId = truncateString(`${env}${cleanSlug}`);

console.table({
blogUrl,
postTitle,
postSlug,
cleanSlug,
externalId,
});


if (!postSlug) throw new Error("Unable to query Discourse API. Error reading slug.");

topicId = await searchDiscourseExternalId(externalId);

// First check if the dev blog post exists in Discourse
// Get the comments if it does
if (typeof topicId === "number") {
comments = await getDiscourseTopicbyID(topicId);
} else {
// If the dev blog post does not exist in Discourse
// Create a new topic and get the comments
topicId = await createDiscourseTopic(postTitle, externalId, cleanSlug, blogUrl, DISCOURSE_TOPIC_ID);
if (typeof topicId === "number") {
comments = await getDiscourseTopicbyID(topicId);
comments.shift();
comments = { topicId, comments };

return await response.status(200).json(comments);
} else {
console.log("Unable to create Discourse topic TopicID is not a number.");
return await response.status(500).json({ error: "Unable to create Discourse topic TopicID is not a number." });
}
}

comments.shift();
comments = { topicId, comments };

return await response.status(200).json(comments);
} catch (err) {
console.log("err on getDiscourseComments", err);
return await response.status(500).json({ error: "Unable to get topics from Discourse." });
}
}

async function createDiscourseTopic(title, externalId, slug, blogUrl, DISCOURSE_TOPIC_ID) {
console.log(`Creating a new topic in Discourse - ${title}`)
try {
const response = await axios.post(`${discourse_endpoint}/posts`, {
title: title,
raw: `This is a companion discussion topic for the original entry at ${blogUrl}${slug}`,
category: DISCOURSE_TOPIC_ID,
embed_url: `${blogUrl}${slug}`,
external_id: externalId,
tags: ['devblog'],
visible: false
}, { headers })

let topicId = await response.data.topic_id

console.log('Topic successfully created with topic_id', topicId)

return topicId

} catch(err) {
console.log('err on createDiscourseTopic', err)
return err
}
}

async function getDiscourseTopicbyID(topicId) {
console.log(`Topic found setting topic id - ${topicId}`)
try {
let response = await axios.get(`${discourse_endpoint}/t/${topicId}.json`, { headers })
let { data } = await response
let post_stream = data.post_stream
let post_count = data.posts_count

// If there is more than one comment make the topic visibile in Discourse
if (post_count > 1 && data.visible === false) {
console.log(`Topic has more than one comment. Changing visibility to visible.`)
await axios.put(`${discourse_endpoint}/t/${topicId}`, {
visible: true
}, { headers })
}

// Filter only 'regular' posts in Discourse. (e.g. not moderator actions, small_actions, whispers)
post_stream.posts = post_stream.posts.filter(post => post.post_type === 1)

return post_stream.posts
} catch(err) {
console.log('err on getDiscourseTopicbyID', err)
return err
}
}

async function searchDiscourseExternalId(externalId) {
console.log(`Searching for external_id in Discourse - ${externalId}`);
try {
const data = await axios.get(`${discourse_endpoint}/t/external_id/${externalId}.json`, { headers });
return data.data.id;
} catch (err) {
if (err.response.status === 404) {
console.log("No topics found in Discourse.");
return null;
}
console.log("Unable to search Discourse for external_id.", err);
return err;
}
}


// Truncate external_id to 50 characters per Discourse API requirements
function truncateString(str) {
if (str.length <= 50) {
return str
}
return str.slice(0, 50)
}

// Remove query params and hash from URL to prevent duplicate topics
function cleanUrl(url) {
return url.split("?")[0].split("#")[0];
}

// Create a function to get the host name from the request and add /blog/ to the end
async function getBlogUrl(req) {
const host = req.headers.host
return `https://${host}/blog/`
}

module.exports = getDiscourseComments;
136 changes: 136 additions & 0 deletions website/api/get-discourse-topics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
const axios = require('axios')

async function getDiscourseTopics(request, response) {
const { DISCOURSE_API_KEY , DISCOURSE_USER } = process.env

const body = request.body

try {
// Set API endpoint and headers
let discourse_endpoint = `https://discourse.getdbt.com`
let headers = {
'Accept': 'application/json',
'Api-Key': DISCOURSE_API_KEY,
'Api-Username': DISCOURSE_USER,
}

const query = buildQueryString(body)
if(!query) throw new Error('Unable to build query string.')

// Get topics from Discourse
let { data: { posts, topics } } = await axios.get(`${discourse_endpoint}/search?q=${query}`, { headers })

// Return empty array if no topics found for search query
// 200 status is used to prevent triggering Datadog alerts
if(!topics || topics?.length <= 0) {
// Log message with encoded query and end function
console.log('Unable to get results from api request.')
console.log(`Search query: ${query}`)
return await response.status(200).json([])
}

// Set author and like_count for topics if not querying by specific term
let allTopics = topics
if(!body?.term) {
allTopics = topics.reduce((topicsArr, topic) => {
// Get first post in topic
const firstTopicPost = posts?.find(post =>
post?.post_number === 1 &&
post?.topic_id === topic?.id
)
// If post found
// Get username
if(firstTopicPost?.username) {
topic.author = firstTopicPost.username
}
// Get like count
if(firstTopicPost?.like_count) {
topic.like_count = firstTopicPost.like_count
}

if(firstTopicPost?.blurb) {
topic.blurb = firstTopicPost.blurb
}

// Push updated topic to array
topicsArr.push(topic)

return topicsArr
}, [])
}

// Return topics
//return await returnResponse(200, allTopics)
return await response.status(200).json(allTopics)
} catch(err) {
// Log and return the error
console.log('err', err)
return await response.status(500).json({ error: 'Unable to get topics from Discourse.'})
}
}

function buildQueryString(body) {
if(!body) return null

// start with empty query string
let query = ''

// check param and apply to query if set
for (const [key, value] of Object.entries(body)) {
// validate categories
// if valid, add to query string
if(validateItem({ key, value })) {
if(key === 'category') {
query += `#${value} `
} else if(key === 'inString') {
query += `in:${value}`
} else if(key === 'status' && Array.isArray(value)) {
value?.map(item => {
query += `${key}:${item} `
})
} else {
query += `${key}:${value} `
}
}
}

if(query) {
const encodedQuery = encodeURIComponent(query)
return encodedQuery
}
}

function validateItem({ key, value }) {
// predefined Discourse values
// https://docs.discourse.org/#tag/Search/operation/search
const inStringValues = ['title', 'first', 'pinned', 'wiki']
const orderValues = ['latest', 'likes', 'views', 'latest_topic']
const statusValues = ['open', 'closed', 'public', 'archived', 'noreplies', 'single_user', 'solved', 'unsolved']

// validate keys
if(key === 'inString') {
return inStringValues.includes(value)
? true
: false
} else if(key === 'order') {
return orderValues.includes(value)
? true
: false
} else if(key === 'status') {
if(Array.isArray(value)) {
let isValid = true
value?.map(item => {
if(!statusValues.includes(item)) isValid = false
})
return isValid
} else {
return statusValues.includes(value)
? true
: false
}
} else {
return true
}
}

module.exports = getDiscourseTopics
4 changes: 0 additions & 4 deletions website/dbt-versions.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@ exports.versions = [
version: "1.1",
EOLDate: "2023-04-28",
},
{
version: "1.0",
EOLDate: "2022-12-03"
},
]

exports.versionedPages = [
Expand Down
2 changes: 1 addition & 1 deletion website/docs/docs/build/derived-metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ In MetricFlow, derived metrics are metrics created by defining an expression usi
| `metrics` | The list of metrics used in the derived metrics. | Required |
| `alias` | Optional alias for the metric that you can use in the expr. | Optional |
| `filter` | Optional filter to apply to the metric. | Optional |
| `offset_window` | Set the period for the offset window, such as 1 month. This will return the value of the metric one month from the metric time. This can't be used with `offset_to_grain`. | Required |
| `offset_window` | Set the period for the offset window, such as 1 month. This will return the value of the metric one month from the metric time. | Required |

The following displays the complete specification for derived metrics, along with an example.

Expand Down
6 changes: 0 additions & 6 deletions website/docs/docs/build/incremental-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,6 @@ A `unique_key` enables updating existing rows instead of just appending new rows

Not specifying a `unique_key` will result in append-only behavior, which means dbt inserts all rows returned by the model's SQL into the preexisting target table without regard for whether the rows represent duplicates.

<VersionBlock lastVersion="1.0">

The optional `unique_key` parameter specifies a field that can uniquely identify each row within your model. You can define `unique_key` in a configuration block at the top of your model. If your model doesn't contain a single field that is unique, but rather a combination of columns, we recommend that you create a single column that can serve as a unique identifier (by concatenating and hashing those columns), and pass it into your model's configuration.

</VersionBlock>

<VersionBlock firstVersion="1.1">

The optional `unique_key` parameter specifies a field (or combination of fields) that define the grain of your model. That is, the field(s) identify a single unique row. You can define `unique_key` in a configuration block at the top of your model, and it can be a single column name or a list of column names.
Expand Down
23 changes: 20 additions & 3 deletions website/docs/docs/build/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,18 +284,35 @@ packages:
</File>

### Local packages
Packages that you have stored locally can be installed by specifying the path to the project, like so:
A "local" package is a dbt project accessible from your local file system. You can install it by specifying the project's path. It works best when you nest the project within a subdirectory relative to your current project's directory.

<File name='packages.yml'>

```yaml
packages:
- local: /opt/dbt/redshift # use a local path
- local: relative/path/to/subdirectory
```
</File>
Local packages should only be used for specific situations, for example, when testing local changes to a package.
Other patterns may work in some cases, but not always. For example, if you install this project as a package elsewhere, or try running it on a different system, the relative and absolute paths will yield the same results.
<File name='packages.yml'>
```yaml
packages:
# not recommended - support for these patterns vary
- local: /../../redshift # relative path to a parent directory
- local: /opt/dbt/redshift # absolute path on the system
```
</File>
There are a few specific use cases where we recommend using a "local" package:
1. **Monorepo** &mdash; When you have multiple projects, each nested in a subdirectory, within a monorepo. "Local" packages allow you to combine projects for coordinated development and deployment.
2. **Testing changes** &mdash; To test changes in one project or package within the context of a downstream project or package that uses it. By temporarily switching the installation to a "local" package, you can make changes to the former and immediately test them in the latter for quicker iteration. This is similar to [editable installs](https://pip.pypa.io/en/stable/topics/local-project-installs/) in Python.
3. **Nested project** &mdash; When you have a nested project that defines fixtures and tests for a project of utility macros, like [the integration tests within the `dbt-utils` package](https://github.com/dbt-labs/dbt-utils/tree/main/integration_tests).


## What packages are available?
Check out [dbt Hub](https://hub.getdbt.com) to see the library of published dbt packages!
Expand Down
Loading

0 comments on commit 0c98632

Please sign in to comment.