Adding gitlab ci file.

This commit is contained in:
Dan Chadwick
2024-03-10 21:44:34 +00:00
parent d55838a927
commit 0d77a3eeea
219 changed files with 15260 additions and 130 deletions

View File

@@ -0,0 +1,2 @@
.idea
.idea/

View File

@@ -0,0 +1,39 @@
## JSON Ingestion Examples
The data is pulled from https://jsonplaceholder.typicode.com/
### Approach 1: Migrate API
#### Requirements
- Migrate
- Migrate Plus
There are 2 migrations:
- images (5000 total)
- json_posts_to_posts (100 total)
To run them you can do:
```
drush mim images --limit=100
drush mim json_posts_to_posts
```
there are only 100 posts, so for our purposes we don't need more than 100 images.
Once both migrations have been run you can see the data at `/migration-posts` which is controller by a view.
This approach has the advantage of requiring migrate API which pulls in a lot of helpful things,
such as rollbacks, update old items, only importing new items...etc.
### Approach 2: Custom Drush Command
Will import the first 100 images only, followed by 100 posts.
Also available for viewing on `/migration-posts` after import.
```
drush jie-posts
```
### Approach 3: Views JSON Source
#### Requirements
- views_json_source
Make sure `views_json_source` module is included in your codebase.
Then visit `/views-json-source` after enabling this module which is a preconfigured view of titles and body fields from external JSON.
### Approach 4: Javascript Only
1. Attach a javascript library to a page
2. Fetch the API data and render it all client side

View File

@@ -0,0 +1,16 @@
{
"name": "drupal/json_ingestion_examples",
"type": "drupal-module",
"license": "GPL-2.0-or-later",
"authors": [
{
"name": "Dan Chadwick",
"email": "dan.chadwick@acquia.com"
}
],
"require": {
"drupal/core": "^9 || ^10",
"drupal/migrate_plus": "^6.0",
"drupal/views_json_source": "^1.4"
}
}

View File

@@ -0,0 +1,87 @@
langcode: en
status: true
dependencies:
config:
- field.field.node.post.body
- field.field.node.post.field_image
- image.style.thumbnail
- node.type.post
module:
- image
- path
- text
id: node.post.default
targetEntityType: node
bundle: post
mode: default
content:
body:
type: text_textarea_with_summary
weight: 121
region: content
settings:
rows: 9
summary_rows: 3
placeholder: ''
show_summary: false
third_party_settings: { }
created:
type: datetime_timestamp
weight: 10
region: content
settings: { }
third_party_settings: { }
field_image:
type: image_image
weight: 1
region: content
settings:
progress_indicator: throbber
preview_image_style: thumbnail
third_party_settings: { }
path:
type: path
weight: 30
region: content
settings: { }
third_party_settings: { }
promote:
type: boolean_checkbox
weight: 15
region: content
settings:
display_label: true
third_party_settings: { }
status:
type: boolean_checkbox
weight: 120
region: content
settings:
display_label: true
third_party_settings: { }
sticky:
type: boolean_checkbox
weight: 16
region: content
settings:
display_label: true
third_party_settings: { }
title:
type: string_textfield
weight: -5
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
uid:
type: entity_reference_autocomplete
weight: 5
region: content
settings:
match_operator: CONTAINS
match_limit: 10
size: 60
placeholder: ''
third_party_settings: { }
hidden: { }

View File

@@ -0,0 +1,41 @@
langcode: en
status: true
dependencies:
config:
- field.field.node.post.body
- field.field.node.post.field_image
- image.style.wide
- node.type.post
module:
- image
- text
- user
id: node.post.default
targetEntityType: node
bundle: post
mode: default
content:
body:
type: text_default
label: hidden
settings: { }
third_party_settings: { }
weight: 101
region: content
field_image:
type: image
label: hidden
settings:
image_link: ''
image_style: wide
image_loading:
attribute: eager
third_party_settings: { }
weight: -1
region: content
links:
settings: { }
third_party_settings: { }
weight: 100
region: content
hidden: { }

View File

@@ -0,0 +1,43 @@
langcode: en
status: true
dependencies:
config:
- core.entity_view_mode.node.teaser
- field.field.node.post.body
- field.field.node.post.field_image
- image.style.medium
- node.type.post
module:
- image
- text
- user
id: node.post.teaser
targetEntityType: node
bundle: post
mode: teaser
content:
body:
type: text_summary_or_trimmed
label: hidden
settings:
trim_length: 600
third_party_settings: { }
weight: 101
region: content
field_image:
type: image
label: hidden
settings:
image_link: content
image_style: medium
image_loading:
attribute: lazy
third_party_settings: { }
weight: -1
region: content
links:
settings: { }
third_party_settings: { }
weight: 100
region: content
hidden: { }

View File

@@ -0,0 +1,23 @@
langcode: en
status: true
dependencies:
config:
- field.storage.node.body
- node.type.post
module:
- text
id: node.post.body
field_name: body
entity_type: node
bundle: post
label: Body
description: ''
required: false
translatable: true
default_value: { }
default_value_callback: ''
settings:
display_summary: true
required_summary: false
allowed_formats: { }
field_type: text_with_summary

View File

@@ -0,0 +1,37 @@
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_image
- node.type.post
module:
- image
id: node.post.field_image
field_name: field_image
entity_type: node
bundle: post
label: Image
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings:
handler: 'default:file'
handler_settings: { }
file_directory: '[date:custom:Y]-[date:custom:m]'
file_extensions: 'png gif jpg jpeg webp'
max_filesize: ''
max_resolution: ''
min_resolution: ''
alt_field: true
alt_field_required: true
title_field: false
title_field_required: false
default_image:
uuid: ''
alt: ''
title: ''
width: null
height: null
field_type: image

View File

@@ -0,0 +1,33 @@
langcode: en
status: true
dependencies:
module:
- file
- image
- node
_core:
default_config_hash: EymokncRIZ7SgQT2IdOQhQJicX4nNc0K89ik-LxmOHE
id: node.field_image
field_name: field_image
entity_type: node
type: image
settings:
target_type: file
display_field: false
display_default: false
uri_scheme: public
default_image:
uuid: ''
alt: ''
title: ''
width: null
height: null
module: image
locked: false
cardinality: 1
translatable: true
indexes:
target_id:
- target_id
persist_with_no_fields: false
custom_storage: false

View File

@@ -0,0 +1,17 @@
langcode: en
status: true
dependencies:
module:
- menu_ui
third_party_settings:
menu_ui:
available_menus:
- main
parent: 'main:'
name: Post
type: post
description: ''
help: ''
new_revision: true
preview_mode: 1
display_submitted: true

View File

@@ -0,0 +1,191 @@
langcode: en
status: true
dependencies:
config:
- core.entity_view_mode.node.teaser
- node.type.post
module:
- node
- user
id: migration_based_posts
label: 'Migration Based Posts'
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
id: default
display_title: Default
display_plugin: default
position: 0
display_options:
title: 'Migration Based Posts'
fields:
title:
id: title
table: node_field_data
field: title
relationship: none
group_type: group
admin_label: ''
entity_type: node
entity_field: title
plugin_id: field
label: ''
exclude: false
alter:
alter_text: false
make_link: false
absolute: false
word_boundary: false
ellipsis: false
strip_tags: false
trim: false
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: string
settings:
link_to_entity: true
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
pager:
type: mini
options:
offset: 0
items_per_page: 10
total_pages: null
id: 0
tags:
next:
previous:
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
empty: { }
sorts:
created:
id: created
table: node_field_data
field: created
relationship: none
group_type: group
admin_label: ''
entity_type: node
entity_field: created
plugin_id: date
order: DESC
expose:
label: ''
field_identifier: ''
exposed: false
granularity: second
arguments: { }
filters:
status:
id: status
table: node_field_data
field: status
entity_type: node
entity_field: status
plugin_id: boolean
value: '1'
group: 1
expose:
operator: ''
type:
id: type
table: node_field_data
field: type
entity_type: node
entity_field: type
plugin_id: bundle
value:
post: post
style:
type: default
row:
type: 'entity:node'
options:
view_mode: teaser
query:
type: views_query
options:
query_comment: ''
disable_sql_rewrite: false
distinct: false
replica: false
query_tags: { }
relationships: { }
header: { }
footer: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags: { }
page_1:
id: page_1
display_title: Page
display_plugin: page
position: 1
display_options:
display_extenders: { }
path: migration-posts
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags: { }

View File

@@ -0,0 +1,199 @@
langcode: en
status: true
dependencies:
module:
- views_json_source
id: views_json_source_demo
label: 'Views JSON Source'
module: views
description: ''
tag: ''
base_table: json
base_field: ''
display:
default:
id: default
display_title: Default
display_plugin: default
position: 0
display_options:
title: 'Views JSON Source'
fields:
value:
id: value
table: json
field: value
relationship: none
group_type: group
admin_label: ''
entity_type: null
entity_field: null
plugin_id: views_json_source_field
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: h2
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
key: title
trusted_html: 0
value_1:
id: value_1
table: json
field: value
relationship: none
group_type: group
admin_label: ''
plugin_id: views_json_source_field
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
key: body
trusted_html: 0
pager:
type: mini
options:
offset: 0
items_per_page: 10
total_pages: null
id: 0
tags:
next:
previous:
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
access:
type: none
options: { }
cache:
type: tag
options: { }
empty: { }
sorts: { }
arguments: { }
filters: { }
style:
type: default
row:
type: fields
query:
type: views_query
options:
json_file: 'https://jsonplaceholder.typicode.com/posts'
row_apath: /
headers: ''
single_payload: 0
show_errors: 1
relationships: { }
header: { }
footer: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_interface'
- url.query_args
tags: { }
page_1:
id: page_1
display_title: Page
display_plugin: page
position: 1
display_options:
display_extenders: { }
path: views-json-source
cache_metadata:
max-age: -1
contexts:
- 'languages:language_interface'
- url.query_args
tags: { }

View File

@@ -0,0 +1,5 @@
services:
json_ingestion_examples.commands:
class: Drupal\json_ingestion_examples\Drush\Commands\JsonIngestionCommands
tags:
- { name: drush.command }

View File

@@ -0,0 +1,9 @@
name: JSON Ingestion Examples
type: module
package: custom
description: 'Ways to import JSON data to Drupal'
core_version_requirement: ^9 || ^10
dependencies:
- drupal:migrate
- drupal:migrate_plus
- drupal:views_json_source

View File

@@ -0,0 +1,6 @@
<?php
function json_ingestion_examples_install() {
$mod_installer = \Drupal::service('module_installer');
$mod_installer->install(['migrate', 'migrate_plus', 'views_json_source']);
}

View File

@@ -0,0 +1,54 @@
id: images
label: 'Import remote images.'
source:
plugin: url
data_fetcher_plugin: http
data_parser_plugin: json
urls: 'https://jsonplaceholder.typicode.com/photos'
item_selector: /
constants:
file_dest_dir: 'public://migrated-images/'
jpeg: '.jpeg'
ids:
image_id:
type: string
fields:
-
name: image_id
label: 'Image ID'
selector: /id
-
name: image_title
label: 'Image URL'
selector: /title
-
name: image_url
label: 'Image URL'
selector: /url
process:
_prep_filename:
-
plugin: callback
callable: basename
source: image_url
filename:
plugin: concat
source:
- '@_prep_filename'
- constants/jpeg
_prep_file_destination:
plugin: concat
source:
- constants/file_dest_dir
- '@filename'
uri:
plugin: file_copy
source:
- image_url
- '@_prep_file_destination'
status:
plugin: default_value
default_value: 1
alt: image_title
destination:
plugin: 'entity:file'

View File

@@ -0,0 +1,37 @@
id: json_posts_to_posts
label: 'JSON Migrate - Posts to Posts'
migration_group: migrate_posts
source:
plugin: url
data_fetcher_plugin: http
data_parser_plugin: json
urls: 'https://jsonplaceholder.typicode.com/posts'
item_selector: /
ids:
post_id:
type: string
fields:
-
name: post_id
label: 'Post ID'
selector: /id
-
name: post_title
label: 'Post Title'
selector: /title
-
name: post_body
label: 'Post Body'
selector: /body
process:
title: post_title
body/value: post_body
field_image:
plugin: migration_lookup
migration: images
source: post_id # this gets an image with the same ID as the post.
destination:
plugin: 'entity:node'
default_bundle: post
migration_dependencies: { }

View File

@@ -0,0 +1,90 @@
<?php
namespace Drupal\json_ingestion_examples\Drush\Commands;
use Drupal\Core\File\FileSystemInterface;
use Drupal\file\Entity\File;
use Drupal\node\Entity\Node;
use Drush\Commands\DrushCommands;
/**
* Drush command file.
*/
class JsonIngestionCommands extends DrushCommands {
/**
* A custom Drush command to displays the given text.
*
* @command json-ingestion-examples:update-posts
* The amount to iterate over
* @aliases jie-posts
*/
public function updatePosts() {
$this->importImages();
$this->importPosts();
}
/**
* Imports all images.
*/
public function importImages() {
// Go fetch JSON for images.
$http_client = \Drupal::httpClient();
$images_json_url = 'https://jsonplaceholder.typicode.com/photos';
$images_json = $http_client->request('GET', $images_json_url)->getBody()->getContents();
// Reduce to 100 images as only have 100 posts.
$images_array = array_slice(json_decode($images_json), 0, 100);
$directory = 'public://drush-generated';
/** @var \Drupal\Core\File\FileSystemInterface $file_system */
$file_system = \Drupal::service('file_system');
// Make sure directory exists.
$file_system->prepareDirectory($directory, FileSystemInterface:: CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
foreach ($images_array as $img_ob) {
// Download file, always replace.
$file = file_get_contents($img_ob->url);
$filename = basename($img_ob->url) . '.jpeg';
$filepath = $directory . '/' . $filename;
$file_system->saveData($file, $filepath, FileSystemInterface::EXISTS_REPLACE);
// Create file entity.
$file = File::create([
'filename' => $filename,
'uri' => $filepath,
'status' => 1,
'uid' => 1,
]);
if ($file->save()) {
$this->writeln("File $filename created");
}
}
}
/**
* Imports all posts.
*/
public function importPosts() {
// Go fetch JSON for posts.
$http_client = \Drupal::httpClient();
$node_storage = \Drupal::entityTypeManager()->getStorage('node');
$posts_json_url = 'https://jsonplaceholder.typicode.com/posts';
$posts_json = $http_client->request('GET', $posts_json_url)->getBody()->getContents();
$posts_array = json_decode($posts_json);
foreach ($posts_array as $post) {
$node_check = $node_storage->loadByProperties([
'type' => 'post',
'title' => $post->title
]);
// If title already exists, skip.
if ($node_check) {
continue;
}
$new_post = Node::create(['type' => 'post']);
$new_post->set('title', $post->title);
$new_post->set('body', $post->body);
$new_post->set('field_image', $post->id);
$new_post->enforceIsNew();
if ($new_post->save()) {
$this->writeln("New post created: $post->title");
}
}
}
}